From 93e7135352130ea0a8a5d98744ac90d306cd9187 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:54:30 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=9C=A8=E7=BC=A9=E6=94=BE=E6=97=B6=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E9=94=99=E4=BD=8D=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/services/SettingsService.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index 9c5a6b9..4a90c8a 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -53,11 +53,35 @@ export class SettingsService extends Service { kind: 'number', defaultValue: 1.0, writePermission: 'admin', - onChanged: (_ctx, next) => { + onChanged: (ctx, next) => { const zoom = Number(next) || 1.0 webContents.getAllWebContents().forEach((wc: any) => { wc.setZoomFactor(zoom) }) + + // 更新全局侧边栏窗口的位置,确保在缩放后位置正确 + const { BrowserWindow, screen } = require('electron') + const sidebarWindow = BrowserWindow.getAllWindows().find((win) => { + try { + return win.webContents.getURL().includes('#global-sidebar') + } catch { + return false + } + }) + + if (sidebarWindow && !sidebarWindow.isDestroyed()) { + const primaryDisplay = screen.getPrimaryDisplay() + const { width, height } = primaryDisplay.workAreaSize + const winWidth = 84 + const winHeight = 300 + + sidebarWindow.setBounds({ + x: width - winWidth, + y: Math.floor(height / 2 - winHeight / 2), + width: winWidth, + height: winHeight + }) + } } } } From e4c27530a663783084cfad6f0119d0474de0b61d Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:07:54 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=9C=A8=E7=BC=A9=E6=94=BE=E6=97=B6=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E5=A4=A7=E5=B0=8F=E9=94=99=E4=BD=8D=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=88=E6=B7=BB=E5=8A=A0=E7=AA=97=E5=8F=A3=E5=A4=A7?= =?UTF-8?q?=E5=B0=8F=E8=B0=83=E6=95=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/services/SettingsService.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index 4a90c8a..06e20d3 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -1,6 +1,6 @@ import { Service } from '../../shared/kernel' import { MainContext } from '../context' -import { BrowserWindow, webContents } from 'electron' +import { BrowserWindow, webContents, screen } from 'electron' import type { IpcMainInvokeEvent } from 'electron' import type { settingsKey, settingsSpec, settingChange } from '../../preload/types' import type { permissionLevel } from './PermissionService' @@ -53,14 +53,13 @@ export class SettingsService extends Service { kind: 'number', defaultValue: 1.0, writePermission: 'admin', - onChanged: (ctx, next) => { + onChanged: (_ctx, next) => { const zoom = Number(next) || 1.0 webContents.getAllWebContents().forEach((wc: any) => { wc.setZoomFactor(zoom) }) - // 更新全局侧边栏窗口的位置,确保在缩放后位置正确 - const { BrowserWindow, screen } = require('electron') + // 更新全局侧边栏窗口的位置和大小 const sidebarWindow = BrowserWindow.getAllWindows().find((win) => { try { return win.webContents.getURL().includes('#global-sidebar') @@ -72,8 +71,10 @@ export class SettingsService extends Service { if (sidebarWindow && !sidebarWindow.isDestroyed()) { const primaryDisplay = screen.getPrimaryDisplay() const { width, height } = primaryDisplay.workAreaSize - const winWidth = 84 - const winHeight = 300 + const baseWidth = 84 + const baseHeight = 300 + const winWidth = Math.round(baseWidth * zoom) + const winHeight = Math.round(baseHeight * zoom) sidebarWindow.setBounds({ x: width - winWidth, From 01bd7be1edd669a02b7b5ed0eed389a952e50230 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:14:49 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=B1=95=E5=BC=80/=E6=94=B6=E7=BC=A9=E6=97=B6?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=A4=A7=E5=B0=8F=E4=B8=8D=E9=9A=8F=E7=BC=A9?= =?UTF-8?q?=E6=94=BE=E6=AF=94=E4=BE=8B=E8=B0=83=E6=95=B4=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/components/GlobalSidebar.tsx | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx index 27b9fc1..893e2b3 100644 --- a/src/renderer/src/components/GlobalSidebar.tsx +++ b/src/renderer/src/components/GlobalSidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Button, Tooltip } from 'tdesign-react' import { HomeIcon, @@ -12,6 +12,31 @@ import { export const GlobalSidebar: React.FC = () => { const [expanded, setExpanded] = useState(false) const [showToggle, setShowToggle] = useState(true) + const [zoom, setZoom] = useState(1.0) + + useEffect(() => { + if (!(window as any).api) return + + // 加载初始缩放值 + const loadZoom = async () => { + const res = await (window as any).api.getSetting('window_zoom') + if (res.success && res.data) { + setZoom(res.data) + } + } + loadZoom() + + // 监听缩放变化 + const unsubscribe = (window as any).api.onSettingChanged((change: any) => { + if (change?.key === 'window_zoom') { + setZoom(change.value) + } + }) + + return () => { + if (typeof unsubscribe === 'function') unsubscribe() + } + }, []) const handleExpand = () => { // 1. 先隐藏三角 @@ -20,7 +45,9 @@ export const GlobalSidebar: React.FC = () => { // 2. 稍后扩大窗口 setTimeout(() => { if ((window as any).api) { - ;(window as any).api.windowResize(84, 300) + const width = Math.round(84 * zoom) + const height = Math.round(300 * zoom) + ;(window as any).api.windowResize(width, height) } // 3. 最后显示侧边栏内容 setTimeout(() => { @@ -36,7 +63,9 @@ export const GlobalSidebar: React.FC = () => { // 2. 稍后缩小窗口 setTimeout(() => { if ((window as any).api) { - ;(window as any).api.windowResize(24, 300) + const width = Math.round(24 * zoom) + const height = Math.round(300 * zoom) + ;(window as any).api.windowResize(width, height) } // 3. 最后重新显示三角(等待透明度动画完成) setTimeout(() => { @@ -54,7 +83,7 @@ export const GlobalSidebar: React.FC = () => {
{
@@ -77,7 +110,10 @@ export const GlobalSidebar: React.FC = () => { style={{ backgroundColor: 'var(--ss-card-bg)', height: 'fit-content', - willChange: 'opacity, transform' + willChange: 'opacity, transform', + width: `${60 * zoom}px`, + padding: `${12 * zoom}px ${8 * zoom}px`, + gap: `${12 * zoom}px` }} > {/* 顶部的关闭/收起按钮 */} @@ -90,19 +126,19 @@ export const GlobalSidebar: React.FC = () => { - + - + - + - +
diff --git a/src/renderer/src/components/Leaderboard.tsx b/src/renderer/src/components/Leaderboard.tsx index 57cd4e5..b3c29bd 100644 --- a/src/renderer/src/components/Leaderboard.tsx +++ b/src/renderer/src/components/Leaderboard.tsx @@ -32,12 +32,17 @@ export const Leaderboard: React.FC = () => { const fetchRankings = useCallback(async () => { if (!(window as any).api) return setLoading(true) - const res = await (window as any).api.queryLeaderboard({ range: timeRange }) - if (res.success && res.data) { - setStartTime(res.data.startTime) - setData(res.data.rows) + try { + const res = await (window as any).api.queryLeaderboard({ range: timeRange }) + if (res.success && res.data) { + setStartTime(res.data.startTime) + setData(res.data.rows) + } + } catch (e) { + console.error('Failed to fetch rankings:', e) + } finally { + setLoading(false) } - setLoading(false) }, [timeRange]) useEffect(() => { @@ -77,48 +82,51 @@ export const Leaderboard: React.FC = () => { } const handleExport = () => { - const title = timeRange === 'today' ? '今天' : timeRange === 'week' ? '本周' : '本月' + // 使用 requestIdleCallback 或 setTimeout 避免阻塞 UI + setTimeout(() => { + const title = timeRange === 'today' ? '今天' : timeRange === 'week' ? '本周' : '本月' - const sanitizeCell = (v: unknown) => { - if (typeof v !== 'string') return v - if (/^[=+\-@]/.test(v)) return `'${v}` - return v - } + const sanitizeCell = (v: unknown) => { + if (typeof v !== 'string') return v + if (/^[=+\-@]/.test(v)) return `'${v}` + return v + } - const sheetData = [ - ['排名', '姓名', '总积分', `${title}变化`], - ...data.map((item, index) => [ - index + 1, - sanitizeCell(item.name), - item.score, - item.range_change - ]) - ] + const sheetData = [ + ['排名', '姓名', '总积分', `${title}变化`], + ...data.map((item, index) => [ + index + 1, + sanitizeCell(item.name), + item.score, + item.range_change + ]) + ] - const ws = XLSX.utils.aoa_to_sheet(sheetData) - ws['!cols'] = [{ wch: 6 }, { wch: 14 }, { wch: 10 }, { wch: 10 }] + const ws = XLSX.utils.aoa_to_sheet(sheetData) + ws['!cols'] = [{ wch: 6 }, { wch: 14 }, { wch: 10 }, { wch: 10 }] - const wb = XLSX.utils.book_new() - XLSX.utils.book_append_sheet(wb, ws, '排行榜') + const wb = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(wb, ws, '排行榜') - const xlsxBytes = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }) - const blob = new Blob([xlsxBytes], { - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - }) + const xlsxBytes = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }) + const blob = new Blob([xlsxBytes], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }) - const link = document.createElement('a') - const url = URL.createObjectURL(blob) - link.setAttribute('href', url) - link.setAttribute( - 'download', - `排行榜_${timeRange}_${new Date().toISOString().slice(0, 10)}.xlsx` - ) - link.style.visibility = 'hidden' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) - MessagePlugin.success('导出成功') + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + link.setAttribute( + 'download', + `排行榜_${timeRange}_${new Date().toISOString().slice(0, 10)}.xlsx` + ) + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + MessagePlugin.success('导出成功') + }, 0) } const columns: PrimaryTableCol[] = [ @@ -215,6 +223,8 @@ export const Leaderboard: React.FC = () => { loading={loading} bordered hover + pagination={{ pageSize: 30, total: data.length, defaultCurrent: 1 }} + scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }} className="ss-table-center" style={{ color: 'var(--ss-text-main)' }} /> diff --git a/src/renderer/src/components/ReasonManager.tsx b/src/renderer/src/components/ReasonManager.tsx index 552e097..e951db8 100644 --- a/src/renderer/src/components/ReasonManager.tsx +++ b/src/renderer/src/components/ReasonManager.tsx @@ -35,11 +35,16 @@ export const ReasonManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { const fetchReasons = useCallback(async () => { if (!(window as any).api) return setLoading(true) - const res = await (window as any).api.queryReasons() - if (res.success && res.data) { - setData(res.data) + try { + const res = await (window as any).api.queryReasons() + if (res.success && res.data) { + setData(res.data) + } + } catch (e) { + console.error('Failed to fetch reasons:', e) + } finally { + setLoading(false) } - setLoading(false) }, []) useEffect(() => { @@ -149,6 +154,8 @@ export const ReasonManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { loading={loading} bordered hover + pagination={{ pageSize: 50, total: data.length, defaultCurrent: 1 }} + scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} /> diff --git a/src/renderer/src/components/ScoreManager.tsx b/src/renderer/src/components/ScoreManager.tsx index fe3c44a..e1afd14 100644 --- a/src/renderer/src/components/ScoreManager.tsx +++ b/src/renderer/src/components/ScoreManager.tsx @@ -93,17 +93,25 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { const fetchData = useCallback(async () => { if (!(window as any).api) return - setLoading(true) - const [stuRes, reaRes, eveRes] = await Promise.all([ - (window as any).api.queryStudents({}), - (window as any).api.queryReasons(), - (window as any).api.queryEvents({ limit: 10 }) - ]) + // 使用 setTimeout 避免 UI 阻塞 + setTimeout(async () => { + setLoading(true) + try { + const [stuRes, reaRes, eveRes] = await Promise.all([ + (window as any).api.queryStudents({}), + (window as any).api.queryReasons(), + (window as any).api.queryEvents({ limit: 10 }) + ]) - if (stuRes.success) setStudents(stuRes.data) - if (reaRes.success) setReasons(reaRes.data) - if (eveRes.success) setEvents(eveRes.data) - setLoading(false) + if (stuRes.success) setStudents(stuRes.data) + if (reaRes.success) setReasons(reaRes.data) + if (eveRes.success) setEvents(eveRes.data) + } catch (e) { + console.error('Failed to fetch data:', e) + } finally { + setLoading(false) + } + }, 0) }, []) useEffect(() => { @@ -328,7 +336,8 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { rowKey="uuid" loading={loading} size="small" - pagination={{ pageSize: 5, total: events.length }} + pagination={{ pageSize: 5, total: events.length, defaultCurrent: 1 }} + scroll={{ type: 'virtual' }} style={{ color: 'var(--ss-text-main)' }} /> diff --git a/src/renderer/src/components/SettlementHistory.tsx b/src/renderer/src/components/SettlementHistory.tsx index c64a35e..5d6524f 100644 --- a/src/renderer/src/components/SettlementHistory.tsx +++ b/src/renderer/src/components/SettlementHistory.tsx @@ -36,13 +36,18 @@ export const SettlementHistory: React.FC = () => { const fetchSettlements = useCallback(async () => { if (!(window as any).api) return setLoading(true) - const res = await (window as any).api.querySettlements() - setLoading(false) - if (!res.success) { - MessagePlugin.error(res.message || '查询失败') - return + try { + const res = await (window as any).api.querySettlements() + if (!res.success) { + MessagePlugin.error(res.message || '查询失败') + return + } + setSettlements(res.data || []) + } catch (e) { + console.error('Failed to fetch settlements:', e) + } finally { + setLoading(false) } - setSettlements(res.data || []) }, []) useEffect(() => { @@ -62,14 +67,19 @@ export const SettlementHistory: React.FC = () => { if (!(window as any).api) return setSelectedId(id) setDetailLoading(true) - const res = await (window as any).api.querySettlementLeaderboard({ settlement_id: id }) - setDetailLoading(false) - if (!res.success || !res.data) { - MessagePlugin.error(res.message || '查询失败') - return + try { + const res = await (window as any).api.querySettlementLeaderboard({ settlement_id: id }) + if (!res.success || !res.data) { + MessagePlugin.error(res.message || '查询失败') + return + } + setSelectedSettlement(res.data.settlement) + setRows(res.data.rows || []) + } catch (e) { + console.error('Failed to fetch settlement leaderboard:', e) + } finally { + setDetailLoading(false) } - setSelectedSettlement(res.data.settlement) - setRows(res.data.rows || []) } const columns: PrimaryTableCol[] = useMemo( @@ -120,6 +130,8 @@ export const SettlementHistory: React.FC = () => { loading={detailLoading} bordered hover + pagination={{ pageSize: 50, total: rows.length, defaultCurrent: 1 }} + scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }} style={{ color: 'var(--ss-text-main)' }} /> diff --git a/src/renderer/src/components/StudentManager.tsx b/src/renderer/src/components/StudentManager.tsx index 46d8033..d829734 100644 --- a/src/renderer/src/components/StudentManager.tsx +++ b/src/renderer/src/components/StudentManager.tsx @@ -1,7 +1,13 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { Table, Button, Space, MessagePlugin, Dialog, Form, Input } from 'tdesign-react' import type { PrimaryTableCol } from 'tdesign-react' -import * as XLSX from 'xlsx' + +// 创建 XLSX Worker +const createXlsxWorker = () => { + return new Worker(new URL('../workers/xlsxWorker.ts', import.meta.url), { + type: 'module' + }) +} interface student { id: number @@ -20,8 +26,17 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { const [xlsxAoa, setXlsxAoa] = useState([]) const [xlsxSelectedCol, setXlsxSelectedCol] = useState(null) const xlsxInputRef = useRef(null) + const xlsxWorkerRef = useRef(null) const [form] = Form.useForm() + // 初始化 Worker + useEffect(() => { + xlsxWorkerRef.current = createXlsxWorker() + return () => { + xlsxWorkerRef.current?.terminate() + } + }, []) + const emitDataUpdated = (category: 'students' | 'all') => { window.dispatchEvent(new CustomEvent('ss:data-updated', { detail: { category } })) } @@ -29,11 +44,16 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { const fetchStudents = useCallback(async () => { if (!(window as any).api) return setLoading(true) - const res = await (window as any).api.queryStudents({}) - if (res.success && res.data) { - setData(res.data) + try { + const res = await (window as any).api.queryStudents({}) + if (res.success && res.data) { + setData(res.data) + } + } catch (e) { + console.error('Failed to fetch students:', e) + } finally { + setLoading(false) } - setLoading(false) }, []) useEffect(() => { @@ -123,30 +143,40 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { } const parseXlsxFile = async (file: File) => { + if (!xlsxWorkerRef.current) { + MessagePlugin.error('Worker 未初始化') + return + } + setXlsxLoading(true) try { const buf = await file.arrayBuffer() - const wb = XLSX.read(buf, { type: 'array' }) - const firstSheetName = wb.SheetNames?.[0] - if (!firstSheetName) { - MessagePlugin.error('xlsx 中未找到工作表') - return - } - const ws = wb.Sheets[firstSheetName] - const aoa = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false, defval: '' }) as any[][] - if (!Array.isArray(aoa) || aoa.length === 0) { - MessagePlugin.error('xlsx 内容为空') - return + + // 使用 Worker 处理文件解析,避免阻塞主线程 + xlsxWorkerRef.current.postMessage({ + type: 'parseXlsx', + data: { buffer: buf } + }) + + // 监听 Worker 消息 + const handleMessage = (event: MessageEvent) => { + if (event.data.type === 'success') { + setXlsxFileName(file.name) + setXlsxAoa(event.data.data) + setXlsxSelectedCol(null) + setXlsxVisible(true) + setImportVisible(false) + setXlsxLoading(false) + } else if (event.data.type === 'error') { + MessagePlugin.error(event.data.error || '解析 xlsx 失败') + setXlsxLoading(false) + } + xlsxWorkerRef.current?.removeEventListener('message', handleMessage) } - setXlsxFileName(file.name) - setXlsxAoa(aoa) - setXlsxSelectedCol(null) - setXlsxVisible(true) - setImportVisible(false) + xlsxWorkerRef.current.addEventListener('message', handleMessage) } catch (e: any) { MessagePlugin.error(e?.message || '解析 xlsx 失败') - } finally { setXlsxLoading(false) } } @@ -314,6 +344,8 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { loading={loading} bordered hover + pagination={{ pageSize: 50, total: data.length, defaultCurrent: 1 }} + scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} /> diff --git a/src/renderer/src/workers/xlsxWorker.ts b/src/renderer/src/workers/xlsxWorker.ts new file mode 100644 index 0000000..a2fb2dd --- /dev/null +++ b/src/renderer/src/workers/xlsxWorker.ts @@ -0,0 +1,30 @@ +import * as XLSX from 'xlsx' + +// 监听主线程消息 +self.addEventListener('message', async (event: MessageEvent) => { + const { type, data } = event.data + + if (type === 'parseXlsx') { + try { + const { buffer } = data + const wb = XLSX.read(buffer, { type: 'array' }) + const firstSheetName = wb.SheetNames?.[0] + if (!firstSheetName) { + self.postMessage({ type: 'error', error: 'xlsx 中未找到工作表' }) + return + } + const ws = wb.Sheets[firstSheetName] + const aoa = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false, defval: '' }) as any[][] + if (!Array.isArray(aoa) || aoa.length === 0) { + self.postMessage({ type: 'error', error: 'xlsx 内容为空' }) + return + } + + self.postMessage({ type: 'success', data: aoa }) + } catch (error: any) { + self.postMessage({ type: 'error', error: error?.message || '解析 xlsx 失败' }) + } + } +}) + +export {} From 70bba94135da67f2f3cd1a22a8452d6735757fa3 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:36:37 +0800 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=E5=9C=A8=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=B7=BB=E5=8A=A0=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E8=80=85=E5=B7=A5=E5=85=B7=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/components/GlobalSidebar.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx index e18247d..8846eae 100644 --- a/src/renderer/src/components/GlobalSidebar.tsx +++ b/src/renderer/src/components/GlobalSidebar.tsx @@ -6,7 +6,8 @@ import { UserAddIcon, ChevronRightIcon, ChevronLeftIcon, - SettingIcon + SettingIcon, + CodeIcon } from 'tdesign-icons-react' export const GlobalSidebar: React.FC = () => { @@ -169,6 +170,16 @@ export const GlobalSidebar: React.FC = () => { + + + + ) From 5afa02ee52cc5bc9bebc59166fbbcbd08c8d77c8 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:16:37 +0800 Subject: [PATCH 08/18] =?UTF-8?q?fix:=20=E5=BC=80=E5=8F=91=E8=80=85?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=8C=89=E9=92=AE=E4=BB=85=E5=9C=A8=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=8E=AF=E5=A2=83=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/components/GlobalSidebar.tsx | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/components/GlobalSidebar.tsx b/src/renderer/src/components/GlobalSidebar.tsx index 8846eae..1be547c 100644 --- a/src/renderer/src/components/GlobalSidebar.tsx +++ b/src/renderer/src/components/GlobalSidebar.tsx @@ -96,7 +96,7 @@ export const GlobalSidebar: React.FC = () => {
{ className={`global-sidebar-toggle ${!showToggle ? 'hidden' : ''}`} style={{ willChange: 'opacity, transform', - width: `${24 * zoom}px`, - height: `${60 * zoom}px` + width: `24px`, + height: `60px` }} > @@ -124,9 +124,9 @@ export const GlobalSidebar: React.FC = () => { backgroundColor: 'var(--ss-card-bg)', height: 'fit-content', willChange: 'opacity, transform', - width: `${60 * zoom}px`, - padding: `${12 * zoom}px ${8 * zoom}px`, - gap: `${12 * zoom}px` + width: `60px`, + padding: `12px 8px`, + gap: `12px` }} > {/* 顶部的关闭/收起按钮 */} @@ -171,15 +171,17 @@ export const GlobalSidebar: React.FC = () => { - - - + {import.meta.env.DEV && ( + + + + )}
) From 0fa80e419f13acdef39b019919c7ffa54cdb334f Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:18:37 +0800 Subject: [PATCH 09/18] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Mica=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=94=AF=E6=8C=81Windows=2011=E4=BA=91?= =?UTF-8?q?=E6=AF=8D=E6=95=88=E6=9E=9C=E9=80=8F=E6=98=8E=E8=83=8C=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/mica.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 themes/mica.json diff --git a/themes/mica.json b/themes/mica.json new file mode 100644 index 0000000..a4b335a --- /dev/null +++ b/themes/mica.json @@ -0,0 +1,26 @@ +{ + "name": "云母", + "id": "mica", + "mode": "light", + "config": { + "tdesign": { + "brandColor": "#0052D9", + "warningColor": "#E37318", + "errorColor": "#D32029", + "successColor": "#248232" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(255, 255, 255, 0.7)", + "--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-item-hover": "rgba(0, 0, 0, 0.05)", + "--ss-sidebar-text": "#1a1a1a", + "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)", + "--ss-sidebar-active-text": "#1a1a1a" + } + } +} From 6b41ae079a3eeb579f37804c0272f38378fa0ced Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:19:12 +0800 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20lint:fix=20?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(components): 调整窗口控制图标大小和样式 style: 统一代码格式和缩进 fix(App): 修复路径比较逻辑的空格问题 --- package.json | 1 + src/preload/index.ts | 3 ++- src/renderer/src/App.tsx | 2 +- src/renderer/src/components/ContentArea.tsx | 20 ++++++++++++------- src/renderer/src/components/Home.tsx | 18 ++++++++++------- src/renderer/src/components/TitleBar.tsx | 6 +++++- .../src/components/WindowControls.tsx | 6 +++++- 7 files changed, 38 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 5bfadff..39aaade 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "format": "prettier --write .", "lint": "eslint --cache .", + "lint:fix": "eslint --cache . --fix", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "pnpm -s typecheck:node && pnpm -s typecheck:web", diff --git a/src/preload/index.ts b/src/preload/index.ts index ca64a29..b420d3f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -86,7 +86,8 @@ const api = { return () => ipcRenderer.removeListener('app:navigate', subscription) }, toggleDevTools: () => ipcRenderer.invoke('window:toggle-devtools'), - windowResize: (width: number, height: number) => ipcRenderer.invoke('window:resize', width, height), + windowResize: (width: number, height: number) => + ipcRenderer.invoke('window:resize', width, height), // Logger queryLogs: (lines?: number) => ipcRenderer.invoke('log:query', lines), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 788a019..ba27e7d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -17,7 +17,7 @@ function MainContent(): React.JSX.Element { // 统一路径格式进行比对,防止 / 和 /home 导致重复跳转 const currentPath = location.pathname === '/' ? '/home' : location.pathname const targetPath = route === '/' ? '/home' : route - + if (currentPath !== targetPath) { navigate(route) } diff --git a/src/renderer/src/components/ContentArea.tsx b/src/renderer/src/components/ContentArea.tsx index a811a6d..9108a5e 100644 --- a/src/renderer/src/components/ContentArea.tsx +++ b/src/renderer/src/components/ContentArea.tsx @@ -3,13 +3,19 @@ import { Layout, Space, Button, Tag, Loading } from 'tdesign-react' import { Routes, Route, Navigate } from 'react-router-dom' 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 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 diff --git a/src/renderer/src/components/Home.tsx b/src/renderer/src/components/Home.tsx index d274829..25311f6 100644 --- a/src/renderer/src/components/Home.tsx +++ b/src/renderer/src/components/Home.tsx @@ -79,7 +79,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { ]) if (stuRes.success) { - const enrichedStudents = (stuRes.data as student[]).map(s => ({ + const enrichedStudents = (stuRes.data as student[]).map((s) => ({ ...s, pinyinName: pinyin(s.name, { toneType: 'none' }).toLowerCase(), pinyinFirst: getFirstLetter(s.name) @@ -120,7 +120,11 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { if (pyLower.includes(q0)) return true const q1 = q0.replace(/\s+/g, '') - if (q1 && (nameLower.replace(/\s+/g, '').includes(q1) || pyLower.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(s.name, q0) @@ -167,7 +171,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { const groups: Record = {} sortedStudents.forEach((s) => { - const key = sortType === 'alphabet' ? (s.pinyinFirst || '#') : getSurname(s.name) + const key = sortType === 'alphabet' ? s.pinyinFirst || '#' : getSurname(s.name) if (!groups[key]) groups[key] = [] groups[key].push(s) }) @@ -312,7 +316,6 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { border: '1px solid var(--ss-border-color)', overflow: 'visible' }} - hover > {rankBadge && (
= ({ canEdit }) => {
(groupRefs.current[group.key] = el)} + ref={(el) => { + groupRefs.current[group.key] = el + }} > {group.key !== 'all' && (
= ({ canEdit }) => { fontWeight: 'bold', color: 'var(--ss-text-main)', marginBottom: '16px', - paddingLeft: '4px', display: 'flex', alignItems: 'center', gap: '8px', @@ -889,4 +893,4 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
) -} \ No newline at end of file +} diff --git a/src/renderer/src/components/TitleBar.tsx b/src/renderer/src/components/TitleBar.tsx index a8322ef..2f87ff8 100644 --- a/src/renderer/src/components/TitleBar.tsx +++ b/src/renderer/src/components/TitleBar.tsx @@ -109,7 +109,11 @@ export function TitleBar({ children }: TitleBarProps): React.JSX.Element { onClick={maximize} style={{ width: '46px', height: '32px', borderRadius: 0 }} > - {isMaximized ? : } + {isMaximized ? ( + + ) : ( + + )}
@@ -121,11 +123,10 @@ export const GlobalSidebar: React.FC = () => {
diff --git a/themes/acrylic-auto.json b/themes/acrylic-auto.json new file mode 100644 index 0000000..e7eefdc --- /dev/null +++ b/themes/acrylic-auto.json @@ -0,0 +1,31 @@ +{ + "name": "Acrylic-自动", + "id": "acrylic-auto", + "mode": "light", + "mica": { + "effect": "acrylic", + "theme": "auto", + "radius": "medium" + }, + "config": { + "tdesign": { + "brandColor": "#0052D9", + "warningColor": "#E37318", + "errorColor": "#D32029", + "successColor": "#248232" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(255, 255, 255, 0.85)", + "--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-item-hover": "rgba(0, 0, 0, 0.04)", + "--ss-sidebar-text": "#1a1a1a", + "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.06)", + "--ss-sidebar-active-text": "#1a1a1a" + } + } +} diff --git a/themes/acrylic-dark.json b/themes/acrylic-dark.json new file mode 100644 index 0000000..4e86118 --- /dev/null +++ b/themes/acrylic-dark.json @@ -0,0 +1,31 @@ +{ + "name": "Acrylic-深色", + "id": "acrylic-dark", + "mode": "dark", + "mica": { + "effect": "acrylic", + "theme": "dark", + "radius": "medium" + }, + "config": { + "tdesign": { + "brandColor": "#4C8BF5", + "warningColor": "#E37318", + "errorColor": "#D54941", + "successColor": "#2BA471" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(30, 30, 30, 0.85)", + "--ss-text-main": "#e0e0e0", + "--ss-text-secondary": "#a0a0a0", + "--ss-border-color": "rgba(255, 255, 255, 0.08)", + "--ss-header-bg": "transparent", + "--ss-sidebar-bg": "rgba(30, 30, 30, 0.9)", + "--ss-item-hover": "rgba(255, 255, 255, 0.04)", + "--ss-sidebar-text": "#e0e0e0", + "--ss-sidebar-active-bg": "rgba(255, 255, 255, 0.06)", + "--ss-sidebar-active-text": "#e0e0e0" + } + } +} diff --git a/themes/acrylic-light.json b/themes/acrylic-light.json new file mode 100644 index 0000000..cd6ddd7 --- /dev/null +++ b/themes/acrylic-light.json @@ -0,0 +1,31 @@ +{ + "name": "Acrylic-浅色", + "id": "acrylic-light", + "mode": "light", + "mica": { + "effect": "acrylic", + "theme": "light", + "radius": "medium" + }, + "config": { + "tdesign": { + "brandColor": "#0052D9", + "warningColor": "#E37318", + "errorColor": "#D32029", + "successColor": "#248232" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(255, 255, 255, 0.85)", + "--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-item-hover": "rgba(0, 0, 0, 0.04)", + "--ss-sidebar-text": "#1a1a1a", + "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.06)", + "--ss-sidebar-active-text": "#1a1a1a" + } + } +} diff --git a/themes/custom.json b/themes/custom.json new file mode 100644 index 0000000..f9374db --- /dev/null +++ b/themes/custom.json @@ -0,0 +1,31 @@ +{ + "name": "自定义", + "id": "custom", + "mode": "light", + "mica": { + "effect": "mica", + "theme": "auto", + "radius": "medium" + }, + "config": { + "tdesign": { + "brandColor": "#0052D9", + "warningColor": "#E37318", + "errorColor": "#D32029", + "successColor": "#248232" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(255, 255, 255, 0.7)", + "--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-item-hover": "rgba(0, 0, 0, 0.05)", + "--ss-sidebar-text": "#1a1a1a", + "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)", + "--ss-sidebar-active-text": "#1a1a1a" + } + } +} diff --git a/themes/mica-auto.json b/themes/mica-auto.json new file mode 100644 index 0000000..c462a38 --- /dev/null +++ b/themes/mica-auto.json @@ -0,0 +1,31 @@ +{ + "name": "Mica-自动", + "id": "mica-auto", + "mode": "light", + "mica": { + "effect": "mica", + "theme": "auto", + "radius": "medium" + }, + "config": { + "tdesign": { + "brandColor": "#0052D9", + "warningColor": "#E37318", + "errorColor": "#D32029", + "successColor": "#248232" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(255, 255, 255, 0.7)", + "--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-item-hover": "rgba(0, 0, 0, 0.05)", + "--ss-sidebar-text": "#1a1a1a", + "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)", + "--ss-sidebar-active-text": "#1a1a1a" + } + } +} diff --git a/themes/mica-dark.json b/themes/mica-dark.json new file mode 100644 index 0000000..217bdec --- /dev/null +++ b/themes/mica-dark.json @@ -0,0 +1,31 @@ +{ + "name": "Mica-深色", + "id": "mica-dark", + "mode": "dark", + "mica": { + "effect": "mica", + "theme": "dark", + "radius": "medium" + }, + "config": { + "tdesign": { + "brandColor": "#4C8BF5", + "warningColor": "#E37318", + "errorColor": "#D54941", + "successColor": "#2BA471" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(30, 30, 30, 0.7)", + "--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-item-hover": "rgba(255, 255, 255, 0.05)", + "--ss-sidebar-text": "#e0e0e0", + "--ss-sidebar-active-bg": "rgba(255, 255, 255, 0.08)", + "--ss-sidebar-active-text": "#e0e0e0" + } + } +} diff --git a/themes/mica-light.json b/themes/mica-light.json new file mode 100644 index 0000000..80f274d --- /dev/null +++ b/themes/mica-light.json @@ -0,0 +1,31 @@ +{ + "name": "Mica-浅色", + "id": "mica-light", + "mode": "light", + "mica": { + "effect": "mica", + "theme": "light", + "radius": "medium" + }, + "config": { + "tdesign": { + "brandColor": "#0052D9", + "warningColor": "#E37318", + "errorColor": "#D32029", + "successColor": "#248232" + }, + "custom": { + "--ss-bg-color": "transparent", + "--ss-card-bg": "rgba(255, 255, 255, 0.7)", + "--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-item-hover": "rgba(0, 0, 0, 0.05)", + "--ss-sidebar-text": "#1a1a1a", + "--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)", + "--ss-sidebar-active-text": "#1a1a1a" + } + } +} From 3c21a05c3767dad6b32f6291f8e51927ec7f5e00 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:28:24 +0800 Subject: [PATCH 13/18] =?UTF-8?q?fix(window):=20=E4=BF=AE=E5=A4=8D=20Mica/?= =?UTF-8?q?Acrylic=20=E8=83=8C=E6=99=AF=E4=B8=8D=E9=80=8F=E6=98=8E?= =?UTF-8?q?=E5=8F=8A=E5=A4=B1=E7=84=A6=E5=A4=B1=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/services/WindowManager.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts index 3cc2852..8e68065 100644 --- a/src/main/services/WindowManager.ts +++ b/src/main/services/WindowManager.ts @@ -98,6 +98,8 @@ export class WindowManager extends Service { show: false, autoHideMenuBar: true, frame: false, + transparent: true, + backgroundColor: '#00000000', icon: this.opts.icon, title: input.title, webPreferences: { @@ -161,6 +163,18 @@ export class WindowManager extends Service { win.webContents.send('window:maximized-changed', false) }) + win.on('blur', () => { + if (input.key === 'global-sidebar' || input.key === 'main') { + this.applyMicaEffect(win) + } + }) + + win.on('focus', () => { + if (input.key === 'global-sidebar' || input.key === 'main') { + this.applyMicaEffect(win) + } + }) + win.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } @@ -226,7 +240,10 @@ export class WindowManager extends Service { } } - public setMicaEffect(win: BrowserWindow, effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' = 'mica') { + public setMicaEffect( + win: BrowserWindow, + effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' = 'mica' + ) { if (!micaElectron) return const micaWin = win as MicaWindow From 0c4877497bd5be148e5375cd849c8e7c2a98f177 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:46:21 +0800 Subject: [PATCH 14/18] =?UTF-8?q?style(theme):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E6=A0=B7=E5=BC=8F=E5=92=8C=E9=80=8F=E6=98=8E?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 custom.json 中的圆角大小 - 在 main.css 中添加 TDesign 样式覆盖 - 调整 acrylic 主题的卡片和侧边栏透明度 --- src/renderer/src/assets/main.css | 9 +++++++++ themes/acrylic-dark.json | 4 ++-- themes/acrylic-light.json | 4 ++-- themes/custom.json | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index b11421c..4ac71f1 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -1,6 +1,15 @@ @import 'tdesign-react/dist/reset.css'; @import 'tdesign-react/dist/tdesign.css'; +/* 没用过tdesign,有ai或者人类会用顺便帮我改一下 */ +.t-layout { + background-color: var(--ss-bg-color) !important; +} +.t-default-menu{ + background-color: transparent !important; +} +/* 没用过tdesign,有ai或者人类会用顺便帮我改一下 */ + html, body, #root { diff --git a/themes/acrylic-dark.json b/themes/acrylic-dark.json index 4e86118..2fbd765 100644 --- a/themes/acrylic-dark.json +++ b/themes/acrylic-dark.json @@ -16,12 +16,12 @@ }, "custom": { "--ss-bg-color": "transparent", - "--ss-card-bg": "rgba(30, 30, 30, 0.85)", + "--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.08)", "--ss-header-bg": "transparent", - "--ss-sidebar-bg": "rgba(30, 30, 30, 0.9)", + "--ss-sidebar-bg": "rgba(30, 30, 30, 0.5)", "--ss-item-hover": "rgba(255, 255, 255, 0.04)", "--ss-sidebar-text": "#e0e0e0", "--ss-sidebar-active-bg": "rgba(255, 255, 255, 0.06)", diff --git a/themes/acrylic-light.json b/themes/acrylic-light.json index cd6ddd7..12cb599 100644 --- a/themes/acrylic-light.json +++ b/themes/acrylic-light.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/custom.json b/themes/custom.json index f9374db..7e1efa1 100644 --- a/themes/custom.json +++ b/themes/custom.json @@ -5,7 +5,7 @@ "mica": { "effect": "mica", "theme": "auto", - "radius": "medium" + "radius": "large" }, "config": { "tdesign": { From 2403cfb588e0ef6c366b765e093bbdf1a7b8b97e Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:00:11 +0800 Subject: [PATCH 15/18] feat: add ThemeEditor component with real-time preview and settings integration --- src/renderer/src/App.tsx | 17 +- src/renderer/src/components/Settings.tsx | 32 ++- src/renderer/src/components/ThemeEditor.tsx | 232 ++++++++++++++++++++ 3 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 src/renderer/src/components/ThemeEditor.tsx diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ba27e7d..e6a860e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -5,7 +5,9 @@ import { Sidebar } from './components/Sidebar' import { ContentArea } from './components/ContentArea' import { Wizard } from './components/Wizard' import { ThemeProvider } from './contexts/ThemeContext' +import { ThemeEditorProvider } from './contexts/ThemeEditorContext' import { GlobalSidebar } from './components/GlobalSidebar' +import { ThemeEditor } from './components/ThemeEditor' function MainContent(): React.JSX.Element { const navigate = useNavigate() @@ -143,12 +145,15 @@ function MainContent(): React.JSX.Element { function App(): React.JSX.Element { return ( - - - } /> - } /> - - + + + + } /> + } /> + + + + ) } diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index a1ddd90..075bcbe 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -13,6 +13,7 @@ import { MessagePlugin } from 'tdesign-react' import { useTheme } from '../contexts/ThemeContext' +import { useThemeEditor } from '../contexts/ThemeEditorContext' type permissionLevel = 'admin' | 'points' | 'view' type appSettings = { @@ -23,6 +24,7 @@ type appSettings = { export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => { const { themes, currentTheme, setTheme } = useTheme() + const { startEditing } = useThemeEditor() const [activeTab, setActiveTab] = useState('appearance') const [settings, setSettings] = useState({ is_wizard_completed: false, @@ -261,15 +263,27 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
- +
+ + + +
diff --git a/src/renderer/src/components/ThemeEditor.tsx b/src/renderer/src/components/ThemeEditor.tsx new file mode 100644 index 0000000..d794a5a --- /dev/null +++ b/src/renderer/src/components/ThemeEditor.tsx @@ -0,0 +1,232 @@ +import React, { useEffect } from 'react' +import { + Drawer, + Form, + Input, + Select, + ColorPicker, + Button, + Space, + Divider, + Row, + Col +} from 'tdesign-react' +import { useThemeEditor } from '../contexts/ThemeEditorContext' +import { useTheme } from '../contexts/ThemeContext' +import { generateColorMap } from '../utils/color' +import { themeConfig } from '../../../preload/types' + +const variableGroups = { + background: { + title: '背景颜色', + items: [ + { key: '--ss-bg-color', label: '全局背景' }, + { key: '--ss-card-bg', label: '卡片背景' }, + { key: '--ss-header-bg', label: '顶部栏背景' }, + { key: '--ss-sidebar-bg', label: '侧边栏背景' } + ] + }, + text: { + title: '文字颜色', + items: [ + { key: '--ss-text-main', label: '主要文字' }, + { key: '--ss-text-secondary', label: '次要文字' }, + { key: '--ss-sidebar-active-text', label: '侧边栏选中文字' } + ] + }, + border: { + title: '边框与分割线', + items: [{ key: '--ss-border-color', label: '通用边框' }] + }, + interaction: { + title: '交互状态', + items: [ + { key: '--ss-item-hover', label: '列表悬浮' }, + { key: '--ss-sidebar-active-bg', label: '侧边栏选中背景' } + ] + } +} + +export const ThemeEditor: React.FC = () => { + const { + isEditing, + editingTheme, + updateEditingTheme, + updateConfig, + saveEditingTheme, + cancelEditing + } = useThemeEditor() + + const { currentTheme } = useTheme() + + // 实时预览逻辑 + useEffect(() => { + if (!isEditing || !editingTheme) return + + const applyPreview = (theme: themeConfig) => { + const { tdesign, custom } = theme.config + const root = document.documentElement + + // 1. 设置 TDesign 模式 + root.setAttribute('theme-mode', theme.mode) + + // 2. 设置 TDesign 品牌色 + if (tdesign.brandColor) { + const colorMap = generateColorMap(tdesign.brandColor, theme.mode) + Object.entries(colorMap).forEach(([key, value]) => { + root.style.setProperty(key, value) + }) + } + + // 3. 应用自定义变量 + Object.entries(custom).forEach(([key, value]) => { + root.style.setProperty(key, value) + }) + } + + applyPreview(editingTheme) + }, [editingTheme, isEditing]) + + // 关闭时恢复原有主题 + useEffect(() => { + if (!isEditing && currentTheme) { + const { tdesign, custom } = currentTheme.config + const root = document.documentElement + + root.setAttribute('theme-mode', currentTheme.mode) + + if (tdesign.brandColor) { + const colorMap = generateColorMap(tdesign.brandColor, currentTheme.mode) + Object.entries(colorMap).forEach(([key, value]) => { + root.style.setProperty(key, value) + }) + } + + Object.entries(custom).forEach(([key, value]) => { + root.style.setProperty(key, value) + }) + } + }, [isEditing, currentTheme]) + + if (!editingTheme) return null + + return ( + + + + + } + destroyOnClose + > + + + {/* 基本信息 */} +
+ 基本信息 + + + + updateEditingTheme({ name: v })} + placeholder="请输入主题名称" + /> + + + + + updateEditingTheme({ id: v })} + placeholder="请输入主题 ID" + /> + + + +
+ + {/* TDesign 品牌色 */} +
+ 品牌色 (Brand) + + updateConfig('tdesign', 'brandColor', v)} + enableAlpha={false} + format="HEX" + /> + +
+ + {/* 业务自定义变量 */} +
+ 界面配色 (Custom) + {Object.entries(variableGroups).map(([groupKey, group]) => ( +
+
+ {group.title} +
+ + {group.items.map((item) => ( + +
+
+ {item.label} +
+ updateConfig('custom', item.key, v)} + enableAlpha + format="HEX" + size="small" + /> +
+ + ))} +
+
+ ))} +
+
+ +
+ ) +} 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 16/18] =?UTF-8?q?feat(=E4=B8=BB=E9=A2=98):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=B8=BB=E9=A2=98=E7=BC=96=E8=BE=91=E5=99=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=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)", From 4ad68f599a5d3fe4e561a05a6b9e5dc3e17daab2 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:35:11 +0800 Subject: [PATCH 17/18] =?UTF-8?q?release:=20=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E8=87=B31.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b7cf6d..2b9febb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "secscore", - "version": "1.0.0", + "version": "1.0.1", "description": "SecScore – Your Education Points Management Expert", "main": "./out/main/index.js", "author": "example.com", From 5174a28ed37b00b655de46ef189b109ff80275ba Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:39:24 +0800 Subject: [PATCH 18/18] release:v1.0.1 --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 2b9febb..f678115 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,7 @@ "uuid": "^13.0.0", "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", - "xlsx": "^0.18.5", - "pnpm": "^8.1.2" + "xlsx": "^0.18.5" }, "devDependencies": { "@electron-toolkit/eslint-config-prettier": "^3.0.0",