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),用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供基础的权限保护与数据备份。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+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)",