diff --git a/src/main/hosting/index.ts b/src/main/hosting/index.ts index cab2910..8edddca 100644 --- a/src/main/hosting/index.ts +++ b/src/main/hosting/index.ts @@ -14,7 +14,8 @@ export { SettlementRepositoryToken, ThemeServiceToken, WindowManagerToken, - TrayServiceToken + TrayServiceToken, + AutoScoreServiceToken } from './tokens' export type { appRuntimeContext, diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts index a28ad91..40b66f1 100644 --- a/src/main/hosting/tokens.ts +++ b/src/main/hosting/tokens.ts @@ -13,3 +13,4 @@ export const SettlementRepositoryToken = Symbol.for('secscore.settlementReposito export const ThemeServiceToken = Symbol.for('secscore.themeService') export const WindowManagerToken = Symbol.for('secscore.windowManager') export const TrayServiceToken = Symbol.for('secscore.trayService') +export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService') diff --git a/src/main/index.ts b/src/main/index.ts index 55ac2fa..f829c8b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,6 +15,7 @@ import { DataService } from './services/DataService' import { ThemeService } from './services/ThemeService' import { WindowManager, type windowManagerOptions } from './services/WindowManager' import { TrayService } from './services/TrayService' +import { AutoScoreService } from './services/AutoScoreService' import { StudentRepository } from './repos/StudentRepository' import { ReasonRepository } from './repos/ReasonRepository' import { EventRepository } from './repos/EventRepository' @@ -33,7 +34,8 @@ import { StudentRepositoryToken, ThemeServiceToken, WindowManagerToken, - TrayServiceToken + TrayServiceToken, + AutoScoreServiceToken } from './hosting' type mainAppConfig = { @@ -251,6 +253,32 @@ app.whenReady().then(async () => { TrayServiceToken, (p) => new TrayService(p.get(MainContext), config.window) ) + services.addSingleton( + AutoScoreServiceToken, + (p) => new AutoScoreService(p.get(MainContext)) + ) + }) + .configure(async (_builderContext, appCtx) => { + const services = appCtx.services + services.get(LoggerToken) + const db = services.get(DbManagerToken) as DbManager + await db.initialize() + const settings = services.get(SettingsStoreToken) as SettingsService + await settings.initialize() + services.get(SecurityServiceToken) + services.get(PermissionServiceToken) + services.get(AuthService) + services.get(DataService) + services.get(StudentRepositoryToken) + services.get(ReasonRepositoryToken) + services.get(EventRepositoryToken) + services.get(SettlementRepositoryToken) + services.get(ThemeServiceToken) + services.get(WindowManagerToken) + const tray = services.get(TrayServiceToken) as TrayService + tray.initialize() + const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService + autoScore.initialize?.() }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services diff --git a/src/main/services/AutoScoreService.ts b/src/main/services/AutoScoreService.ts new file mode 100644 index 0000000..b7fa94d --- /dev/null +++ b/src/main/services/AutoScoreService.ts @@ -0,0 +1,345 @@ +import { Service } from '../../shared/kernel' +import { MainContext } from '../context' +import { student } from '../repos/StudentRepository' + +interface AutoScoreRule { + id: number + enabled: boolean + name: string + intervalMinutes: number // 间隔分钟数 + studentNames: string[] // 学生姓名列表,空数组代表所有学生 + scoreValue: number // 每次加分值 + reason: string // 加分理由 + lastExecuted?: Date // 最后执行时间 +} + +declare module '../../shared/kernel' { + interface Context { + autoScore: AutoScoreService + } +} + +export class AutoScoreService extends Service { + private rules: AutoScoreRule[] = [] + private timers: Map = new Map() + private initialized = false + + constructor(ctx: MainContext) { + super(ctx, 'autoScore') + this.registerIpc() + this.loadRulesFromSettings() + this.startRules() + } + + private get mainCtx() { + return this.ctx as MainContext + } + + private registerIpc() { + this.mainCtx.handle('auto-score:getRules', async (event) => { + try { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + return { success: true, data: this.getRules() } + } catch (err: any) { + return { success: false, message: err.message } + } + }) + + this.mainCtx.handle('auto-score:addRule', async (event, rule) => { + try { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + const id = await this.addRule(rule) + return { success: true, data: id } + } catch (err: any) { + return { success: false, message: err.message } + } + }) + + this.mainCtx.handle('auto-score:updateRule', async (event, rule) => { + try { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + const success = await this.updateRule(rule) + return { success, data: success } + } catch (err: any) { + return { success: false, message: err.message } + } + }) + + this.mainCtx.handle('auto-score:deleteRule', async (event, ruleId) => { + try { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + const success = await this.deleteRule(ruleId) + return { success, data: success } + } catch (err: any) { + return { success: false, message: err.message } + } + }) + + this.mainCtx.handle('auto-score:toggleRule', async (event, params) => { + try { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + const { ruleId, enabled } = params + const success = await this.toggleRule(ruleId, enabled) + return { success, data: success } + } catch (err: any) { + return { success: false, message: err.message } + } + }) + + this.mainCtx.handle('auto-score:getStatus', async (event) => { + try { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + return { success: true, data: { enabled: this.isEnabled() } } + } catch (err: any) { + return { success: false, message: err.message } + } + }) + } + + private async loadRulesFromSettings() { + try { + // 从设置中加载自动化规则 + const settings = await this.mainCtx.settings.getAllRaw() + const autoScoreRulesStr = settings['auto_score_rules'] || '[]' + const rulesFromSettings = JSON.parse(autoScoreRulesStr) + + this.rules = rulesFromSettings.map((rule: any) => ({ + ...rule, + lastExecuted: rule.lastExecuted ? new Date(rule.lastExecuted) : undefined + })) + } catch (error) { + console.error('Failed to load auto score rules from settings:', error) + this.rules = [] + } + } + + private async saveRulesToSettings() { + try { + const rulesToSave = this.rules.map(({ lastExecuted, ...rule }) => ({ + ...rule, + lastExecuted: lastExecuted?.toISOString() + })) + await this.mainCtx.settings.setRaw('auto_score_rules', JSON.stringify(rulesToSave)) + } catch (error) { + console.error('Failed to save auto score rules to settings:', error) + } + } + + private async startRules() { + if (this.initialized) { + this.stopRules() + } + + for (const rule of this.rules) { + if (rule.enabled) { + this.startRuleTimer(rule) + } + } + + this.initialized = true + } + + private startRuleTimer(rule: AutoScoreRule) { + // 清除现有的定时器 + if (this.timers.has(rule.id)) { + clearTimeout(this.timers.get(rule.id)!) + this.timers.delete(rule.id) + } + + // 计算下次执行时间 + const now = new Date() + const intervalMs = rule.intervalMinutes * 60 * 1000 + + // 如果规则之前执行过,计算从上次执行到现在需要等待的时间 + let delayMs = intervalMs + if (rule.lastExecuted) { + const timeSinceLastExecution = now.getTime() - rule.lastExecuted.getTime() + delayMs = intervalMs - (timeSinceLastExecution % intervalMs) + // 如果已经超过了间隔时间,立即执行 + if (timeSinceLastExecution >= intervalMs) { + delayMs = 0 + } + } + + const timer = setTimeout(() => { + this.executeRule(rule) + // 设置重复执行 + this.setRuleInterval(rule) + }, delayMs) + + this.timers.set(rule.id, timer) + } + + private setRuleInterval(rule: AutoScoreRule) { + const intervalMs = rule.intervalMinutes * 60 * 1000 + const timer = setInterval(() => { + this.executeRule(rule) + }, intervalMs) + + this.timers.set(rule.id, timer) + } + + private async executeRule(rule: AutoScoreRule) { + try { + console.log(`Executing auto score rule: ${rule.name}`) + + const studentRepo = this.mainCtx.students + const eventRepo = this.mainCtx.events + + let studentsToScore: student[] = [] + if (rule.studentNames.length === 0) { + // 如果没有指定学生,对所有学生加分 + const allStudents = await studentRepo.findAll() + studentsToScore = allStudents + } else { + // 否则只对指定的学生加分 + const allStudents = await studentRepo.findAll() + for (const name of rule.studentNames) { + const student = allStudents.find(s => s.name === name) + if (student) { + studentsToScore.push(student) + } + } + } + + // 为每个学生添加积分事件 + for (const student of studentsToScore) { + await eventRepo.create({ + student_name: student.name, + reason_content: rule.reason || `自动化加分 - ${rule.name}`, + delta: rule.scoreValue + }) + } + + // 更新规则的最后执行时间 + rule.lastExecuted = new Date() + await this.saveRulesToSettings() + + console.log(`Auto score rule executed successfully for ${studentsToScore.length} students`) + } catch (error) { + console.error(`Failed to execute auto score rule ${rule.name}:`, error) + } + } + + private stopRules() { + for (const [_, timer] of this.timers) { + clearTimeout(timer) + clearInterval(timer) + } + this.timers.clear() + } + + async addRule(rule: Omit): Promise { + const newId = this.rules.length > 0 ? Math.max(...this.rules.map(r => r.id)) + 1 : 1 + const newRule: AutoScoreRule = { + ...rule, + id: newId, + lastExecuted: undefined + } + + this.rules.push(newRule) + await this.saveRulesToSettings() + + if (rule.enabled) { + this.startRuleTimer(newRule) + } + + return newId + } + + async updateRule(rule: Partial & { id: number }): Promise { + const index = this.rules.findIndex(r => r.id === rule.id) + if (index === -1) return false + + // 停止旧的定时器 + if (this.timers.has(rule.id)) { + clearTimeout(this.timers.get(rule.id)!) + clearInterval(this.timers.get(rule.id)!) + this.timers.delete(rule.id) + } + + // 更新规则 + const updatedRule = { ...this.rules[index], ...rule } + this.rules[index] = updatedRule + + // 保存到设置 + await this.saveRulesToSettings() + + // 如果规则已启用,启动新的定时器 + if (updatedRule.enabled) { + this.startRuleTimer(updatedRule) + } + + return true + } + + async deleteRule(ruleId: number): Promise { + const index = this.rules.findIndex(r => r.id === ruleId) + if (index === -1) return false + + // 停止定时器 + if (this.timers.has(ruleId)) { + clearTimeout(this.timers.get(ruleId)!) + clearInterval(this.timers.get(ruleId)!) + this.timers.delete(ruleId) + } + + // 从数组中移除规则 + this.rules.splice(index, 1) + await this.saveRulesToSettings() + + return true + } + + async toggleRule(ruleId: number, enabled: boolean): Promise { + const rule = this.rules.find(r => r.id === ruleId) + if (!rule) return false + + rule.enabled = enabled + + // 停止现有定时器 + if (this.timers.has(ruleId)) { + clearTimeout(this.timers.get(ruleId)!) + clearInterval(this.timers.get(ruleId)!) + this.timers.delete(ruleId) + } + + // 如果规则现在是启用的,启动定时器 + if (enabled) { + this.startRuleTimer(rule) + } + + await this.saveRulesToSettings() + return true + } + + getRules(): AutoScoreRule[] { + return [...this.rules] + } + + isEnabled(): boolean { + return this.rules.some(rule => rule.enabled) + } + + async restart() { + this.stopRules() + await this.loadRulesFromSettings() + this.startRules() + } + + async initialize(): Promise { + // 确保服务正确初始化 + await this.loadRulesFromSettings() + this.startRules() + } + + async dispose(): Promise { + this.stopRules() + } +} \ No newline at end of file diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index f344246..14ed658 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -78,6 +78,16 @@ export class SettingsService extends Service { defaultValue: 'rounded', writePermission: 'admin', validate: (v) => v === 'rounded' || v === 'small' || v === 'square' + }, + auto_score_enabled: { + kind: 'boolean', + defaultValue: false, + writePermission: 'admin' + }, + auto_score_rules: { + kind: 'json', + defaultValue: [], + writePermission: 'admin' } } diff --git a/src/preload/index.ts b/src/preload/index.ts index ecd5836..15deba5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -104,6 +104,9 @@ const api = { ipcRenderer.invoke('log:write', payload), registerUrlProtocol: () => ipcRenderer.invoke('app:register-url-protocol') + , + // Generic invoke wrapper for backward compatibility with callers using `api.invoke` + invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args) } if (process.contextIsolated) { diff --git a/src/preload/types.ts b/src/preload/types.ts index ebeedc5..e65444d 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -29,6 +29,8 @@ export type settingsSpec = { window_theme: 'auto' | 'dark' | 'light' window_effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' window_radius: 'rounded' | 'small' | 'square' + auto_score_enabled: boolean + auto_score_rules: any[] } export type settingsKey = keyof settingsSpec @@ -152,4 +154,6 @@ export interface electronApi { }) => Promise> registerUrlProtocol: () => Promise> + // Generic invoke wrapper (minimal compatibility API) + invoke?: (channel: string, ...args: any[]) => Promise } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c272ef5..75d1618 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -41,6 +41,7 @@ function MainContent(): React.JSX.Element { if (p.startsWith('/leaderboard')) return 'leaderboard' if (p.startsWith('/settlements')) return 'settlements' if (p.startsWith('/reasons')) return 'reasons' + if (p.startsWith('/auto-score')) return 'auto-score' if (p.startsWith('/settings')) return 'settings' return 'home' }, [location.pathname]) @@ -103,6 +104,7 @@ function MainContent(): React.JSX.Element { if (key === 'leaderboard') navigate('/leaderboard') if (key === 'settlements') navigate('/settlements') if (key === 'reasons') navigate('/reasons') + if (key === 'auto-score') navigate('/auto-score') if (key === 'settings') navigate('/settings') } diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx new file mode 100644 index 0000000..75664df --- /dev/null +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -0,0 +1,440 @@ +import React, { useState, useEffect } from 'react' +import { + Card, + Form, + Input, + InputNumber, + Button, + MessagePlugin, + Table, + PrimaryTableCol, + Tag, + Space, + Switch, + Popconfirm, + Radio, + Select, + TooltipLite +} from 'tdesign-react' + +interface AutoScoreRule { + id: number + enabled: boolean + name: string + intervalMinutes: number + studentNames: string[] + scoreValue: number + reason: string + lastExecuted?: string +} + +interface AutoScoreRuleFormValues { + name: string + intervalMinutes: number + studentNames: string + scoreValue: number + reason: string +} + +export const AutoScoreManager: React.FC = () => { + const [rules, setRules] = useState([]) + const [students, setStudents] = useState<{ id: number; name: string }[]>([]) + const [loading, setLoading] = useState(false) + const [form] = Form.useForm() + const [editingRuleId, setEditingRuleId] = useState(null) + + const fetchRules = async () => { + if (!(window as any).api) return + + setLoading(true) + try { + // 权限检查:确保当前为 admin + try { + const authRes = await (window as any).api.authGetStatus() + if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { + MessagePlugin.error('需要管理员权限以查看自动加分规则,请先登录管理员账号') + setLoading(false) + return + } + } catch (e) { + // 如果权限检查失败,继续让后端返回更明确的错误 + console.warn('Auth check failed', e) + } + + const [rulesRes, studentsRes] = await Promise.all([ + (window as any).api.invoke('auto-score:getRules', {}), + (window as any).api.queryStudents({}) + ]) + if (rulesRes.success) { + setRules(rulesRes.data) + } else { + MessagePlugin.error(rulesRes.message || '获取规则失败') + } + if (studentsRes.success) { + setStudents(studentsRes.data) + } + } catch (error) { + console.error('Failed to fetch auto score rules:', error) + MessagePlugin.error('获取规则失败') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchRules() + }, []) + + const handleSubmit = async () => { + if (!(window as any).api) return; + + const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & { timeUnit: string }; + + if (!values.name || values.intervalMinutes == null || values.scoreValue == null) { + MessagePlugin.warning('请填写完整信息'); + return; + } + + // 根据单位转换间隔时间 + const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes; + + // 确保 studentNames 是数组类型 + const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []; + + const ruleData = { + enabled: true, + name: values.name, + intervalMinutes, + studentNames, + scoreValue: values.scoreValue, + reason: values.reason || `自动化加分 - ${values.name}`, + }; + + // 权限检查:仅管理员可创建/更新规则 + try { + const authRes = await (window as any).api.authGetStatus() + if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { + MessagePlugin.error('需要管理员权限以创建或更新自动加分规则') + return + } + } catch (e) { + console.warn('Auth check failed', e) + } + + try { + let res + if (editingRuleId !== null) { + // 更新现有规则 + res = await (window as any).api.invoke('auto-score:updateRule', { + id: editingRuleId, + ...ruleData + }) + } else { + // 创建新规则 + res = await (window as any).api.invoke('auto-score:addRule', ruleData) + } + + if (res.success) { + MessagePlugin.success(editingRuleId !== null ? '规则更新成功' : '规则创建成功') + // 手动清空表单字段,避免 form.reset() 导致的栈溢出 + form.setFieldsValue({ + name: '', + intervalMinutes: undefined, + studentNames: '', + scoreValue: undefined, + reason: '', + timeUnit: 'minutes' + }) + setEditingRuleId(null) + fetchRules() // 刷新规则列表 + } else { + MessagePlugin.error(res.message || (editingRuleId !== null ? '更新规则失败' : '创建规则失败')) + } + } catch (error) { + console.error('Failed to submit auto score rule:', error) + MessagePlugin.error(editingRuleId !== null ? '更新规则失败' : '创建规则失败') + } + } + const handleEdit = (rule: AutoScoreRule) => { + setEditingRuleId(rule.id) + form.setFieldsValue({ + name: rule.name, + intervalMinutes: rule.intervalMinutes, + studentNames: rule.studentNames.join(', '), + scoreValue: rule.scoreValue, + reason: rule.reason + }) + } + + const handleDelete = async (ruleId: number) => { + if (!(window as any).api) return + // 权限检查 + try { + const authRes = await (window as any).api.authGetStatus() + if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { + MessagePlugin.error('需要管理员权限以删除自动加分规则') + return + } + } catch (e) { + console.warn('Auth check failed', e) + } + + try { + const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId) + if (res.success) { + MessagePlugin.success('规则删除成功') + fetchRules() // 刷新规则列表 + } else { + MessagePlugin.error(res.message || '删除规则失败') + } + } catch (error) { + console.error('Failed to delete auto score rule:', error) + MessagePlugin.error('删除规则失败') + } + } + + const handleToggle = async (ruleId: number, enabled: boolean) => { + if (!(window as any).api) return + // 权限检查 + try { + const authRes = await (window as any).api.authGetStatus() + if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { + MessagePlugin.error('需要管理员权限以启用/禁用自动加分规则') + return + } + } catch (e) { + console.warn('Auth check failed', e) + } + + try { + const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled }) + if (res.success) { + MessagePlugin.success(enabled ? '规则已启用' : '规则已禁用') + fetchRules() // 刷新规则列表 + } else { + MessagePlugin.error(res.message || (enabled ? '启用规则失败' : '禁用规则失败')) + } + } catch (error) { + console.error('Failed to toggle auto score rule:', error) + MessagePlugin.error(enabled ? '启用规则失败' : '禁用规则失败') + } + } + + const handleResetForm = () => { + // 手动清空表单字段,避免 form.reset() 导致的栈溢出 + form.setFieldsValue({ + name: '', + intervalMinutes: undefined, + studentNames: '', + scoreValue: undefined, + reason: '' + }) + setEditingRuleId(null) + } + + const columns: PrimaryTableCol[] = [ + { + colKey: 'enabled', + title: '状态', + width: 80, + cell: ({ row }) => ( + handleToggle(row.id, value)} + size="small" + /> + ) + }, + { colKey: 'name', title: '规则名称', width: 150 }, + { + colKey: 'intervalMinutes', + title: '间隔', + width: 100, + cell: ({ row }) => { + const isDays = row.intervalMinutes >= 1440 + const value = isDays ? row.intervalMinutes / 1440 : row.intervalMinutes + const unit = isDays ? '天' : '分钟' + return `${value} ${unit}` + } + }, + { + colKey: 'scoreValue', + title: '分值', + width: 80, + cell: ({ row }) => ( + 0 ? 'success' : 'danger'} variant="light"> + {row.scoreValue > 0 ? `+${row.scoreValue}` : row.scoreValue} + + ) + }, + { + colKey: 'studentNames', + title: '适用学生', + width: 150, + cell: ({ row }) => { + if (row.studentNames.length === 0) { + return 所有学生 + } + const studentList = row.studentNames.join(',\n') + return ( + + {row.studentNames.length} 名学生 + + ) + } + }, + { colKey: 'reason', title: '理由', ellipsis: true }, + { + colKey: 'lastExecuted', + title: '最后执行', + width: 150, + cell: ({ row }) => { + if (!row.lastExecuted) return 未执行 + try { + const date = new Date(row.lastExecuted) + return date.toLocaleString() + } catch { + return 无效时间 + } + } + }, + { + colKey: 'operation', + title: '操作', + width: 150, + cell: ({ row }) => ( + + + handleDelete(row.id)} + > + + + + ) + } + ] + + return ( +
+

自动化加分管理

+ + +
+
+ + + + + + + + + + + + + 分钟 + + + + + + + + + + + + +
+ +
+ + +
+
+
+ + + + + +
+

使用说明

+
    +
  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • +
  • 间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次
  • +
  • 如果"适用学生"字段为空,则规则适用于所有学生
  • +
  • 可以随时启用/禁用规则,不会影响已保存的规则配置
  • +
+
+ + ) +} \ No newline at end of file diff --git a/src/renderer/src/components/ContentArea.tsx b/src/renderer/src/components/ContentArea.tsx index a049d52..4993e67 100644 --- a/src/renderer/src/components/ContentArea.tsx +++ b/src/renderer/src/components/ContentArea.tsx @@ -17,6 +17,9 @@ const Leaderboard = lazy(() => import('./Leaderboard').then((m) => ({ default: m const SettlementHistory = lazy(() => import('./SettlementHistory').then((m) => ({ default: m.SettlementHistory })) ) +const AutoScoreManager = lazy(() => + import('./AutoScoreManager').then((m) => ({ default: m.AutoScoreManager })) +) const { Content } = Layout @@ -121,6 +124,7 @@ export function ContentArea({ } /> } /> } /> + } /> } /> } /> diff --git a/src/renderer/src/components/ScoreManager.tsx b/src/renderer/src/components/ScoreManager.tsx index e1afd14..227b741 100644 --- a/src/renderer/src/components/ScoreManager.tsx +++ b/src/renderer/src/components/ScoreManager.tsx @@ -138,7 +138,10 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { return } const values = form.getFieldsValue(true) as any - if (!values.student_name || !values.reason_content) { + + // 支持多选学生 + const studentNames = Array.isArray(values.student_name) ? values.student_name : [values.student_name] + if (!studentNames || studentNames.length === 0 || !values.reason_content) { MessagePlugin.warning('请填写完整信息') return } @@ -161,26 +164,39 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { : Math.abs(deltaInput) : Number(selectedReason?.delta ?? 0) - const res = await (window as any).api.createEvent({ - student_name: values.student_name, - reason_content: values.reason_content, - delta: delta - }) + // 为每个选中的学生创建事件 + try { + let successCount = 0 + for (const studentName of studentNames) { + const res = await (window as any).api.createEvent({ + student_name: studentName, + reason_content: values.reason_content, + delta: delta + }) + if (res.success) { + successCount++ + } + } - if (res.success) { - MessagePlugin.success('积分提交成功') - form.setFieldsValue({ - delta: undefined, - reason_content: '', - reason_id: undefined, - type: 'add' - }) - fetchData() - emitDataUpdated('events') - } else { - MessagePlugin.error(res.message || '提交失败') + if (successCount === studentNames.length) { + MessagePlugin.success(`已为 ${successCount} 名学生提交积分`) + form.setFieldsValue({ + student_name: [], + delta: undefined, + reason_content: '', + reason_id: undefined, + type: 'add' + }) + fetchData() + emitDataUpdated('events') + } else { + MessagePlugin.warning(`成功提交 ${successCount}/${studentNames.length} 名学生的积分`) + fetchData() + emitDataUpdated('events') + } + } finally { + setSubmitLoading(false) } - setSubmitLoading(false) } const handleUndo = async (uuid: string) => { @@ -250,6 +266,7 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {