= ({ permission
))}
-
-
-
-
-
-
-
- handleUpdateSetting('appearance_background_blur', String(v || 0))
- }
- style={{ width: '160px' }}
- />
-
@@ -396,6 +407,30 @@ export const Settings: React.FC<{ permission: PermissionLevel }> = ({ permission
+
+
+
+
+ 将当前未结算记录划分为一个阶段,并将所有学生积分清零。
+
+
+
+
{
+ const [settlements, setSettlements] = useState([])
+ const [loading, setLoading] = useState(false)
+
+ const [selectedId, setSelectedId] = useState(null)
+ const [selectedSettlement, setSelectedSettlement] = useState<
+ { id: number; start_time: string; end_time: string } | null
+ >(null)
+ const [rows, setRows] = useState([])
+ const [detailLoading, setDetailLoading] = useState(false)
+
+ const formatRange = (s: { start_time: string; end_time: string }) => {
+ const start = new Date(s.start_time).toLocaleString()
+ const end = new Date(s.end_time).toLocaleString()
+ return `${start} - ${end}`
+ }
+
+ 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
+ }
+ setSettlements(res.data || [])
+ }, [])
+
+ useEffect(() => {
+ fetchSettlements()
+ }, [fetchSettlements])
+
+ useEffect(() => {
+ const onDataUpdated = (e: any) => {
+ const category = e?.detail?.category
+ if (category === 'events' || category === 'all') fetchSettlements()
+ }
+ window.addEventListener('ss:data-updated', onDataUpdated as any)
+ return () => window.removeEventListener('ss:data-updated', onDataUpdated as any)
+ }, [fetchSettlements])
+
+ const openSettlement = async (id: number) => {
+ 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
+ }
+ setSelectedSettlement(res.data.settlement)
+ setRows(res.data.rows || [])
+ }
+
+ const columns: PrimaryTableCol[] = useMemo(
+ () => [
+ {
+ colKey: 'rank',
+ title: '排名',
+ width: 70,
+ align: 'center',
+ cell: ({ rowIndex }) => {rowIndex + 1}
+ },
+ { colKey: 'name', title: '姓名', width: 160 },
+ {
+ colKey: 'score',
+ title: '阶段积分',
+ width: 120,
+ cell: ({ row }) => {row.score}
+ }
+ ],
+ []
+ )
+
+ if (selectedId !== null && selectedSettlement) {
+ return (
+
+
+
+ 结算排行榜
+
+ {formatRange(selectedSettlement)}
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
结算历史
+
+ {settlements.map((s) => (
+
+ 阶段 #{s.id}
+
+ {formatRange(s)}
+
+
+
+
+ 记录数: {s.event_count}
+
+
+
+ ))}
+
+ {!loading && settlements.length === 0 && (
+
暂无结算记录
+ )}
+
+ )
+}
+
diff --git a/src/renderer/src/components/StudentManager.tsx b/src/renderer/src/components/StudentManager.tsx
index 427ee41..70cd94a 100644
--- a/src/renderer/src/components/StudentManager.tsx
+++ b/src/renderer/src/components/StudentManager.tsx
@@ -55,7 +55,14 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
MessagePlugin.warning('请输入姓名')
return
}
- const res = await (window as any).api.createStudent(values)
+
+ const name = values.name.trim()
+ if (data.some((s) => s.name === name)) {
+ MessagePlugin.warning('学生姓名已存在')
+ return
+ }
+
+ const res = await (window as any).api.createStudent({ ...values, name })
if (res.success) {
MessagePlugin.success('添加成功')
setVisible(false)
diff --git a/src/renderer/src/contexts/ThemeContext.tsx b/src/renderer/src/contexts/ThemeContext.tsx
index 4a07d59..812ca28 100644
--- a/src/renderer/src/contexts/ThemeContext.tsx
+++ b/src/renderer/src/contexts/ThemeContext.tsx
@@ -1,5 +1,6 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'
import { ThemeConfig } from '../../../preload/types'
+import { generateColorMap } from '../utils/color'
interface ThemeContextType {
currentTheme: ThemeConfig | null
@@ -21,7 +22,12 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
root.setAttribute('theme-mode', theme.mode)
// 2. 设置 TDesign 品牌色
- if (tdesign.brandColor) root.style.setProperty('--td-brand-color', tdesign.brandColor)
+ if (tdesign.brandColor) {
+ const colorMap = generateColorMap(tdesign.brandColor, theme.mode)
+ Object.entries(colorMap).forEach(([key, value]) => {
+ root.style.setProperty(key, value)
+ })
+ }
if (tdesign.warningColor) root.style.setProperty('--td-warning-color', tdesign.warningColor)
if (tdesign.errorColor) root.style.setProperty('--td-error-color', tdesign.errorColor)
if (tdesign.successColor) root.style.setProperty('--td-success-color', tdesign.successColor)
diff --git a/src/renderer/src/utils/color.ts b/src/renderer/src/utils/color.ts
new file mode 100644
index 0000000..7bac7e1
--- /dev/null
+++ b/src/renderer/src/utils/color.ts
@@ -0,0 +1,151 @@
+function hexToRgb(hex: string): [number, number, number] {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
+ return result
+ ? [
+ parseInt(result[1], 16),
+ parseInt(result[2], 16),
+ parseInt(result[3], 16)
+ ]
+ : [0, 0, 0]
+}
+
+function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
+ r /= 255
+ g /= 255
+ b /= 255
+ const max = Math.max(r, g, b)
+ const min = Math.min(r, g, b)
+ let h = 0
+ let s = 0
+ const v = max
+ const d = max - min
+ s = max === 0 ? 0 : d / max
+
+ if (max !== min) {
+ switch (max) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0)
+ break
+ case g:
+ h = (b - r) / d + 2
+ break
+ case b:
+ h = (r - g) / d + 4
+ break
+ }
+ h /= 6
+ }
+ return [h * 360, s, v]
+}
+
+function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
+ let r = 0,
+ g = 0,
+ b = 0
+ const i = Math.floor(h / 60)
+ const f = h / 60 - i
+ const p = v * (1 - s)
+ const q = v * (1 - f * s)
+ const t = v * (1 - (1 - f) * s)
+ switch (i % 6) {
+ case 0:
+ r = v
+ g = t
+ b = p
+ break
+ case 1:
+ r = q
+ g = v
+ b = p
+ break
+ case 2:
+ r = p
+ g = v
+ b = t
+ break
+ case 3:
+ r = p
+ g = q
+ b = v
+ break
+ case 4:
+ r = t
+ g = p
+ b = v
+ break
+ case 5:
+ r = v
+ g = p
+ b = q
+ break
+ }
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
+}
+
+function componentToHex(c: number): string {
+ const hex = c.toString(16)
+ return hex.length === 1 ? '0' + hex : hex
+}
+
+function rgbToHex(r: number, g: number, b: number): string {
+ return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b)
+}
+
+export function generateColorMap(
+ brandColor: string,
+ mode: 'light' | 'dark'
+): Record {
+ const [r, g, b] = hexToRgb(brandColor)
+ const [h, s, v] = rgbToHsv(r, g, b)
+
+ const colors: Record = {}
+ const palette: string[] = []
+
+ for (let i = 1; i <= 10; i++) {
+ let newS = s
+ let newV = v
+
+ if (i === 6) {
+ palette.push(brandColor)
+ continue
+ }
+
+ if (mode === 'light') {
+ if (i < 6) {
+ newS = s * Math.pow(0.6, (6 - i) / 5)
+ newV = v + (1 - v) * ((6 - i) / 5) * 0.9
+ } else {
+ newS = s + (1 - s) * ((i - 6) / 4) * 0.2
+ newV = v * Math.pow(0.85, (i - 6) / 4)
+ }
+ } else {
+ if (i < 6) {
+ newS = s * Math.pow(0.8, (6 - i) / 5)
+ newV = v * Math.pow(0.9, (6 - i) / 5)
+ } else {
+ newS = s + (1 - s) * ((i - 6) / 4) * 0.4
+ newV = v + (1 - v) * ((i - 6) / 4) * 0.5
+ }
+ }
+
+ newS = Math.max(0, Math.min(1, newS))
+ newV = Math.max(0, Math.min(1, newV))
+
+ const [nr, ng, nb] = hsvToRgb(h, newS, newV)
+ palette.push(rgbToHex(nr, ng, nb))
+ }
+
+ palette.forEach((color, index) => {
+ colors[`--td-brand-color-${index + 1}`] = color
+ })
+
+ colors['--td-brand-color'] = brandColor
+
+ colors['--td-brand-color-light'] = palette[0]
+ colors['--td-brand-color-focus'] = palette[1]
+ colors['--td-brand-color-disabled'] = palette[2]
+ colors['--td-brand-color-hover'] = palette[4]
+ colors['--td-brand-color-active'] = palette[6]
+
+ return colors
+}
diff --git a/themes/dark.json b/themes/dark.json
index 3c59ae8..0d247f8 100644
--- a/themes/dark.json
+++ b/themes/dark.json
@@ -17,7 +17,8 @@
"--ss-border-color": "#333333",
"--ss-header-bg": "#1e1e1e",
"--ss-sidebar-bg": "#1e1e1e",
- "--ss-item-hover": "#2c2c2c"
+ "--ss-item-hover": "#2c2c2c",
+ "--ss-sidebar-text": "#ffffff"
}
}
}