From b5874980fcfc34cdc9b339ef8e73b7ebbceb76c0 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:32:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=B8=BB=E9=A2=98):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E7=BC=96=E8=BE=91=E5=99=A8=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增主题编辑器组件和上下文管理 - 实现主题的保存、删除和自定义功能 - 更新主题相关API接口 - 调整主题样式透明度 - 替换应用logo为高清版本 - 更新README和package.json描述信息 --- README.md | 19 +++- package.json | 7 +- scripts/ci/parse-commit.mjs | 14 ++- scripts/clean-db.mjs | 6 +- src/main/services/SettingsService.ts | 3 +- src/main/services/ThemeService.ts | 67 +++++++++++--- src/preload/index.ts | 2 + src/preload/types.ts | 2 + src/renderer/src/assets/logoHD-dark.svg | 7 ++ src/renderer/src/assets/logoHD.svg | 7 ++ src/renderer/src/components/ContentArea.tsx | 2 + src/renderer/src/components/GlobalSidebar.tsx | 1 - src/renderer/src/components/Sidebar.tsx | 2 +- src/renderer/src/components/ThemeEditor.tsx | 17 ++-- .../src/contexts/ThemeEditorContext.tsx | 89 +++++++++++++++++++ themes/acrylic-auto.json | 4 +- themes/mica-auto.json | 4 +- themes/mica-dark.json | 4 +- themes/mica-light.json | 4 +- 19 files changed, 221 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/assets/logoHD-dark.svg create mode 100644 src/renderer/src/assets/logoHD.svg create mode 100644 src/renderer/src/contexts/ThemeEditorContext.tsx diff --git a/README.md b/README.md index 0bac02c..5a7ec28 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,23 @@ # SecScore -一款本地离线(未来加入远程模式)的“教育积分管理”桌面软件(Electron + React + TypeScript),用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供基础的权限保护与数据备份。 + +

+ GitHub release (latest by date) + CI + Downloads + License + GitHub issues + GitHub pull requests + Electron + React + TypeScript + Vite +

+ +SecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。 + + +SecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。 ## 主要功能 diff --git a/package.json b/package.json index c987989..2b7cf6d 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "secscore", "version": "1.0.0", - "description": "An Electron application with React and TypeScript", + "description": "SecScore – Your Education Points Management Expert", "main": "./out/main/index.js", "author": "example.com", - "homepage": "https://electron-vite.org", + "homepage": "https://example.org", "scripts": { "format": "prettier --write .", "lint": "eslint --cache .", @@ -35,7 +35,8 @@ "uuid": "^13.0.0", "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "pnpm": "^8.1.2" }, "devDependencies": { "@electron-toolkit/eslint-config-prettier": "^3.0.0", diff --git a/scripts/ci/parse-commit.mjs b/scripts/ci/parse-commit.mjs index e734af5..cfacb09 100644 --- a/scripts/ci/parse-commit.mjs +++ b/scripts/ci/parse-commit.mjs @@ -13,19 +13,27 @@ const rawMessage = '' const message = String(rawMessage || '').trim() +// 是否以 release: 开头 +const isReleasePrefix = /^release:/i.test(message) + const semverRe = /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/ const versionMatch = message.match(semverRe) const version = versionMatch ? versionMatch[1] : '' const buildToken = message.match(/\b(build:(?:win|mac|linux|unpack|all))\b/i)?.[1]?.toLowerCase() ?? '' -const buildScript = +let buildScript = buildToken === 'build:all' ? 'build:all' : buildToken && buildToken.startsWith('build:') ? buildToken : '' +// 如果是 release: 开头且没有显式指定构建脚本,默认为 build:all +if (isReleasePrefix && !buildScript) { + buildScript = 'build:all' +} + const supportedBuildScripts = new Set(['build:win', 'build:mac', 'build:linux', 'build:unpack']) const hasBuildScript = buildScript ? buildScript === 'build:all' @@ -38,7 +46,7 @@ const fail = (text) => { process.exit(1) } -const hasAnySignal = Boolean(version || buildScript) +const hasAnySignal = Boolean(isReleasePrefix || version || buildScript) const shouldSkip = !hasAnySignal if (!shouldSkip) { @@ -46,7 +54,7 @@ if (!shouldSkip) { fail( [ '未在提交信息中检测到版本号。', - '示例:release: v1.2.3 build:win', + '示例:release: v1.2.3 build:win 或 release: 1.2.3', '支持格式:v1.2.3 或 1.2.3(可带 -beta.1 等后缀)' ].join('\n') ) diff --git a/scripts/clean-db.mjs b/scripts/clean-db.mjs index 8ce8e19..53f062a 100644 --- a/scripts/clean-db.mjs +++ b/scripts/clean-db.mjs @@ -12,6 +12,10 @@ for (const name of targets) { console.log(`[clean-db] removed ${filePath}`) } } catch (e) { - console.error(`[clean-db] failed to remove ${filePath}:`, e?.message || e) + if (e.code === 'EPERM' || e.code === 'EACCES') { + console.warn(`[clean-db] skipped ${filePath}: file in use or permission denied`) + } else { + console.error(`[clean-db] failed to remove ${filePath}:`, e?.message || e) + } } } diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index 7bfcd61..0ee83ff 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -91,7 +91,8 @@ export class SettingsService extends Service { kind: 'string', defaultValue: 'mica', writePermission: 'admin', - validate: (v) => ['mica', 'tabbed', 'acrylic', 'blur', 'transparent', 'none'].includes(v as string) + validate: (v) => + ['mica', 'tabbed', 'acrylic', 'blur', 'transparent', 'none'].includes(v as string) }, window_radius: { kind: 'string', diff --git a/src/main/services/ThemeService.ts b/src/main/services/ThemeService.ts index b5791c6..0493893 100644 --- a/src/main/services/ThemeService.ts +++ b/src/main/services/ThemeService.ts @@ -88,19 +88,62 @@ export class ThemeService extends Service { return { success: true } }) - this.mainCtx.handle('theme:set-custom', async (event, config: { - effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' - theme: 'auto' | 'dark' | 'light' - radius: 'small' | 'medium' | 'large' - }) => { + this.mainCtx.handle('theme:save', async (event, theme: themeConfig) => { if (!this.mainCtx.permissions.requirePermission(event, 'admin')) return { success: false, message: 'Permission denied' } - this.currentThemeId = 'custom' - this.applyMicaConfig(config) - this.notifyThemeUpdate() - return { success: true } + try { + const filePath = path.join(this.themeDir, `${theme.id}.json`) + fs.writeFileSync(filePath, JSON.stringify(theme, null, 2), 'utf-8') + this.notifyThemeUpdate() + return { success: true } + } catch (e) { + return { success: false, message: String(e) } + } }) + + this.mainCtx.handle('theme:delete', async (event, themeId: string) => { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + + if (themeId.startsWith('default-')) { + return { success: false, message: 'Cannot delete default themes' } + } + + try { + const filePath = path.join(this.themeDir, `${themeId}.json`) + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath) + } + if (this.currentThemeId === themeId) { + this.currentThemeId = 'light' // Fallback + } + this.notifyThemeUpdate() + return { success: true } + } catch (e) { + return { success: false, message: String(e) } + } + }) + + this.mainCtx.handle( + 'theme:set-custom', + async ( + event, + config: { + effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' + theme: 'auto' | 'dark' | 'light' + radius: 'small' | 'medium' | 'large' + } + ) => { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + + this.currentThemeId = 'custom' + this.applyMicaConfig(config) + this.notifyThemeUpdate() + return { success: true } + } + ) } private applyMicaEffect(themeId: string) { @@ -116,9 +159,9 @@ export class ThemeService extends Service { radius: 'small' | 'medium' | 'large' }) { const radiusMap: Record = { - 'small': 'small', - 'medium': 'rounded', - 'large': 'rounded' + small: 'small', + medium: 'rounded', + large: 'rounded' } const windows = BrowserWindow.getAllWindows() diff --git a/src/preload/index.ts b/src/preload/index.ts index 8102a3f..d169f51 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -7,6 +7,8 @@ const api = { getThemes: () => ipcRenderer.invoke('theme:list'), getCurrentTheme: () => ipcRenderer.invoke('theme:current'), setTheme: (themeId: string) => ipcRenderer.invoke('theme:set', themeId), + saveTheme: (theme: themeConfig) => ipcRenderer.invoke('theme:save', theme), + deleteTheme: (themeId: string) => ipcRenderer.invoke('theme:delete', themeId), setCustomTheme: (config: { effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' theme: 'auto' | 'dark' | 'light' diff --git a/src/preload/types.ts b/src/preload/types.ts index 6c9cdaf..a456e31 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -43,6 +43,8 @@ export interface electronApi { getThemes: () => Promise> getCurrentTheme: () => Promise> setTheme: (themeId: string) => Promise> + saveTheme: (theme: themeConfig) => Promise> + deleteTheme: (themeId: string) => Promise> setCustomTheme: (config: { effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' theme: 'auto' | 'dark' | 'light' diff --git a/src/renderer/src/assets/logoHD-dark.svg b/src/renderer/src/assets/logoHD-dark.svg new file mode 100644 index 0000000..b190979 --- /dev/null +++ b/src/renderer/src/assets/logoHD-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/renderer/src/assets/logoHD.svg b/src/renderer/src/assets/logoHD.svg new file mode 100644 index 0000000..51ff5ea --- /dev/null +++ b/src/renderer/src/assets/logoHD.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/renderer/src/components/ContentArea.tsx b/src/renderer/src/components/ContentArea.tsx index 9108a5e..a049d52 100644 --- a/src/renderer/src/components/ContentArea.tsx +++ b/src/renderer/src/components/ContentArea.tsx @@ -2,6 +2,7 @@ import React, { Suspense, lazy } from 'react' import { Layout, Space, Button, Tag, Loading } from 'tdesign-react' import { Routes, Route, Navigate } from 'react-router-dom' import { WindowControls } from './WindowControls' +import { ThemeEditor } from './ThemeEditor' const Home = lazy(() => import('./Home').then((m) => ({ default: m.Home }))) const StudentManager = lazy(() => @@ -125,6 +126,7 @@ export function ContentArea({ + ) } diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx index adccddc..c8fa6d1 100644 --- a/src/renderer/src/components/GlobalSidebar.tsx +++ b/src/renderer/src/components/GlobalSidebar.tsx @@ -55,7 +55,6 @@ export const GlobalSidebar: React.FC = () => { // 1. 先隐藏三角 setShowToggle(false) - // 2. 稍后扩大窗口 setTimeout(() => { if ((window as any).api) { diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 9f30b8f..1c80c65 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -7,7 +7,7 @@ import { ViewListIcon, HomeIcon } from 'tdesign-icons-react' -import appLogo from '../assets/logo.svg' +import appLogo from '../assets/logoHD.svg' const { Aside } = Layout diff --git a/src/renderer/src/components/ThemeEditor.tsx b/src/renderer/src/components/ThemeEditor.tsx index d794a5a..8786d18 100644 --- a/src/renderer/src/components/ThemeEditor.tsx +++ b/src/renderer/src/components/ThemeEditor.tsx @@ -135,16 +135,16 @@ export const ThemeEditor: React.FC = () => { 基本信息 - + updateEditingTheme({ name: v })} placeholder="请输入主题名称" /> - + - + updateEditingTheme({ id: v })} placeholder="请输入主题 ID" /> - + @@ -170,14 +170,14 @@ export const ThemeEditor: React.FC = () => { {/* TDesign 品牌色 */}
品牌色 (Brand) - + updateConfig('tdesign', 'brandColor', v)} enableAlpha={false} format="HEX" /> - +
{/* 业务自定义变量 */} @@ -216,7 +216,6 @@ export const ThemeEditor: React.FC = () => { onChange={(v) => updateConfig('custom', item.key, v)} enableAlpha format="HEX" - size="small" /> diff --git a/src/renderer/src/contexts/ThemeEditorContext.tsx b/src/renderer/src/contexts/ThemeEditorContext.tsx new file mode 100644 index 0000000..e254413 --- /dev/null +++ b/src/renderer/src/contexts/ThemeEditorContext.tsx @@ -0,0 +1,89 @@ +import React, { createContext, useContext, useState, useCallback } from 'react' +import { themeConfig } from '../../../preload/types' +import { useTheme } from './ThemeContext' + +interface ThemeEditorContextType { + isEditing: boolean + editingTheme: themeConfig | null + startEditing: (theme?: themeConfig) => void + updateEditingTheme: (updates: Partial) => void + updateConfig: (type: 'tdesign' | 'custom', key: string, value: string) => void + saveEditingTheme: () => Promise + cancelEditing: () => void +} + +const ThemeEditorContext = createContext(undefined) + +export const ThemeEditorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { currentTheme } = useTheme() + const [isEditing, setIsEditing] = useState(false) + const [editingTheme, setEditingTheme] = useState(null) + + const startEditing = useCallback( + (theme?: themeConfig) => { + if (theme) { + setEditingTheme(JSON.parse(JSON.stringify(theme))) + } else if (currentTheme) { + const newTheme: themeConfig = JSON.parse(JSON.stringify(currentTheme)) + newTheme.id = `custom-${Date.now()}` + newTheme.name = '新自定义主题' + setEditingTheme(newTheme) + } + setIsEditing(true) + }, + [currentTheme] + ) + + const updateEditingTheme = useCallback((updates: Partial) => { + setEditingTheme((prev) => (prev ? { ...prev, ...updates } : null)) + }, []) + + const updateConfig = useCallback((type: 'tdesign' | 'custom', key: string, value: string) => { + setEditingTheme((prev) => { + if (!prev) return null + const next = { ...prev } + next.config = { ...next.config } + next.config[type] = { ...next.config[type], [key]: value } + return next + }) + }, []) + + const saveEditingTheme = useCallback(async () => { + if (!editingTheme) return + const res = await (window as any).api.saveTheme(editingTheme) + if (res.success) { + await (window as any).api.setTheme(editingTheme.id) + setIsEditing(false) + setEditingTheme(null) + } else { + console.error('Failed to save theme:', res.message) + } + }, [editingTheme]) + + const cancelEditing = useCallback(() => { + setIsEditing(false) + setEditingTheme(null) + }, []) + + return ( + + {children} + + ) +} + +export const useThemeEditor = () => { + const context = useContext(ThemeEditorContext) + if (!context) throw new Error('useThemeEditor must be used within ThemeEditorProvider') + return context +} diff --git a/themes/acrylic-auto.json b/themes/acrylic-auto.json index e7eefdc..de4fee6 100644 --- a/themes/acrylic-auto.json +++ b/themes/acrylic-auto.json @@ -16,12 +16,12 @@ }, "custom": { "--ss-bg-color": "transparent", - "--ss-card-bg": "rgba(255, 255, 255, 0.85)", + "--ss-card-bg": "rgba(255, 255, 255, 0.3)", "--ss-text-main": "#1a1a1a", "--ss-text-secondary": "#5e5e5e", "--ss-border-color": "rgba(0, 0, 0, 0.08)", "--ss-header-bg": "transparent", - "--ss-sidebar-bg": "rgba(255, 255, 255, 0.9)", + "--ss-sidebar-bg": "rgba(255, 255, 255, 0.5)", "--ss-item-hover": "rgba(0, 0, 0, 0.04)", "--ss-sidebar-text": "#1a1a1a", "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.06)", diff --git a/themes/mica-auto.json b/themes/mica-auto.json index c462a38..0a17dc0 100644 --- a/themes/mica-auto.json +++ b/themes/mica-auto.json @@ -16,12 +16,12 @@ }, "custom": { "--ss-bg-color": "transparent", - "--ss-card-bg": "rgba(255, 255, 255, 0.7)", + "--ss-card-bg": "rgba(255, 255, 255, 0.3)", "--ss-text-main": "#1a1a1a", "--ss-text-secondary": "#5e5e5e", "--ss-border-color": "rgba(0, 0, 0, 0.1)", "--ss-header-bg": "transparent", - "--ss-sidebar-bg": "rgba(255, 255, 255, 0.85)", + "--ss-sidebar-bg": "rgba(255, 255, 255, 0.5)", "--ss-item-hover": "rgba(0, 0, 0, 0.05)", "--ss-sidebar-text": "#1a1a1a", "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)", diff --git a/themes/mica-dark.json b/themes/mica-dark.json index 217bdec..49c5af0 100644 --- a/themes/mica-dark.json +++ b/themes/mica-dark.json @@ -16,12 +16,12 @@ }, "custom": { "--ss-bg-color": "transparent", - "--ss-card-bg": "rgba(30, 30, 30, 0.7)", + "--ss-card-bg": "rgba(30, 30, 30, 0.3)", "--ss-text-main": "#e0e0e0", "--ss-text-secondary": "#a0a0a0", "--ss-border-color": "rgba(255, 255, 255, 0.1)", "--ss-header-bg": "transparent", - "--ss-sidebar-bg": "rgba(30, 30, 30, 0.85)", + "--ss-sidebar-bg": "rgba(30, 30, 30, 0.5)", "--ss-item-hover": "rgba(255, 255, 255, 0.05)", "--ss-sidebar-text": "#e0e0e0", "--ss-sidebar-active-bg": "rgba(255, 255, 255, 0.08)", diff --git a/themes/mica-light.json b/themes/mica-light.json index 80f274d..a6fab85 100644 --- a/themes/mica-light.json +++ b/themes/mica-light.json @@ -16,12 +16,12 @@ }, "custom": { "--ss-bg-color": "transparent", - "--ss-card-bg": "rgba(255, 255, 255, 0.7)", + "--ss-card-bg": "rgba(255, 255, 255, 0.3)", "--ss-text-main": "#1a1a1a", "--ss-text-secondary": "#5e5e5e", "--ss-border-color": "rgba(0, 0, 0, 0.1)", "--ss-header-bg": "transparent", - "--ss-sidebar-bg": "rgba(255, 255, 255, 0.85)", + "--ss-sidebar-bg": "rgba(255, 255, 255, 0.5)", "--ss-item-hover": "rgba(0, 0, 0, 0.05)", "--ss-sidebar-text": "#1a1a1a", "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)",