From 0d86f38ed717bc0034d1332d684784955e6f8307 Mon Sep 17 00:00:00 2001 From: PANDA-JSR Date: Tue, 17 Mar 2026 20:54:14 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=8A=A0gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 7207c4e..dc6ef01 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules # Build outputs dist dist-ssr +out *.local # Tauri @@ -15,6 +16,11 @@ src-tauri/gen # Database data.sql +*.sqlite +*.sqlite3 +*.db +*.db-shm +*.db-wal # Logs logs From d9617bacc18cdc8f2bbb6992575e5782bc943e3a Mon Sep 17 00:00:00 2001 From: PANDA-JSR Date: Tue, 17 Mar 2026 21:40:03 +0800 Subject: [PATCH 2/4] fix(tauri): restore window controls and expose renderer api --- .gitignore | 1 + src/components/ContentArea.tsx | 6 +++++- src/components/WindowControls.tsx | 27 +++++++++++++++++++++------ src/main.tsx | 5 +++++ src/preload/types.ts | 25 +++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index dc6ef01..c09cea9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ out src-tauri/target src-tauri/Cargo.lock src-tauri/gen +.cargo-local/ # Database data.sql diff --git a/src/components/ContentArea.tsx b/src/components/ContentArea.tsx index b175d49..b968003 100644 --- a/src/components/ContentArea.tsx +++ b/src/components/ContentArea.tsx @@ -2,6 +2,7 @@ import React, { Suspense, lazy } from "react" import { Layout, Space, Button, Tag, Spin } from "antd" import { Routes, Route, Navigate } from "react-router-dom" import { useTranslation } from "react-i18next" +import { WindowControls } from "./WindowControls" const Home = lazy(() => import("./Home").then((m) => ({ default: m.Home }))) const StudentManager = lazy(() => @@ -67,6 +68,7 @@ export function ContentArea({ background: "var(--ss-header-bg)", borderBottom: "1px solid var(--ss-border-color)", flexShrink: 0, + WebkitAppRegion: "drag", } as React.CSSProperties } > @@ -76,7 +78,8 @@ export function ContentArea({ { display: "flex", alignItems: "center", - paddingRight: "12px", + paddingRight: "8px", + WebkitAppRegion: "no-drag", } as React.CSSProperties } > @@ -94,6 +97,7 @@ export function ContentArea({ )} + diff --git a/src/components/WindowControls.tsx b/src/components/WindowControls.tsx index b6c4596..b5eeba9 100644 --- a/src/components/WindowControls.tsx +++ b/src/components/WindowControls.tsx @@ -11,13 +11,28 @@ export function WindowControls(): React.JSX.Element { const [isMaximized, setIsMaximized] = useState(false) useEffect(() => { - if (!(window as any).api) return - ;(window as any).api.windowIsMaximized().then((v: boolean) => setIsMaximized(v)) + const api = (window as any).api + if (!api) return - const cleanup = (window as any).api.onWindowMaximizedChanged((maximized: boolean) => { - setIsMaximized(maximized) - }) - return cleanup + let unlisten: (() => void) | null = null + + api + .windowIsMaximized() + .then((v: boolean) => setIsMaximized(v)) + .catch(() => void 0) + + api + .onWindowMaximizedChanged((maximized: boolean) => { + setIsMaximized(maximized) + }) + .then((fn: () => void) => { + unlisten = fn + }) + .catch(() => void 0) + + return () => { + if (unlisten) unlisten() + } }, []) const minimize = () => { diff --git a/src/main.tsx b/src/main.tsx index 0f8455c..03fb68f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,6 +7,11 @@ import App from "./App" import { ClientContext } from "./ClientContext" import { StudentService } from "./services/StudentService" import { ServiceProvider } from "./contexts/ServiceContext" +import { api } from "./preload/types" + +if (!(window as any).api) { + ;(window as any).api = api +} const ctx = new ClientContext() new StudentService(ctx) diff --git a/src/preload/types.ts b/src/preload/types.ts index 8179ae5..3d97cf4 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -173,6 +173,9 @@ const api = { windowMaximize: (): Promise => invoke("window_maximize"), windowClose: (): Promise => invoke("window_close"), windowIsMaximized: (): Promise => invoke("window_is_maximized"), + toggleDevTools: (): Promise => invoke("toggle_devtools"), + windowResize: (width: number, height: number): Promise => + invoke("window_resize", { width, height }), onWindowMaximizedChanged: (callback: (maximized: boolean) => void): Promise => { return listen("window:maximized-changed", (event) => { callback(event.payload) @@ -291,6 +294,28 @@ const api = { ruleIds: number[] ): Promise<{ success: boolean; data?: boolean; message?: string }> => invoke("auto_score_sort_rules", { ruleIds }), + + // Generic invoke wrapper for backward compatibility with callers using `api.invoke` + invoke: async (channel: string, ...args: any[]): Promise => { + switch (channel) { + case "auto-score:getRules": + return api.autoScoreGetRules() + case "auto-score:addRule": + return api.autoScoreAddRule(args[0]) + case "auto-score:updateRule": + return api.autoScoreUpdateRule(args[0]) + case "auto-score:deleteRule": + return api.autoScoreDeleteRule(args[0]) + case "auto-score:toggleRule": + return api.autoScoreToggleRule(args[0]) + case "auto-score:sortRules": + return api.autoScoreSortRules(args[0]) + case "auto-score:getStatus": + return api.autoScoreGetStatus() + default: + throw new Error(`Unsupported legacy invoke channel: ${channel}`) + } + }, } export default api From d12fb33b2a050a5e08db318341f6daf261155e54 Mon Sep 17 00:00:00 2001 From: PANDA-JSR Date: Tue, 17 Mar 2026 21:40:38 +0800 Subject: [PATCH 3/4] chore: commit all remaining changes --- src-tauri/src/commands/data.rs | 1 - src-tauri/src/commands/event.rs | 2 +- src-tauri/src/commands/reason.rs | 2 +- src-tauri/src/commands/window.rs | 2 -- src-tauri/src/lib.rs | 4 ++-- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/commands/data.rs b/src-tauri/src/commands/data.rs index 1a4f81b..266219e 100644 --- a/src-tauri/src/commands/data.rs +++ b/src-tauri/src/commands/data.rs @@ -1,5 +1,4 @@ use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; use std::sync::Arc; use tauri::State; diff --git a/src-tauri/src/commands/event.rs b/src-tauri/src/commands/event.rs index f0dec05..b095006 100644 --- a/src-tauri/src/commands/event.rs +++ b/src-tauri/src/commands/event.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, Duration, TimeZone, Timelike, Utc}; +use chrono::{Datelike, Duration, Timelike, Utc}; use parking_lot::RwLock; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set, diff --git a/src-tauri/src/commands/reason.rs b/src-tauri/src/commands/reason.rs index dfa772d..cbb7010 100644 --- a/src-tauri/src/commands/reason.rs +++ b/src-tauri/src/commands/reason.rs @@ -1,5 +1,5 @@ use parking_lot::RwLock; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryOrder, Set}; +use sea_orm::{ActiveModelTrait, EntityTrait, QueryOrder, Set}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tauri::State; diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 9377ae0..2b9f6d1 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -4,8 +4,6 @@ use tauri::{AppHandle, Manager}; use crate::state::AppState; -use super::response::IpcResponse; - #[tauri::command] pub async fn window_minimize( app: AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0303812..c9e06f0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -29,7 +29,7 @@ pub fn setup_app(app: &mut App) -> Result<(), Box> { } fn setup_database(app: &mut App) -> Result<(), Box> { - let handle = app.handle(); + let handle = app.handle().clone(); let db_path = if cfg!(debug_assertions) { std::path::PathBuf::from("data.sql") } else { @@ -52,7 +52,7 @@ fn setup_database(app: &mut App) -> Result<(), Box> { match create_sqlite_connection(&db_path_str).await { Ok(conn) => { let state = handle.state::(); - let mut state_guard = state.write(); + let state_guard = state.write(); let mut db_guard = state_guard.db.write(); *db_guard = Some(conn); eprintln!("Database connected to: {}", db_path_str); From feb35427285ab4b3936d89361ce6c842a43ad2fe Mon Sep 17 00:00:00 2001 From: PANDA-JSR Date: Tue, 17 Mar 2026 22:22:45 +0800 Subject: [PATCH 4/4] fix(frontend): stabilize tauri listeners and logging payloads --- index.html | 1 + public/favicon.svg | 11 +++++++++ src-tauri/src/main.rs | 2 +- src/App.tsx | 36 +++++++++++++++++++++------- src/components/Settings.tsx | 40 +++++++++++++++++++++---------- src/components/Sidebar.tsx | 31 +++++++++++++++++------- src/components/TitleBar.tsx | 33 ++++++++++++++++++++----- src/components/WindowControls.tsx | 6 +++++ src/contexts/ThemeContext.tsx | 33 +++++++++++++++++-------- src/main.tsx | 2 +- src/preload/types.ts | 10 +++++--- vite.config.ts | 4 ++++ 12 files changed, 159 insertions(+), 50 deletions(-) create mode 100644 public/favicon.svg diff --git a/index.html b/index.html index 63d0740..5dc77af 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + SecScore diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..ae889c5 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 774c263..e47aa28 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,9 +9,9 @@ fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .setup(|app| { - setup_app(app)?; let state = AppState::new(app.handle().clone()); app.manage(Arc::new(RwLock::new(state))); + setup_app(app)?; Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/src/App.tsx b/src/App.tsx index a825e06..38a99c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,16 +15,34 @@ function MainContent(): React.JSX.Element { const [messageApi, contextHolder] = message.useMessage() useEffect(() => { - if (!(window as any).api) return - const unlisten = (window as any).api.onNavigate((route: string) => { - const currentPath = location.pathname === "/" ? "/home" : location.pathname - const targetPath = route === "/" ? "/home" : route + const api = (window as any).api + if (!api) return - if (currentPath !== targetPath) { - navigate(route) - } - }) - return () => unlisten() + let disposed = false + let unlisten: (() => void) | null = null + + api + .onNavigate((route: string) => { + const currentPath = location.pathname === "/" ? "/home" : location.pathname + const targetPath = route === "/" ? "/home" : route + + if (currentPath !== targetPath) { + navigate(route) + } + }) + .then((fn: () => void) => { + if (disposed) { + fn() + return + } + unlisten = fn + }) + .catch(() => void 0) + + return () => { + disposed = true + if (unlisten) unlisten() + } }, [navigate, location.pathname]) const [wizardVisible, setWizardVisible] = useState(false) diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index ccc231c..25f6ee7 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -95,20 +95,36 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission useEffect(() => { loadAll() - if (!(window as any).api) return - const unsubscribe = (window as any).api.onSettingChanged((change: any) => { - setSettings((prev) => { - if (change?.key === "log_level") return { ...prev, log_level: change.value } - if (change?.key === "is_wizard_completed") - return { ...prev, is_wizard_completed: change.value } - if (change?.key === "window_zoom") return { ...prev, window_zoom: change.value } - if (change?.key === "auto_score_enabled") - return { ...prev, auto_score_enabled: change.value } - return prev + const api = (window as any).api + if (!api) return + + let disposed = false + let unlisten: (() => void) | null = null + + api + .onSettingChanged((change: any) => { + setSettings((prev) => { + if (change?.key === "log_level") return { ...prev, log_level: change.value } + if (change?.key === "is_wizard_completed") + return { ...prev, is_wizard_completed: change.value } + if (change?.key === "window_zoom") return { ...prev, window_zoom: change.value } + if (change?.key === "auto_score_enabled") + return { ...prev, auto_score_enabled: change.value } + return prev + }) }) - }) + .then((fn: () => void) => { + if (disposed) { + fn() + return + } + unlisten = fn + }) + .catch(() => void 0) + return () => { - if (typeof unsubscribe === "function") unsubscribe() + disposed = true + if (unlisten) unlisten() } }, []) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 4d34745..a3a508d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -39,15 +39,30 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps): const handleStatusChange = () => { loadDbStatus() } - if ((window as any).api) { - const unsubscribe = (window as any).api.onSettingChanged( - (change: { key: string; value: any }) => { - if (change.key === "pg_connection_status") { - handleStatusChange() - } + const api = (window as any).api + if (!api) return + + let disposed = false + let unlisten: (() => void) | null = null + + api + .onSettingChanged((change: { key: string; value: any }) => { + if (change.key === "pg_connection_status") { + handleStatusChange() } - ) - return unsubscribe + }) + .then((fn: () => void) => { + if (disposed) { + fn() + return + } + unlisten = fn + }) + .catch(() => void 0) + + return () => { + disposed = true + if (unlisten) unlisten() } }, []) diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 1b333eb..75e592f 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -16,13 +16,34 @@ export function TitleBar({ children }: TitleBarProps): React.JSX.Element { const [isMaximized, setIsMaximized] = useState(false) useEffect(() => { - if (!(window as any).api) return - ;(window as any).api.windowIsMaximized().then((v: boolean) => setIsMaximized(v)) + const api = (window as any).api + if (!api) return - const cleanup = (window as any).api.onWindowMaximizedChanged((maximized: boolean) => { - setIsMaximized(maximized) - }) - return cleanup + let disposed = false + let unlisten: (() => void) | null = null + + api + .windowIsMaximized() + .then((v: boolean) => setIsMaximized(v)) + .catch(() => void 0) + + api + .onWindowMaximizedChanged((maximized: boolean) => { + setIsMaximized(maximized) + }) + .then((fn: () => void) => { + if (disposed) { + fn() + return + } + unlisten = fn + }) + .catch(() => void 0) + + return () => { + disposed = true + if (unlisten) unlisten() + } }, []) const minimize = () => { diff --git a/src/components/WindowControls.tsx b/src/components/WindowControls.tsx index b5eeba9..f24d01e 100644 --- a/src/components/WindowControls.tsx +++ b/src/components/WindowControls.tsx @@ -14,6 +14,7 @@ export function WindowControls(): React.JSX.Element { const api = (window as any).api if (!api) return + let disposed = false let unlisten: (() => void) | null = null api @@ -26,11 +27,16 @@ export function WindowControls(): React.JSX.Element { setIsMaximized(maximized) }) .then((fn: () => void) => { + if (disposed) { + fn() + return + } unlisten = fn }) .catch(() => void 0) return () => { + disposed = true if (unlisten) unlisten() } }, []) diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 23b3e59..3f9b4e0 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -120,23 +120,36 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre }, [applyThemeConfig]) useEffect(() => { - if (!(window as any).api) return + const api = (window as any).api + if (!api) return + ;(async () => { await loadThemes() await loadCurrentTheme() })() - const unsubscribe = (window as any).api.onThemeChanged((theme: themeConfig) => { - setCurrentTheme(theme) - currentThemeRef.current = theme - applyThemeConfig(theme) - loadThemes() - }) + let disposed = false + let unlisten: (() => void) | null = null + + api + .onThemeChanged((theme: themeConfig) => { + setCurrentTheme(theme) + currentThemeRef.current = theme + applyThemeConfig(theme) + loadThemes() + }) + .then((fn: () => void) => { + if (disposed) { + fn() + return + } + unlisten = fn + }) + .catch(() => void 0) return () => { - if (typeof unsubscribe === "function") { - unsubscribe() - } + disposed = true + if (unlisten) unlisten() } }, [applyThemeConfig, loadCurrentTheme, loadThemes]) diff --git a/src/main.tsx b/src/main.tsx index 03fb68f..a0d52c0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -24,7 +24,7 @@ const safeWriteLog = (payload: { try { const api = (window as any).api if (!api?.writeLog) return - api.writeLog(payload) + Promise.resolve(api.writeLog(payload)).catch(() => void 0) } catch { return } diff --git a/src/preload/types.ts b/src/preload/types.ts index 3d97cf4..908cd63 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -188,15 +188,19 @@ const api = { }, // Logger - queryLogs: (params?: { lines?: number }): Promise<{ success: boolean; data: string[] }> => - invoke("log_query", { params }), + queryLogs: ( + input?: number | { lines?: number } + ): Promise<{ success: boolean; data: string[] }> => { + const lines = typeof input === "number" ? input : input?.lines + return invoke("log_query", { lines }) + }, clearLogs: (): Promise<{ success: boolean }> => invoke("log_clear"), setLogLevel: (level: string): Promise<{ success: boolean }> => invoke("log_set_level", { level }), writeLog: (payload: { level: string message: string meta?: any - }): Promise<{ success: boolean }> => invoke("log_write", payload), + }): Promise<{ success: boolean }> => invoke("log_write", { payload }), // Database Connection dbTestConnection: ( diff --git a/vite.config.ts b/vite.config.ts index de7e70b..efac6a8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,6 +9,10 @@ export default defineConfig({ "@": resolve(__dirname, "src"), }, }, + optimizeDeps: { + // Avoid scanning legacy `old-ss` entries under project root. + entries: ["index.html"], + }, build: { outDir: "dist", emptyOutDir: true,