diff --git a/src/main/index.ts b/src/main/index.ts index f05cb9e..2c13ac6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -165,100 +165,18 @@ app.whenReady().then(async () => { const tray = services.get(TrayServiceToken) as TrayService tray.initialize() - const logLevelSetting = ctx.settings.getValue('log_level') as logLevel - if (logLevelSetting) { - ctx.logger.setLevel(logLevelSetting) - } - - ctx.logger.info('Application starting...') - - const mainConsole = console as any - mainConsole.log = (...args: any[]) => ctx.logger.info(String(args[0] ?? ''), ...args.slice(1)) - mainConsole.info = (...args: any[]) => - ctx.logger.info(String(args[0] ?? ''), ...args.slice(1)) - mainConsole.warn = (...args: any[]) => - ctx.logger.warn(String(args[0] ?? ''), ...args.slice(1)) - mainConsole.error = (...args: any[]) => - ctx.logger.error(String(args[0] ?? ''), ...args.slice(1)) - mainConsole.debug = (...args: any[]) => - ctx.logger.debug(String(args[0] ?? ''), ...args.slice(1)) - mainConsole.trace = (...args: any[]) => - ctx.logger.debug('console.trace', { args, stack: new Error('console.trace').stack }) - - if (!config.isDev) { - try { - if (!fs.existsSync(config.themeDir)) { - fs.mkdirSync(config.themeDir, { recursive: true }) - } - const existing = fs - .readdirSync(config.themeDir) - .filter((f) => f.toLowerCase().endsWith('.json')) - if (existing.length === 0) { - const builtinThemeDir = join(app.getAppPath(), 'themes') - if (fs.existsSync(builtinThemeDir)) { - const files = fs - .readdirSync(builtinThemeDir) - .filter((f) => f.toLowerCase().endsWith('.json')) - for (const f of files) { - const src = join(builtinThemeDir, f) - const dest = join(config.themeDir, f) - try { - fs.copyFileSync(src, dest) - } catch (e: any) { - ctx.logger.warn('Failed to copy builtin theme', { - src, - dest, - message: e?.message - }) - } - } - } - } - } catch (e: any) { - ctx.logger.warn('Failed to initialize theme directory', { message: e?.message }) + // Open Global Sidebar on startup + ctx.windows.open({ + key: 'global-sidebar', + title: 'SecScore Sidebar', + route: '/global-sidebar', + options: { + transparent: true, + alwaysOnTop: true, + hasShadow: false, + type: 'toolbar' } - } - - const uncaughtExceptionHandler = (err: any) => { - ctx.logger.error('uncaughtException', { - message: err?.message, - stack: err?.stack - }) - } - process.on('uncaughtException', uncaughtExceptionHandler) - ctx.effect(() => process.removeListener('uncaughtException', uncaughtExceptionHandler)) - - const unhandledRejectionHandler = (reason: any) => { - if (reason instanceof Error) { - ctx.logger.error('unhandledRejection', { message: reason.message, stack: reason.stack }) - } else { - ctx.logger.error('unhandledRejection', reason) - } - } - process.on('unhandledRejection', unhandledRejectionHandler) - ctx.effect(() => process.removeListener('unhandledRejection', unhandledRejectionHandler)) - - const renderProcessGoneHandler = (_: any, __: any, details: any) => { - ctx.logger.error('render-process-gone', details) - } - app.on('render-process-gone', renderProcessGoneHandler) - ctx.effect(() => app.removeListener('render-process-gone', renderProcessGoneHandler)) - - const childProcessGoneHandler = (_: any, details: any) => { - ctx.logger.error('child-process-gone', details) - } - app.on('child-process-gone', childProcessGoneHandler) - ctx.effect(() => app.removeListener('child-process-gone', childProcessGoneHandler)) - - ctx.windows.open({ key: 'main', title: 'SecScore', route: '/' }) - - const activateHandler = () => { - if (!ctx.windows.get('main')) { - ctx.windows.open({ key: 'main', title: 'SecScore', route: '/' }) - } - } - app.on('activate', activateHandler) - ctx.effect(() => app.removeListener('activate', activateHandler)) + }) }) const host = await builder.build() diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts index 32ca28c..779562b 100644 --- a/src/main/services/WindowManager.ts +++ b/src/main/services/WindowManager.ts @@ -51,7 +51,10 @@ export class WindowManager extends Service { public open(input: windowOpenInput) { const existing = this.get(input.key) if (existing) { - if (input.route) void this.loadRoute(existing, input.route) + if (input.route) { + // Use soft navigation if window exists to prevent reload + existing.webContents.send('app:navigate', input.route) + } existing.show() existing.focus() return existing @@ -72,6 +75,25 @@ export class WindowManager extends Service { ...input.options }) + // Special positioning for global sidebar + if (input.key === 'global-sidebar') { + const { screen } = require('electron') + const primaryDisplay = screen.getPrimaryDisplay() + const { width, height } = primaryDisplay.workAreaSize + const winWidth = 84 + const winHeight = 300 + win.setBounds({ + x: width - winWidth, + y: Math.floor(height / 2 - winHeight / 2), + width: winWidth, + height: winHeight + }) + win.setAlwaysOnTop(true, 'screen-saver') + win.setVisibleOnAllWorkspaces(true) + win.setSkipTaskbar(true) + win.setResizable(false) + } + const zoom = Number(this.mainCtx.settings.getValue('window_zoom')) || 1.0 win.webContents.setZoomFactor(zoom) @@ -112,13 +134,13 @@ export class WindowManager extends Service { public navigate(key: string, route: string) { const win = this.get(key) if (!win) return false - void this.loadRoute(win, route) + win.webContents.send('app:navigate', route) return true } public navigateWindow(win: BrowserWindow, route: string) { if (win.isDestroyed()) return false - void this.loadRoute(win, route) + win.webContents.send('app:navigate', route) return true } @@ -210,5 +232,20 @@ export class WindowManager extends Service { } } }) + + this.mainCtx.handle('window:resize', (event, width: number, height: number) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win) { + const bounds = win.getBounds() + // Keep right side pinned + const newX = bounds.x + (bounds.width - width) + win.setBounds({ + x: newX, + y: bounds.y, + width, + height + }) + } + }) } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 7789203..ca64a29 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -80,7 +80,13 @@ const api = { ipcRenderer.on('window:maximized-changed', subscription) return () => ipcRenderer.removeListener('window:maximized-changed', subscription) }, + onNavigate: (callback: (route: string) => void) => { + const subscription = (_event: any, route: string) => callback(route) + ipcRenderer.on('app:navigate', subscription) + return () => ipcRenderer.removeListener('app:navigate', subscription) + }, toggleDevTools: () => ipcRenderer.invoke('window:toggle-devtools'), + windowResize: (width: number, height: number) => ipcRenderer.invoke('window:resize', width, height), // Logger queryLogs: (lines?: number) => ipcRenderer.invoke('log:query', lines), diff --git a/src/preload/types.ts b/src/preload/types.ts index a92958f..485a7f9 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -122,7 +122,9 @@ export interface electronApi { windowClose: () => Promise windowIsMaximized: () => Promise onWindowMaximizedChanged: (callback: (maximized: boolean) => void) => () => void + onNavigate: (callback: (route: string) => void) => () => void toggleDevTools: () => Promise + windowResize: (width: number, height: number) => Promise // Logger queryLogs: (lines?: number) => Promise> diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 193ba21..788a019 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,15 +1,30 @@ import { Layout, Dialog, Input, MessagePlugin } from 'tdesign-react' import { useEffect, useMemo, useState } from 'react' -import { HashRouter, useLocation, useNavigate } from 'react-router-dom' +import { HashRouter, useLocation, useNavigate, Routes, Route } from 'react-router-dom' import { Sidebar } from './components/Sidebar' import { ContentArea } from './components/ContentArea' import { Wizard } from './components/Wizard' import { ThemeProvider } from './contexts/ThemeContext' +import { GlobalSidebar } from './components/GlobalSidebar' function MainContent(): React.JSX.Element { const navigate = useNavigate() const location = useLocation() + useEffect(() => { + if (!(window as any).api) return + const unlisten = (window as any).api.onNavigate((route: string) => { + // 统一路径格式进行比对,防止 / 和 /home 导致重复跳转 + const currentPath = location.pathname === '/' ? '/home' : location.pathname + const targetPath = route === '/' ? '/home' : route + + if (currentPath !== targetPath) { + navigate(route) + } + }) + return () => unlisten() + }, [navigate, location.pathname]) + const [wizardVisible, setWizardVisible] = useState(false) const [permission, setPermission] = useState<'admin' | 'points' | 'view'>('view') const [hasAnyPassword, setHasAnyPassword] = useState(false) @@ -129,7 +144,10 @@ function App(): React.JSX.Element { return ( - + + } /> + } /> + ) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 9617bae..7940374 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -96,3 +96,40 @@ html[theme-mode='dark'] .ss-sidebar .t-menu__item.t-is-active svg:not([fill='non text-align: center; justify-content: center; } + +/* Global Sidebar Toggle Button */ +.global-sidebar-toggle { + width: 24px; + height: 60px; + background-color: var(--ss-card-bg); + border-radius: 8px 0 0 8px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: -2px 0 8px rgba(0,0,0,0.1); + border: 1px solid var(--ss-border-color); + border-right: none; + color: var(--ss-text-main); + z-index: 10; + flex-shrink: 0; + animation: twinkle-animation 3s infinite ease-in-out; + transition: background-color 0.3s, color 0.3s; +} + +.global-sidebar-toggle:hover { + background-color: var(--ss-item-hover); + color: var(--td-brand-color); +} + +@keyframes twinkle-animation { + 0%, 100% { + box-shadow: -2px 0 8px rgba(0,0,0,0.1); + filter: brightness(1); + } + 50% { + box-shadow: -4px 0 12px var(--td-brand-color-light), 0 0 15px var(--td-brand-color-focus); + filter: brightness(1.2); + color: var(--td-brand-color); + } +} diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx new file mode 100644 index 0000000..ba50a1c --- /dev/null +++ b/src/renderer/src/components/GlobalSidebar.tsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react' +import { Button, Tooltip } from 'tdesign-react' +import { + HomeIcon, + ViewListIcon, + UserAddIcon, + ChevronRightIcon, + ChevronLeftIcon, + SettingIcon +} from 'tdesign-icons-react' + +export const GlobalSidebar: React.FC = () => { + const [expanded, setExpanded] = useState(false) + + useEffect(() => { + if (!(window as any).api) return + if (expanded) { + (window as any).api.windowResize(84, 300) + } else { + (window as any).api.windowResize(24, 300) + } + }, [expanded]) + + const openMain = () => { + if (!(window as any).api) return + (window as any).api.openWindow({ key: 'main', route: '/' }) + } + + const openQuickPoint = () => { + // 统一使用 / 路径,因为主页即是积分操作页 + openMain() + } + + const openLeaderboard = () => { + if (!(window as any).api) return + (window as any).api.openWindow({ key: 'main', route: '/leaderboard' }) + } + + return ( +
+
+ {/* 侧边栏内容区 */} +
+ + + + + + + + + + + + + + + +
+ + {/* 展开/收起手柄 */} +
setExpanded(!expanded)} + className="global-sidebar-toggle" + > + {expanded ? : } +
+
+
+ ) +}