@@ -765,7 +809,9 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
{/* 理由内容 */}
-
+
@@ -773,7 +819,14 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
value={reasonContent}
onChange={setReasonContent}
placeholder="输入加分/扣分的原因(可选)"
- suffixIcon={reasonContent ?
setReasonContent('')} style={{ cursor: 'pointer' }} /> : undefined}
+ suffixIcon={
+ reasonContent ? (
+ setReasonContent('')}
+ style={{ cursor: 'pointer' }}
+ />
+ ) : undefined
+ }
/>
@@ -782,18 +835,40 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
0 ? 'var(--td-success-color-1)' : customScore < 0 ? 'var(--td-error-color-1)' : 'var(--ss-bg-color)',
+ backgroundColor:
+ customScore > 0
+ ? 'var(--td-success-color-1)'
+ : customScore < 0
+ ? 'var(--td-error-color-1)'
+ : 'var(--ss-bg-color)',
borderRadius: '8px',
border: `1px solid ${customScore > 0 ? 'var(--td-success-color-2)' : customScore < 0 ? 'var(--td-error-color-2)' : 'var(--ss-border-color)'}`,
marginTop: '4px'
}}
>
-
+
变更预览:
{selectedStudent.name}{' '}
-
0 ? 'var(--td-success-color)' : customScore < 0 ? 'var(--td-error-color)' : 'inherit' }}>
+ 0
+ ? 'var(--td-success-color)'
+ : customScore < 0
+ ? 'var(--td-error-color)'
+ : 'inherit'
+ }}
+ >
{customScore > 0 ? `+${customScore}` : customScore}
{' '}
分
diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx
index 0c15db9..a1ddd90 100644
--- a/src/renderer/src/components/Settings.tsx
+++ b/src/renderer/src/components/Settings.tsx
@@ -18,6 +18,7 @@ type permissionLevel = 'admin' | 'points' | 'view'
type appSettings = {
is_wizard_completed: boolean
log_level: 'debug' | 'info' | 'warn' | 'error'
+ window_zoom?: string
}
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
@@ -25,7 +26,8 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
const [activeTab, setActiveTab] = useState('appearance')
const [settings, setSettings] = useState({
is_wizard_completed: false,
- log_level: 'info'
+ log_level: 'info',
+ window_zoom: '1.0'
})
const [securityStatus, setSecurityStatus] = useState<{
@@ -91,6 +93,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
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 }
return prev
})
})
@@ -268,6 +271,39 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
))}
+
+
+
+
+ 调节应用界面的整体大小。
+
+
diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx
index 26aca87..9f30b8f 100644
--- a/src/renderer/src/components/Sidebar.tsx
+++ b/src/renderer/src/components/Sidebar.tsx
@@ -1,5 +1,12 @@
import { Layout, Menu } from 'tdesign-react'
-import { UserIcon, SettingIcon, HistoryIcon, RootListIcon, ViewListIcon, HomeIcon } from 'tdesign-icons-react'
+import {
+ UserIcon,
+ SettingIcon,
+ HistoryIcon,
+ RootListIcon,
+ ViewListIcon,
+ HomeIcon
+} from 'tdesign-icons-react'
import appLogo from '../assets/logo.svg'
const { Aside } = Layout
From f57597ca57f200252dcdaf5a28387867e0c474a0 Mon Sep 17 00:00:00 2001
From: Linkon <116425752+Linkon-lcw@users.noreply.github.com>
Date: Sun, 18 Jan 2026 22:06:28 +0800
Subject: [PATCH 3/7] =?UTF-8?q?feat(=E7=AA=97=E5=8F=A3=E7=AE=A1=E7=90=86):?=
=?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E7=B3=BB=E7=BB=9F=E6=89=98=E7=9B=98?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E7=AA=97=E5=8F=A3?=
=?UTF-8?q?=E5=85=B3=E9=97=AD=E8=A1=8C=E4=B8=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 TrayService 实现系统托盘功能
- 主窗口关闭时默认隐藏而非退出
- 添加 window_zoom 配置项支持窗口缩放
- 引入 TrayServiceToken 作为依赖注入标识
- 通过 isQuitting 状态控制应用退出行为
---
src/main/context.ts | 2 ++
src/main/hosting/index.ts | 3 +-
src/main/hosting/tokens.ts | 1 +
src/main/index.ts | 12 ++++++-
src/main/services/TrayService.ts | 58 ++++++++++++++++++++++++++++++
src/main/services/WindowManager.ts | 8 +++++
src/preload/types.ts | 1 +
7 files changed, 83 insertions(+), 2 deletions(-)
create mode 100644 src/main/services/TrayService.ts
diff --git a/src/main/context.ts b/src/main/context.ts
index da3b5dc..05ff78f 100644
--- a/src/main/context.ts
+++ b/src/main/context.ts
@@ -2,6 +2,8 @@ import { Context as BaseContext } from '../shared/kernel'
import { ipcMain } from 'electron'
export class MainContext extends BaseContext {
+ public isQuitting = false
+
constructor() {
super()
}
diff --git a/src/main/hosting/index.ts b/src/main/hosting/index.ts
index 3236dc5..cab2910 100644
--- a/src/main/hosting/index.ts
+++ b/src/main/hosting/index.ts
@@ -13,7 +13,8 @@ export {
EventRepositoryToken,
SettlementRepositoryToken,
ThemeServiceToken,
- WindowManagerToken
+ WindowManagerToken,
+ TrayServiceToken
} from './tokens'
export type {
appRuntimeContext,
diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts
index 6321d8b..a28ad91 100644
--- a/src/main/hosting/tokens.ts
+++ b/src/main/hosting/tokens.ts
@@ -12,3 +12,4 @@ export const EventRepositoryToken = Symbol.for('secscore.eventRepository')
export const SettlementRepositoryToken = Symbol.for('secscore.settlementRepository')
export const ThemeServiceToken = Symbol.for('secscore.themeService')
export const WindowManagerToken = Symbol.for('secscore.windowManager')
+export const TrayServiceToken = Symbol.for('secscore.trayService')
diff --git a/src/main/index.ts b/src/main/index.ts
index e647fdd..f05cb9e 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -14,6 +14,7 @@ import { AuthService } from './services/AuthService'
import { DataService } from './services/DataService'
import { ThemeService } from './services/ThemeService'
import { WindowManager, type windowManagerOptions } from './services/WindowManager'
+import { TrayService } from './services/TrayService'
import { StudentRepository } from './repos/StudentRepository'
import { ReasonRepository } from './repos/ReasonRepository'
import { EventRepository } from './repos/EventRepository'
@@ -31,7 +32,8 @@ import {
SettingsStoreToken,
StudentRepositoryToken,
ThemeServiceToken,
- WindowManagerToken
+ WindowManagerToken,
+ TrayServiceToken
} from './hosting'
type mainAppConfig = {
@@ -137,6 +139,10 @@ app.whenReady().then(async () => {
WindowManagerToken,
(p) => new WindowManager(p.get(MainContext), config.window)
)
+ services.addSingleton(
+ TrayServiceToken,
+ (p) => new TrayService(p.get(MainContext), config.window)
+ )
})
.configure(async (_builderContext, appCtx) => {
const services = appCtx.services
@@ -156,6 +162,8 @@ app.whenReady().then(async () => {
services.get(SettlementRepositoryToken)
services.get(ThemeServiceToken)
services.get(WindowManagerToken)
+ const tray = services.get(TrayServiceToken) as TrayService
+ tray.initialize()
const logLevelSetting = ctx.settings.getValue('log_level') as logLevel
if (logLevelSetting) {
@@ -254,12 +262,14 @@ app.whenReady().then(async () => {
})
const host = await builder.build()
+ const ctx = host.services.get(MainContext) as MainContext
await host.start()
let disposing = false
const beforeQuitHandler = () => {
if (disposing) return
disposing = true
+ ctx.isQuitting = true
app.removeListener('before-quit', beforeQuitHandler)
void host.dispose()
}
diff --git a/src/main/services/TrayService.ts b/src/main/services/TrayService.ts
new file mode 100644
index 0000000..c732695
--- /dev/null
+++ b/src/main/services/TrayService.ts
@@ -0,0 +1,58 @@
+import { Service } from '../../shared/kernel'
+import { MainContext } from '../context'
+import { Tray, Menu, app, nativeImage } from 'electron'
+import type { windowManagerOptions } from './WindowManager'
+
+export class TrayService extends Service {
+ private tray: Tray | null = null
+
+ constructor(
+ ctx: MainContext,
+ private readonly opts: windowManagerOptions
+ ) {
+ super(ctx, 'tray')
+ }
+
+ private get mainCtx() {
+ return this.ctx as MainContext
+ }
+
+ public initialize() {
+ const icon = nativeImage.createFromPath(this.opts.icon)
+ this.tray = new Tray(icon)
+
+ const contextMenu = Menu.buildFromTemplate([
+ {
+ label: '显示主窗口',
+ click: () => {
+ this.showMainWindow()
+ }
+ },
+ { type: 'separator' },
+ {
+ label: '退出 SecScore',
+ click: () => {
+ app.quit()
+ }
+ }
+ ])
+
+ this.tray.setToolTip('SecScore 积分管理')
+ this.tray.setContextMenu(contextMenu)
+
+ this.tray.on('double-click', () => {
+ this.showMainWindow()
+ })
+ }
+
+ private showMainWindow() {
+ const mainWin = this.mainCtx.windows.get('main')
+ if (mainWin) {
+ if (mainWin.isMinimized()) mainWin.restore()
+ mainWin.show()
+ mainWin.focus()
+ } else {
+ this.mainCtx.windows.open({ key: 'main', title: 'SecScore', route: '/' })
+ }
+ }
+}
diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts
index 4e869d7..32ca28c 100644
--- a/src/main/services/WindowManager.ts
+++ b/src/main/services/WindowManager.ts
@@ -76,6 +76,14 @@ export class WindowManager extends Service {
win.webContents.setZoomFactor(zoom)
this.windows.set(input.key, win)
+
+ win.on('close', (event) => {
+ if (!this.mainCtx.isQuitting && input.key === 'main') {
+ event.preventDefault()
+ win.hide()
+ }
+ })
+
win.on('closed', () => {
this.windows.delete(input.key)
})
diff --git a/src/preload/types.ts b/src/preload/types.ts
index a6f7e69..a92958f 100644
--- a/src/preload/types.ts
+++ b/src/preload/types.ts
@@ -20,6 +20,7 @@ export interface themeConfig {
export type settingsSpec = {
is_wizard_completed: boolean
log_level: logLevel
+ window_zoom: number
}
export type settingsKey = keyof settingsSpec
From df742a60771c5ae45caa7fd4f1eaad7701e8c32c Mon Sep 17 00:00:00 2001
From: Linkon <116425752+Linkon-lcw@users.noreply.github.com>
Date: Sun, 18 Jan 2026 22:51:37 +0800
Subject: [PATCH 4/7] =?UTF-8?q?feat(=E5=85=A8=E5=B1=80=E4=BE=A7=E8=BE=B9?=
=?UTF-8?q?=E6=A0=8F):=20=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80=E4=BE=A7?=
=?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=AA=97=E5=8F=A3?=
=?UTF-8?q?=E5=AF=BC=E8=88=AA=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增全局侧边栏组件,支持展开/收起和快速导航功能
- 添加窗口大小调整和导航事件通信接口
- 优化窗口导航逻辑,使用软导航避免页面重载
- 调整主窗口启动逻辑,默认打开全局侧边栏
---
src/main/index.ts | 104 ++--------------
src/main/services/WindowManager.ts | 43 ++++++-
src/preload/index.ts | 6 +
src/preload/types.ts | 2 +
src/renderer/src/App.tsx | 22 +++-
src/renderer/src/assets/main.css | 37 ++++++
src/renderer/src/components/GlobalSidebar.tsx | 116 ++++++++++++++++++
7 files changed, 232 insertions(+), 98 deletions(-)
create mode 100644 src/renderer/src/components/GlobalSidebar.tsx
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 ? : }
+
+
+
+ )
+}
From 1a3f3812d5497ef144b0e728c510fe7ad8812277 Mon Sep 17 00:00:00 2001
From: Linkon <116425752+Linkon-lcw@users.noreply.github.com>
Date: Sun, 18 Jan 2026 23:04:55 +0800
Subject: [PATCH 5/7] =?UTF-8?q?feat(=E4=BE=A7=E8=BE=B9=E6=A0=8F):=20?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BE=A7=E8=BE=B9=E6=A0=8F=E5=B1=95=E5=BC=80?=
=?UTF-8?q?=E6=94=B6=E8=B5=B7=E5=8A=A8=E7=94=BB=E5=92=8C=E4=BA=A4=E4=BA=92?=
=?UTF-8?q?=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
重构侧边栏组件,将展开/收起逻辑拆分为独立函数,添加平滑过渡动画
新增显示/隐藏状态控制,优化用户体验
调整CSS样式以支持新的动画效果
---
src/renderer/src/assets/main.css | 37 ++++-
src/renderer/src/components/GlobalSidebar.tsx | 149 +++++++++---------
2 files changed, 109 insertions(+), 77 deletions(-)
diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css
index 7940374..28e027f 100644
--- a/src/renderer/src/assets/main.css
+++ b/src/renderer/src/assets/main.css
@@ -114,12 +114,41 @@ html[theme-mode='dark'] .ss-sidebar .t-menu__item.t-is-active svg:not([fill='non
z-index: 10;
flex-shrink: 0;
animation: twinkle-animation 3s infinite ease-in-out;
- transition: background-color 0.3s, color 0.3s;
+ transition: opacity 0.1s ease, transform 0.1s ease;
}
-.global-sidebar-toggle:hover {
- background-color: var(--ss-item-hover);
- color: var(--td-brand-color);
+.global-sidebar-toggle.hidden {
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(10px);
+}
+
+.sidebar-content-area {
+ display: flex;
+ flex-direction: column;
+ backgroundColor: var(--ss-card-bg);
+ border-radius: 12px 0 0 12px;
+ box-shadow: -4px 0 16px rgba(0,0,0,0.15);
+ border: 1px solid var(--ss-border-color);
+ border-right: none;
+ padding: 12px 8px;
+ gap: 12px;
+ width: 60px;
+ transition: opacity 0.15s ease, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
+ backface-visibility: hidden;
+ transform: translateZ(0);
+}
+
+.sidebar-content-area.hidden {
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(20px);
+}
+
+.sidebar-content-area.visible {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateX(0);
}
@keyframes twinkle-animation {
diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx
index ba50a1c..ee978f3 100644
--- a/src/renderer/src/components/GlobalSidebar.tsx
+++ b/src/renderer/src/components/GlobalSidebar.tsx
@@ -11,34 +11,47 @@ import {
export const GlobalSidebar: React.FC = () => {
const [expanded, setExpanded] = useState(false)
+ const [showToggle, setShowToggle] = useState(true)
- 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 handleExpand = () => {
+ // 1. 先隐藏三角
+ setShowToggle(false)
+
+ // 2. 稍后扩大窗口
+ setTimeout(() => {
+ if ((window as any).api) {
+ (window as any).api.windowResize(84, 300)
+ }
+ // 3. 最后显示侧边栏内容
+ setTimeout(() => {
+ setExpanded(true)
+ }, 20)
+ }, 100)
+ }
+
+ const handleCollapse = () => {
+ // 1. 先隐藏侧边栏内容
+ setExpanded(false)
+
+ // 2. 稍后缩小窗口
+ setTimeout(() => {
+ if ((window as any).api) {
+ (window as any).api.windowResize(24, 300)
+ }
+ // 3. 最后重新显示三角
+ setTimeout(() => {
+ setShowToggle(true)
+ }, 20)
+ }, 150)
+ }
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 (
{
alignItems: 'center',
justifyContent: 'flex-end',
overflow: 'hidden',
- background: 'transparent',
- userSelect: 'none',
- position: 'fixed',
- right: 0,
- top: 0
+ background: 'transparent'
}}
>
-
+
+
+
+ {/* 侧边栏内容 */}
+
- {/* 侧边栏内容区 */}
-
-
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
- {/* 展开/收起手柄 */}
-
setExpanded(!expanded)}
- className="global-sidebar-toggle"
- >
- {expanded ? : }
-
+
+
+
)
From 8f79653e06787fb4eecdfa534333deab61bf75db Mon Sep 17 00:00:00 2001
From: Linkon <116425752+Linkon-lcw@users.noreply.github.com>
Date: Sun, 18 Jan 2026 23:26:34 +0800
Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BE=A7?=
=?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=8A=A8=E7=94=BB=E5=BB=B6=E8=BF=9F=E5=92=8C?=
=?UTF-8?q?=E6=80=A7=E8=83=BD=EF=BC=8C=E5=AE=9E=E7=8E=B0=E8=B7=AF=E7=94=B1?=
=?UTF-8?q?=E6=87=92=E5=8A=A0=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
调整侧边栏动画延迟时间以匹配透明度动画,添加contain: paint优化性能
将路由组件改为懒加载模式并添加加载状态
优化学生搜索功能,预计算拼音数据提升性能
---
src/renderer/src/assets/main.css | 5 +-
src/renderer/src/components/ContentArea.tsx | 65 +++++++++-----
src/renderer/src/components/GlobalSidebar.tsx | 4 +-
src/renderer/src/components/Home.tsx | 88 ++++++++++---------
4 files changed, 93 insertions(+), 69 deletions(-)
diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css
index 28e027f..37f2d7e 100644
--- a/src/renderer/src/assets/main.css
+++ b/src/renderer/src/assets/main.css
@@ -114,7 +114,7 @@ html[theme-mode='dark'] .ss-sidebar .t-menu__item.t-is-active svg:not([fill='non
z-index: 10;
flex-shrink: 0;
animation: twinkle-animation 3s infinite ease-in-out;
- transition: opacity 0.1s ease, transform 0.1s ease;
+ transition: opacity 0.07s ease, transform 0.15s ease;
}
.global-sidebar-toggle.hidden {
@@ -134,9 +134,10 @@ html[theme-mode='dark'] .ss-sidebar .t-menu__item.t-is-active svg:not([fill='non
padding: 12px 8px;
gap: 12px;
width: 60px;
- transition: opacity 0.15s ease, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
+ transition: opacity 0.1s ease, transform 0.1s cubic-bezier(0.34, 1.56, 0.64, 1);
backface-visibility: hidden;
transform: translateZ(0);
+ contain: paint; /* 性能优化:限制重绘范围 */
}
.sidebar-content-area.hidden {
diff --git a/src/renderer/src/components/ContentArea.tsx b/src/renderer/src/components/ContentArea.tsx
index 11da34a..a811a6d 100644
--- a/src/renderer/src/components/ContentArea.tsx
+++ b/src/renderer/src/components/ContentArea.tsx
@@ -1,14 +1,16 @@
-import { Layout, Space, Button, Tag } from 'tdesign-react'
+import React, { Suspense, lazy } from 'react'
+import { Layout, Space, Button, Tag, Loading } from 'tdesign-react'
import { Routes, Route, Navigate } from 'react-router-dom'
-import { Home } from './Home'
-import { StudentManager } from './StudentManager'
-import { Settings } from './Settings'
-import { ReasonManager } from './ReasonManager'
-import { ScoreManager } from './ScoreManager'
-import { Leaderboard } from './Leaderboard'
-import { SettlementHistory } from './SettlementHistory'
import { WindowControls } from './WindowControls'
+const Home = lazy(() => import('./Home').then(m => ({ default: m.Home })))
+const StudentManager = lazy(() => import('./StudentManager').then(m => ({ default: m.StudentManager })))
+const Settings = lazy(() => import('./Settings').then(m => ({ default: m.Settings })))
+const ReasonManager = lazy(() => import('./ReasonManager').then(m => ({ default: m.ReasonManager })))
+const ScoreManager = lazy(() => import('./ScoreManager').then(m => ({ default: m.ScoreManager })))
+const Leaderboard = lazy(() => import('./Leaderboard').then(m => ({ default: m.Leaderboard })))
+const SettlementHistory = lazy(() => import('./SettlementHistory').then(m => ({ default: m.SettlementHistory })))
+
const { Content } = Layout
interface ContentAreaProps {
@@ -85,22 +87,37 @@ export function ContentArea({
-
- }
- />
- } />
- }
- />
- } />
- } />
- } />
- } />
- } />
-
+
+
+
+ }
+ >
+
+ }
+ />
+ } />
+ }
+ />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
)
diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx
index ee978f3..d7bc00d 100644
--- a/src/renderer/src/components/GlobalSidebar.tsx
+++ b/src/renderer/src/components/GlobalSidebar.tsx
@@ -38,10 +38,10 @@ export const GlobalSidebar: React.FC = () => {
if ((window as any).api) {
(window as any).api.windowResize(24, 300)
}
- // 3. 最后重新显示三角
+ // 3. 最后重新显示三角(等待透明度动画完成)
setTimeout(() => {
setShowToggle(true)
- }, 20)
+ }, 150)
}, 150)
}
diff --git a/src/renderer/src/components/Home.tsx b/src/renderer/src/components/Home.tsx
index 494e027..d274829 100644
--- a/src/renderer/src/components/Home.tsx
+++ b/src/renderer/src/components/Home.tsx
@@ -18,6 +18,8 @@ interface student {
id: number
name: string
score: number
+ pinyinName?: string
+ pinyinFirst?: string
}
interface reason {
@@ -51,33 +53,6 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
window.dispatchEvent(new CustomEvent('ss:data-updated', { detail: { category } }))
}
- const fetchData = useCallback(async (silent = false) => {
- if (!(window as any).api) return
- if (!silent) setLoading(true)
- const [stuRes, reaRes] = await Promise.all([
- (window as any).api.queryStudents({}),
- (window as any).api.queryReasons()
- ])
-
- if (stuRes.success) setStudents(stuRes.data)
- if (reaRes.success) setReasons(reaRes.data)
- if (!silent) setLoading(false)
- }, [])
-
- useEffect(() => {
- fetchData()
- const onDataUpdated = (e: any) => {
- const category = e?.detail?.category
- // 仅在学生名单、理由列表或全量更新时才重新拉取数据
- // 积分变动(events)现在由本地状态维护,不再触发全量刷新以防止页面跳动
- if (category === 'students' || category === 'reasons' || category === 'all') {
- fetchData(true)
- }
- }
- window.addEventListener('ss:data-updated', onDataUpdated as any)
- return () => window.removeEventListener('ss:data-updated', onDataUpdated as any)
- }, [fetchData])
-
// 获取姓氏
const getSurname = (name: string) => {
if (!name) return ''
@@ -95,6 +70,38 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
return py ? py.toUpperCase() : '#'
}
+ const fetchData = useCallback(async (silent = false) => {
+ if (!(window as any).api) return
+ if (!silent) setLoading(true)
+ const [stuRes, reaRes] = await Promise.all([
+ (window as any).api.queryStudents({}),
+ (window as any).api.queryReasons()
+ ])
+
+ if (stuRes.success) {
+ const enrichedStudents = (stuRes.data as student[]).map(s => ({
+ ...s,
+ pinyinName: pinyin(s.name, { toneType: 'none' }).toLowerCase(),
+ pinyinFirst: getFirstLetter(s.name)
+ }))
+ setStudents(enrichedStudents)
+ }
+ if (reaRes.success) setReasons(reaRes.data)
+ if (!silent) setLoading(false)
+ }, [])
+
+ useEffect(() => {
+ fetchData()
+ const onDataUpdated = (e: any) => {
+ const category = e?.detail?.category
+ if (category === 'students' || category === 'reasons' || category === 'all') {
+ fetchData(true)
+ }
+ }
+ window.addEventListener('ss:data-updated', onDataUpdated as any)
+ return () => window.removeEventListener('ss:data-updated', onDataUpdated as any)
+ }, [fetchData])
+
// 获取展示用的文字
const getDisplayText = (name: string) => {
if (!name) return ''
@@ -102,23 +109,22 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
}
// 拼音匹配
- const matchStudentName = useCallback((name: string, keyword: string) => {
+ const matchStudentName = useCallback((s: student, keyword: string) => {
const q0 = keyword.trim().toLowerCase()
if (!q0) return true
- const nameLower = String(name).toLowerCase()
+ const nameLower = String(s.name).toLowerCase()
if (nameLower.includes(q0)) return true
+ const pyLower = s.pinyinName || ''
+ if (pyLower.includes(q0)) return true
+
const q1 = q0.replace(/\s+/g, '')
- if (q1 && nameLower.replace(/\s+/g, '').includes(q1)) return true
+ if (q1 && (nameLower.replace(/\s+/g, '').includes(q1) || pyLower.replace(/\s+/g, '').includes(q1))) return true
try {
- const m0 = match(name, q0)
+ const m0 = match(s.name, q0)
if (Array.isArray(m0)) return true
- if (q1 && q1 !== q0) {
- const m1 = match(name, q1)
- if (Array.isArray(m1)) return true
- }
} catch {
return false
}
@@ -128,13 +134,13 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
// 过滤和排序学生
const sortedStudents = useMemo(() => {
- const filtered = students.filter((s) => matchStudentName(s.name, searchKeyword))
+ const filtered = students.filter((s) => matchStudentName(s, searchKeyword))
switch (sortType) {
case 'alphabet':
return filtered.sort((a, b) => {
- const pyA = pinyin(a.name, { toneType: 'none' })
- const pyB = pinyin(b.name, { toneType: 'none' })
+ const pyA = a.pinyinName || ''
+ const pyB = b.pinyinName || ''
return pyA.localeCompare(pyB)
})
case 'surname':
@@ -161,7 +167,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const groups: Record
= {}
sortedStudents.forEach((s) => {
- const key = sortType === 'alphabet' ? getFirstLetter(s.name) : getSurname(s.name)
+ const key = sortType === 'alphabet' ? (s.pinyinFirst || '#') : getSurname(s.name)
if (!groups[key]) groups[key] = []
groups[key].push(s)
})
@@ -822,7 +828,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
suffixIcon={
reasonContent ? (
setReasonContent('')}
+ onClick={() => setSearchKeyword('')}
style={{ cursor: 'pointer' }}
/>
) : undefined
@@ -883,4 +889,4 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
)
-}
+}
\ No newline at end of file
From 9c0bb8cac353368dfc64f650e4ca368ef8c74890 Mon Sep 17 00:00:00 2001
From: Linkon <116425752+Linkon-lcw@users.noreply.github.com>
Date: Sun, 18 Jan 2026 23:48:07 +0800
Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20lint=20?=
=?UTF-8?q?=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 移除 index.ts 中未使用的 logLevel 导入
- 移除 GlobalSidebar.tsx 中未使用的 useEffect 导入
- 修复 WindowManager.ts 中的 require() 导入,改为 ES6 导入
---
src/main/index.ts | 2 +-
src/main/services/WindowManager.ts | 3 +-
src/renderer/src/components/GlobalSidebar.tsx | 48 +++++++++++--------
3 files changed, 30 insertions(+), 23 deletions(-)
diff --git a/src/main/index.ts b/src/main/index.ts
index 2c13ac6..eb78310 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -6,7 +6,7 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/SecScore_logo.ico?asset'
import { MainContext } from './context'
import { DbManager } from './db/DbManager'
-import { LoggerService, logLevel } from './services/LoggerService'
+import { LoggerService } from './services/LoggerService'
import { SettingsService } from './services/SettingsService'
import { SecurityService } from './services/SecurityService'
import { PermissionService } from './services/PermissionService'
diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts
index 779562b..bb4ffeb 100644
--- a/src/main/services/WindowManager.ts
+++ b/src/main/services/WindowManager.ts
@@ -1,6 +1,6 @@
import { Service } from '../../shared/kernel'
import { MainContext } from '../context'
-import { BrowserWindow, shell } from 'electron'
+import { BrowserWindow, shell, screen } from 'electron'
import type { BrowserWindowConstructorOptions } from 'electron'
export type windowOpenInput = {
@@ -77,7 +77,6 @@ export class WindowManager extends Service {
// 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
diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx
index d7bc00d..27b9fc1 100644
--- a/src/renderer/src/components/GlobalSidebar.tsx
+++ b/src/renderer/src/components/GlobalSidebar.tsx
@@ -1,10 +1,10 @@
-import React, { useState, useEffect } from 'react'
+import React, { useState } from 'react'
import { Button, Tooltip } from 'tdesign-react'
-import {
- HomeIcon,
- ViewListIcon,
- UserAddIcon,
- ChevronRightIcon,
+import {
+ HomeIcon,
+ ViewListIcon,
+ UserAddIcon,
+ ChevronRightIcon,
ChevronLeftIcon,
SettingIcon
} from 'tdesign-icons-react'
@@ -12,15 +12,15 @@ import {
export const GlobalSidebar: React.FC = () => {
const [expanded, setExpanded] = useState(false)
const [showToggle, setShowToggle] = useState(true)
-
+
const handleExpand = () => {
// 1. 先隐藏三角
setShowToggle(false)
-
+
// 2. 稍后扩大窗口
setTimeout(() => {
if ((window as any).api) {
- (window as any).api.windowResize(84, 300)
+ ;(window as any).api.windowResize(84, 300)
}
// 3. 最后显示侧边栏内容
setTimeout(() => {
@@ -32,11 +32,11 @@ export const GlobalSidebar: React.FC = () => {
const handleCollapse = () => {
// 1. 先隐藏侧边栏内容
setExpanded(false)
-
+
// 2. 稍后缩小窗口
setTimeout(() => {
if ((window as any).api) {
- (window as any).api.windowResize(24, 300)
+ ;(window as any).api.windowResize(24, 300)
}
// 3. 最后重新显示三角(等待透明度动画完成)
setTimeout(() => {
@@ -47,11 +47,11 @@ export const GlobalSidebar: React.FC = () => {
const openMain = () => {
if (!(window as any).api) return
- (window as any).api.openWindow({ key: 'main', route: '/' })
+ ;(window as any).api.openWindow({ key: 'main', route: '/' })
}
return (
-
{
}}
>
{/* 日常展示的三角 */}
-
{
{/* 侧边栏内容 */}
-
{
}}
>
{/* 顶部的关闭/收起按钮 */}
-