From e39840acc17d3ac48f249cc7c07603d99e6c8f45 Mon Sep 17 00:00:00 2001 From: NanGua_QWQ Date: Fri, 13 Feb 2026 20:31:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=87=AA=E5=8A=A8=E8=AF=84=E5=88=86):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=87=AA=E5=8A=A8=E8=AF=84=E5=88=86=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=B9=B6=E6=B7=BB=E5=8A=A0=E8=A7=84=E5=88=99=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构AutoScoreService,支持更灵活的触发器和行动配置 - 新增规则管理界面,支持触发器和行动的动态添加与编辑 - 将规则存储从设置迁移到独立JSON文件 - 在设置页面添加版权信息 - 仅在开发环境显示开发中提示 - 调整文件系统服务初始化顺序 --- src/main/index.ts | 12 +- src/main/services/AutoScoreManager.tsx | 554 ------------------ src/main/services/AutoScoreService.ts | 211 +++++-- src/renderer/src/App.tsx | 58 +- .../src/components/AutoScoreManager.tsx | 386 +++++++----- src/renderer/src/components/Settings.tsx | 4 +- src/renderer/src/services/AutoScoreService.ts | 239 ++++++++ tsconfig.web.json | 6 +- 8 files changed, 679 insertions(+), 791 deletions(-) delete mode 100644 src/main/services/AutoScoreManager.tsx create mode 100644 src/renderer/src/services/AutoScoreService.ts diff --git a/src/main/index.ts b/src/main/index.ts index b7d06ba..51a54f2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -269,15 +269,15 @@ app.whenReady().then(async () => { TrayServiceToken, (p) => new TrayService(p.get(MainContext), config.window) ) + services.addSingleton( + FileSystemServiceToken, + (p) => new FileSystemService(p.get(MainContext), config.configDir) + ) services.addSingleton(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext))) services.addSingleton( HttpServerServiceToken, (p) => new HttpServerService(p.get(MainContext)) ) - services.addSingleton( - FileSystemServiceToken, - (p) => new FileSystemService(p.get(MainContext), config.configDir) - ) }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services @@ -303,10 +303,10 @@ app.whenReady().then(async () => { const tray = services.get(TrayServiceToken) as TrayService tray.initialize() } - const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService - autoScore.initialize?.() services.get(HttpServerServiceToken) services.get(FileSystemServiceToken) + const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService + await autoScore.initialize?.() }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services diff --git a/src/main/services/AutoScoreManager.tsx b/src/main/services/AutoScoreManager.tsx deleted file mode 100644 index 2da6ca6..0000000 --- a/src/main/services/AutoScoreManager.tsx +++ /dev/null @@ -1,554 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { AddIcon, Delete1Icon, MoveIcon } from 'tdesign-icons-react' -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' -import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism' -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 [currentPage, setCurrentPage] = useState(1) - const [pageSize, setPageSize] = useState(50) - 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: 'drag', - title: '排序', - cell: () => , - width: 60 - }, - { - 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: 130, - cell: ({ row }) => { - if (row.studentNames.length === 0) { - return 所有学生 - } - const studentList = row.studentNames.join(',\n') - return ( - - {row.studentNames.length} 名学生 - - ) - } - }, - { colKey: 'reason', title: '理由', width: 130, ellipsis: true }, - { - colKey: 'lastExecuted', - title: '最后执行', - width: 180, - 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)}> - - - - ) - } - ] - const onDragSort = (params: any) => setRules(params.newData) - - // 定义触发规则选项 - const triggerOptions = [ - { label: '学生注册', value: 'student_registered' }, - { label: '学生登录', value: 'student_logged_in' }, - { label: '完成作业', value: 'homework_completed' }, - { label: '考试通过', value: 'exam_passed' }, - { label: '参与活动', value: 'event_participated' }, - { label: '签到', value: 'check_in' }, - { label: '其他自定义事件', value: 'custom_event' } - ] - - const initialTriggers = [ - { - id: 1, - triggerEvent: triggerOptions[1], - description: '当学生登录时触发自动化', - haveValue: true, - value: 1 - }, - { - id: 2, - triggerEvent: triggerOptions[2], - description: '当学生完成作业时触发自动化', - haveValue: true, - value: '12' - }, - { - id: 3, - triggerEvent: triggerOptions[3], - description: '当学生考试通过时触发自动化', - haveValue: false, - value: null - } - ] - - const [triggerList, setTriggerList] = useState(initialTriggers) - - const handleTriggerChange = (id: number, value: string) => { - setTriggerList((prev) => - prev.map((t) => - t.id === id - ? { ...t, triggerEvent: triggerOptions.find((o) => o.value === value) || t.triggerEvent } - : t - ) - ) - } - - const handleValueChange = (id: number, val: string) => { - setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value: val } : t))) - } - - const handleDeleteTrigger = (id: number) => { - setTriggerList((prev) => prev.filter((t) => t.id !== id)) - } - - const handleAddTrigger = () => { - const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 - setTriggerList((prev) => [ - ...prev, - { id: nextId, triggerEvent: triggerOptions[0], description: '', haveValue: false, value: '' } - ]) - } - - const triggerItems = triggerList - .filter((t) => t.description !== null) - .map((triggerTest) => ( -
-
- )) - - return ( -
-

自动化加分管理

- - -
-
- - - - - - - - - - - - - 分钟 - - - - - - - - - - - - -
- -
- - -
-
-
- - setCurrentPage(pageInfo.current), - onPageSizeChange: (size) => setPageSize(size) - }} - style={{ color: 'var(--ss-text-main)' }} - /> - - - - - {triggerItems} - - - - - - - println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") - - - {/*
-

使用说明

-
    -
  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • -
  • 间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次
  • -
  • 如果"适用学生"字段为空,则自动化适用于所有学生
  • -
  • 可以随时启用/禁用自动化,不会影响已保存的自动化配置
  • -
-
*/} - - ) -} diff --git a/src/main/services/AutoScoreService.ts b/src/main/services/AutoScoreService.ts index a135a43..cb478b8 100644 --- a/src/main/services/AutoScoreService.ts +++ b/src/main/services/AutoScoreService.ts @@ -6,11 +6,26 @@ interface AutoScoreRule { id: number enabled: boolean name: string - intervalMinutes: number // 间隔分钟数 - studentNames: string[] // 学生姓名列表,空数组代表所有学生 - scoreValue: number // 每次加分值 - reason: string // 加分理由 - lastExecuted?: Date // 最后执行时间 + studentNames: string[] + lastExecuted?: Date + triggers?: { event: string; value?: string }[] + actions?: { event: string; value?: string; reason?: string }[] +} + +interface AutoScoreRuleFileData { + id: number + enabled: boolean + name: string + studentNames: string[] + lastExecuted?: string + triggers?: { event: string; value?: string }[] + actions?: { event: string; value?: string; reason?: string }[] +} + +interface AutoScoreRulesFile { + version: number + rules: AutoScoreRuleFileData[] + updatedAt?: string } declare module '../../shared/kernel' { @@ -19,6 +34,8 @@ declare module '../../shared/kernel' { } } +const RULES_FILE_NAME = 'auto-score-rules.json' + export class AutoScoreService extends Service { private rules: AutoScoreRule[] = [] private timers: Map = new Map() @@ -27,8 +44,6 @@ export class AutoScoreService extends Service { constructor(ctx: MainContext) { super(ctx, 'autoScore') this.registerIpc() - this.loadRulesFromSettings() - this.startRules() } private get mainCtx() { @@ -102,9 +117,82 @@ export class AutoScoreService extends Service { }) } + private async loadRulesFromFile(): Promise { + try { + const fs = this.mainCtx.fileSystem + if (!fs) { + this.logger.warn('FileSystemService not available, falling back to settings') + await this.loadRulesFromSettings() + return + } + + const data = await fs.readJsonFile(RULES_FILE_NAME, 'automatic') + if (data && data.rules) { + this.rules = data.rules.map((rule: any) => { + // 数据迁移:将旧格式转换为新格式 + const migratedRule = this.migrateRule(rule) + return { + ...migratedRule, + lastExecuted: migratedRule.lastExecuted ? new Date(migratedRule.lastExecuted) : undefined + } + }) + // 如果有数据迁移,保存新格式 + if (data.rules.some((rule: any) => rule.intervalMinutes !== undefined || rule.scoreValue !== undefined)) { + await this.saveRulesToFile() + } + } else { + await this.loadRulesFromSettings() + await this.saveRulesToFile() + } + } catch (error) { + this.logger.warn('Failed to load auto score rules from file, falling back to settings', { + error + }) + await this.loadRulesFromSettings() + } + } + + private migrateRule(rule: any): AutoScoreRule { + // 如果已经是新格式,直接返回 + if (!rule.intervalMinutes && !rule.scoreValue) { + return rule + } + + // 迁移旧格式到新格式 + const migratedRule: AutoScoreRule = { + id: rule.id, + enabled: rule.enabled, + name: rule.name, + studentNames: rule.studentNames || [], + lastExecuted: rule.lastExecuted, + triggers: rule.triggers || [], + actions: rule.actions || [] + } + + // 将intervalMinutes迁移到triggers + if (rule.intervalMinutes && !migratedRule.triggers?.find(t => t.event === 'interval_time_passed')) { + migratedRule.triggers = migratedRule.triggers || [] + migratedRule.triggers.push({ + event: 'interval_time_passed', + value: String(rule.intervalMinutes) + }) + } + + // 将scoreValue和reason迁移到actions + if (rule.scoreValue !== undefined && !migratedRule.actions?.find(a => a.event === 'add_score')) { + migratedRule.actions = migratedRule.actions || [] + migratedRule.actions.push({ + event: 'add_score', + value: String(rule.scoreValue), + reason: rule.reason + }) + } + + return migratedRule + } + private async loadRulesFromSettings() { try { - // 从设置中加载自动化规则 const settings = await this.mainCtx.settings.getAllRaw() const autoScoreRulesStr = settings['auto_score_rules'] || '[]' const rulesFromSettings = JSON.parse(autoScoreRulesStr) @@ -114,11 +202,40 @@ export class AutoScoreService extends Service { lastExecuted: rule.lastExecuted ? new Date(rule.lastExecuted) : undefined })) } catch (error) { - console.error('Failed to load auto score rules from settings:', error) + this.logger.error('Failed to load auto score rules from settings:', { error }) this.rules = [] } } + private async saveRulesToFile(): Promise { + try { + const fs = this.mainCtx.fileSystem + if (!fs) { + this.logger.warn('FileSystemService not available, falling back to settings') + await this.saveRulesToSettings() + return + } + + const data: AutoScoreRulesFile = { + version: 1, + rules: this.rules.map(({ lastExecuted, ...rule }) => ({ + ...rule, + lastExecuted: lastExecuted?.toISOString() + })), + updatedAt: new Date().toISOString() + } + + const success = await fs.writeJsonFile(RULES_FILE_NAME, data, 'automatic') + if (!success) { + this.logger.warn('Failed to save rules to file, falling back to settings') + await this.saveRulesToSettings() + } + } catch (error) { + this.logger.error('Failed to save auto score rules to file:', { error }) + await this.saveRulesToSettings() + } + } + private async saveRulesToSettings() { try { const rulesToSave = this.rules.map(({ lastExecuted, ...rule }) => ({ @@ -127,7 +244,7 @@ export class AutoScoreService extends Service { })) await this.mainCtx.settings.setRaw('auto_score_rules', JSON.stringify(rulesToSave)) } catch (error) { - console.error('Failed to save auto score rules to settings:', error) + this.logger.error('Failed to save auto score rules to settings:', { error }) } } @@ -146,22 +263,27 @@ export class AutoScoreService extends Service { } 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 + // 从triggers中读取间隔时间 + const intervalTrigger = rule.triggers?.find(t => t.event === 'interval_time_passed') + const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0 + + if (!intervalMinutes || intervalMinutes <= 0) { + this.logger.warn(`Rule ${rule.name} has no valid interval time, skipping timer`) + return + } + + const now = new Date() + const intervalMs = 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 } @@ -169,7 +291,6 @@ export class AutoScoreService extends Service { const timer = setTimeout(() => { this.executeRule(rule) - // 设置重复执行 this.setRuleInterval(rule) }, delayMs) @@ -177,7 +298,15 @@ export class AutoScoreService extends Service { } private setRuleInterval(rule: AutoScoreRule) { - const intervalMs = rule.intervalMinutes * 60 * 1000 + // 从triggers中读取间隔时间 + const intervalTrigger = rule.triggers?.find(t => t.event === 'interval_time_passed') + const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0 + + if (!intervalMinutes || intervalMinutes <= 0) { + return + } + + const intervalMs = intervalMinutes * 60 * 1000 const timer = setInterval(() => { this.executeRule(rule) }, intervalMs) @@ -187,18 +316,16 @@ export class AutoScoreService extends Service { private async executeRule(rule: AutoScoreRule) { try { - console.log(`Executing auto score rule: ${rule.name}`) + this.logger.info(`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) @@ -208,27 +335,32 @@ export class AutoScoreService extends Service { } } - // 为每个学生添加积分事件 + // 从actions中读取分数和理由 + const scoreAction = rule.actions?.find(a => a.event === 'add_score') + const scoreValue = scoreAction?.value ? parseInt(scoreAction.value, 10) : 0 + const reason = scoreAction?.reason || `自动化加分 - ${rule.name}` + for (const student of studentsToScore) { await eventRepo.create({ student_name: student.name, - reason_content: rule.reason || `自动化加分 - ${rule.name}`, - delta: rule.scoreValue + reason_content: reason, + delta: scoreValue }) } - // 更新规则的最后执行时间 rule.lastExecuted = new Date() - await this.saveRulesToSettings() + await this.saveRulesToFile() - console.log(`Auto score rule executed successfully for ${studentsToScore.length} students`) + this.logger.info( + `Auto score rule executed successfully for ${studentsToScore.length} students` + ) } catch (error) { - console.error(`Failed to execute auto score rule ${rule.name}:`, error) + this.logger.error(`Failed to execute auto score rule ${rule.name}:`, { error }) } } private stopRules() { - for (const [_, timer] of this.timers) { + for (const [timer] of this.timers) { clearTimeout(timer) clearInterval(timer) } @@ -244,7 +376,7 @@ export class AutoScoreService extends Service { } this.rules.push(newRule) - await this.saveRulesToSettings() + await this.saveRulesToFile() if (rule.enabled) { this.startRuleTimer(newRule) @@ -257,21 +389,17 @@ export class AutoScoreService extends Service { 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() + await this.saveRulesToFile() - // 如果规则已启用,启动新的定时器 if (updatedRule.enabled) { this.startRuleTimer(updatedRule) } @@ -283,16 +411,14 @@ export class AutoScoreService extends Service { 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() + await this.saveRulesToFile() return true } @@ -303,19 +429,17 @@ export class AutoScoreService extends Service { 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() + await this.saveRulesToFile() return true } @@ -329,13 +453,12 @@ export class AutoScoreService extends Service { async restart() { this.stopRules() - await this.loadRulesFromSettings() + await this.loadRulesFromFile() this.startRules() } async initialize(): Promise { - // 确保服务正确初始化 - await this.loadRulesFromSettings() + await this.loadRulesFromFile() this.startRules() } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index b207581..dae5638 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -140,37 +140,39 @@ function MainContent(): React.JSX.Element { -
-

- 开发中画面,不代表最终品质 -

-

- SecScore Dev ({getPlatform()}-{getArchitecture()}) -

-
+

+ 开发中画面,不代表最终品质 +

+

+ SecScore Dev ({getPlatform()}-{getArchitecture()}) +

+ + ) : null} ) } diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 2623030..cd9232b 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -2,43 +2,36 @@ import React, { useState, useEffect } from 'react' import { AddIcon, Delete1Icon, MoveIcon } from 'tdesign-icons-react' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism' +import { allTriggers, allActions, TriggerItem, ActionItem } from '../services/AutoScoreService' import { Card, Form, Input, - InputNumber, Button, MessagePlugin, Table, PrimaryTableCol, - Tag, Space, Switch, Popconfirm, Radio, Select, - TooltipLite, - DatePicker + TooltipLite } from 'tdesign-react' interface AutoScoreRule { id: number enabled: boolean name: string - intervalMinutes: number studentNames: string[] - scoreValue: number - reason: string lastExecuted?: string triggers?: { event: string; value?: string }[] + actions?: { event: string; value?: string; reason?: string }[] } interface AutoScoreRuleFormValues { name: string - intervalMinutes: number studentNames: string - scoreValue: number - reason: string } export const AutoScoreManager: React.FC = () => { @@ -55,7 +48,6 @@ export const AutoScoreManager: React.FC = () => { setLoading(true) try { - // 权限检查:确保当前为 admin try { const authRes = await (window as any).api.authGetStatus() if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { @@ -64,7 +56,6 @@ export const AutoScoreManager: React.FC = () => { return } } catch (e) { - // 如果权限检查失败,继续让后端返回更明确的错误 console.warn('Auth check failed', e) } @@ -95,45 +86,61 @@ export const AutoScoreManager: React.FC = () => { const handleSubmit = async () => { if (!(window as any).api) return - const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & { - timeUnit: string - } + const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues - if (!values.name || values.intervalMinutes == null || values.scoreValue == null) { - MessagePlugin.warning('请填写完整信息') + if (!values.name) { + MessagePlugin.warning('请填写自动化名称') return } - // 根据单位转换间隔时间 - const intervalMinutes = - values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes + // 验证触发器必填项 + if (triggerList.length === 0) { + MessagePlugin.warning('请至少添加一个触发器') + return + } + + for (const t of triggerList) { + const def = allTriggers.find((a) => a.eventName === t.eventName) + if (def?.valueType && (!t.value || String(t.value).trim() === '')) { + MessagePlugin.warning(`触发器 ${def.label} 需要填写 value`) + return + } + } + + // 验证行动必填项 + if (actionList.length === 0) { + MessagePlugin.warning('请至少添加一个行动') + return + } + + for (const a of actionList) { + const def = allActions.find((action) => action.eventName === a.eventName) + if (def?.valueType && (!a.value || String(a.value).trim() === '')) { + MessagePlugin.warning(`行动 ${def.label} 需要填写 value`) + return + } + } // 确保 studentNames 是数组类型 const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value })) + const actionsPayload = actionList.map((a) => ({ + event: a.eventName, + value: a.value, + reason: a.reason + })) const ruleData = { enabled: true, name: values.name, - intervalMinutes, studentNames, - scoreValue: values.scoreValue, - reason: values.reason || `自动化加分 - ${values.name}`, - triggers: triggersPayload + triggers: triggersPayload, + actions: actionsPayload } // 权限检查:仅管理员可创建/更新自动化 try { - // 验证触发器必填项(如果某个触发器需要 value,则确保已填写) - for (const t of triggerList) { - const def = allTriggers.find((a) => a.eventName === t.eventName) - if (def?.valueType && (!t.value || String(t.value).trim() === '')) { - MessagePlugin.warning(`触发器 ${def.label} 需要填写 value`) - return - } - } - const authRes = await (window as any).api.authGetStatus() if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { MessagePlugin.error('需要管理员权限以创建或更新自动加分自动化') @@ -161,14 +168,11 @@ export const AutoScoreManager: React.FC = () => { // 手动清空表单字段,避免 form.reset() 导致的栈溢出 form.setFieldsValue({ name: '', - intervalMinutes: undefined, - studentNames: '', - scoreValue: undefined, - reason: '', - timeUnit: 'minutes' + studentNames: '' }) setEditingRuleId(null) setTriggerList([]) + setActionList([]) fetchRules() // 刷新自动化列表 } else { MessagePlugin.error( @@ -184,10 +188,7 @@ export const AutoScoreManager: React.FC = () => { setEditingRuleId(rule.id) form.setFieldsValue({ name: rule.name, - intervalMinutes: rule.intervalMinutes, - studentNames: rule.studentNames.join(', '), - scoreValue: rule.scoreValue, - reason: rule.reason + studentNames: rule.studentNames.join(', ') }) // 如果后端返回了 triggers 字段,把它加载到 triggerList if (rule.triggers && Array.isArray(rule.triggers)) { @@ -204,6 +205,22 @@ export const AutoScoreManager: React.FC = () => { } else { setTriggerList([]) } + // 如果后端返回了 actions 字段,把它加载到 actionList + if (rule.actions && Array.isArray(rule.actions)) { + const mapped = rule.actions.map((a, idx) => { + const found = allActions.find((action) => action.eventName === a.event) + return { + id: idx + 1, + eventName: a.event, + valueType: found?.valueType, + value: a.value ?? '', + reason: a.reason + } + }) + setActionList(mapped) + } else { + setActionList([]) + } } const handleDelete = async (ruleId: number) => { @@ -264,12 +281,11 @@ export const AutoScoreManager: React.FC = () => { // 手动清空表单字段,避免 form.reset() 导致的栈溢出 form.setFieldsValue({ name: '', - intervalMinutes: undefined, - studentNames: '', - scoreValue: undefined, - reason: '' + studentNames: '' }) setEditingRuleId(null) + setTriggerList([]) + setActionList([]) } const columns: PrimaryTableCol[] = [ @@ -293,32 +309,49 @@ export const AutoScoreManager: React.FC = () => { }, { colKey: 'name', title: '自动化名称', width: 150 }, { - colKey: 'intervalMinutes', - title: '间隔', - width: 100, + colKey: 'triggers', + title: '触发器', + width: 150, cell: ({ row }) => { - const isDays = row.intervalMinutes >= 1440 - const value = isDays ? row.intervalMinutes / 1440 : row.intervalMinutes - const unit = isDays ? '天' : '分钟' - return `${value} ${unit}` + if (!row.triggers || row.triggers.length === 0) { + return + } + const triggerLabels = row.triggers.map((t) => { + const def = allTriggers.find((tr) => tr.eventName === t.event) + return def?.label || t.event + }) + return ( + + {row.triggers.length} 个触发器 + + ) } }, { - colKey: 'scoreValue', - title: '分值', - width: 80, - cell: ({ row }) => ( - 0 ? 'success' : 'danger'} variant="light"> - {row.scoreValue > 0 ? `+${row.scoreValue}` : row.scoreValue} - - ) + colKey: 'actions', + title: '行动', + width: 150, + cell: ({ row }) => { + if (!row.actions || row.actions.length === 0) { + return + } + const actionLabels = row.actions.map((a) => { + const def = allActions.find((ac) => ac.eventName === a.event) + return def?.label || a.event + }) + return ( + + {row.actions.length} 个行动 + + ) + } }, { colKey: 'studentNames', title: '适用学生', width: 130, cell: ({ row }) => { - if (row.studentNames.length === 0) { + if (!row.studentNames || row.studentNames.length === 0) { return 所有学生 } const studentList = row.studentNames.join(',\n') @@ -329,7 +362,6 @@ export const AutoScoreManager: React.FC = () => { ) } }, - { colKey: 'reason', title: '理由', width: 130, ellipsis: true }, { colKey: 'lastExecuted', title: '最后执行', @@ -364,47 +396,10 @@ export const AutoScoreManager: React.FC = () => { ] const onDragSort = (params: any) => setRules(params.newData) - type TriggerDef = { - id: number - label: string - description: string - eventName: string - valueType?: 'DatePicker' | 'Input' - } - - const allTriggers: TriggerDef[] = [ - { - id: 1, - label: '根据间隔时间触发', - description: '当学生注册时触发自动化', - eventName: 'interval_time_passed', - valueType: 'DatePicker' - }, - { - id: 2, - label: '按照学生标签触发', - description: '当学生完成作业时触发自动化', - eventName: 'student_tag_matched', - valueType: 'Input' - }, - { - id: 3, - label: '随机时间触发', - description: '当随机时间到达时触发自动化', - eventName: 'random_time_reached' - } - ] - const triggerOptions = allTriggers.map((t) => ({ label: t.label, value: t.eventName })) - // triggerList items only need id, eventName, haveValue and value - type TriggerItem = { - id: number - eventName: string - value?: string - valueType?: 'DatePicker' | 'Input' - } const [triggerList, setTriggerList] = useState([]) + const [actionList, setActionList] = useState([]) const handleTriggerChange = (id: number, value: string) => { const found = allTriggers.find((a) => a.eventName === value) @@ -437,6 +432,38 @@ export const AutoScoreManager: React.FC = () => { ]) } + // 行动管理相关函数 + const handleActionChange = (id: number, value: string) => { + const found = allActions.find((a) => a.eventName === value) + setActionList((prev) => + prev.map((t) => + t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : t + ) + ) + } + + const handleActionValueChange = (id: number, val: string) => { + setActionList((prev) => prev.map((t) => (t.id === id ? { ...t, value: val } : t))) + } + + const handleDeleteAction = (id: number) => { + setActionList((prev) => prev.filter((t) => t.id !== id)) + } + + const handleAddAction = () => { + const nextId = actionList.length ? Math.max(...actionList.map((t) => t.id)) + 1 : 1 + const defaultAction = allActions[0] + setActionList((prev) => [ + ...prev, + { + id: nextId, + eventName: defaultAction.eventName, + valueType: defaultAction.valueType, + value: '' + } + ]) + } + const triggerItems = triggerList .filter((t) => t.eventName !== null) .map((triggerTest) => ( @@ -454,25 +481,87 @@ export const AutoScoreManager: React.FC = () => { placeholder="请选择触发规则" onChange={(value) => handleTriggerChange(triggerTest.id, value as string)} /> - {triggerTest.valueType ? ( - triggerTest.eventName === 'interval_time_passed' ? ( - handleValueChange(triggerTest.id, v ? String(v) : '')} - /> - ) : ( - - handleValueChange(triggerTest.id, String((v as any).target?.value ?? v)) - } - /> - ) - ) : null} + {triggerTest.valueType + ? React.createElement(triggerTest.valueType, { + placeholder: + triggerTest.eventName === 'interval_time_passed' ? '请选择日期' : '请输入时间间隔(天)', + style: { width: '150px' }, + value: + triggerTest.eventName === 'interval_time_passed' + ? triggerTest.value + ? new Date(triggerTest.value) + : undefined + : String(triggerTest.value ?? ''), + onChange: (v: any) => handleValueChange(triggerTest.id, v ? String(v) : '') + }) + : null} + + )) + + const actionOptions = allActions.map((a) => ({ label: a.label, value: a.eventName })) + + const actionItems = actionList + .filter((a) => a.eventName !== null) + .map((action) => ( +
+
)) @@ -491,36 +580,6 @@ export const AutoScoreManager: React.FC = () => { - - - - - - - - - 分钟 - - - - - - - - - -
@@ -567,7 +622,7 @@ export const AutoScoreManager: React.FC = () => { @@ -579,7 +634,26 @@ export const AutoScoreManager: React.FC = () => { icon={} onClick={handleAddTrigger} > - 添加触发器 + 添加规则 + + + + + + + {actionItems} + diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index a95eff9..c1c34d5 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -326,7 +326,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission setHttpLoading(false) } } - + const currentYear = new Date().getFullYear() return (
= ({ permission
版本
v1.0.0
+
版权
+
{'SecScore遵循GPL3.0协议——' + 'CopyRight © 2025-' + currentYear + ' SECTL'}
Electron
{(window as any).electron?.process?.versions?.electron || '-'}
Chromium
diff --git a/src/renderer/src/services/AutoScoreService.ts b/src/renderer/src/services/AutoScoreService.ts new file mode 100644 index 0000000..5e2335c --- /dev/null +++ b/src/renderer/src/services/AutoScoreService.ts @@ -0,0 +1,239 @@ +import React from 'react' +import { Service } from '../../../shared/kernel' +import { ClientContext } from '../ClientContext' +import { Input, Select, Radio } from 'tdesign-react' + +export interface AutoScoreRule { + id: number + enabled: boolean + name: string + studentNames: string[] + lastExecuted?: string + triggers?: { event: string; value?: string }[] + actions?: { event: string; value?: string; reason?: string }[] +} + +export interface AutoScoreRuleInput { + enabled: boolean + name: string + studentNames: string[] + triggers?: { event: string; value?: string }[] + actions?: { event: string; value?: string; reason?: string }[] +} + +declare module '../../../shared/kernel' { + interface Context { + autoScore: AutoScoreService + } +} + +type RenderConfig = { + component?: React.ElementType + props?: Record + render?: (props: any) => React.ReactNode +} + +type TriggerDef = { + id: number + label: string + description: string + eventName: string + valueType?: React.ElementType + renderConfig?: RenderConfig +} + +export const allTriggers: TriggerDef[] = [ + { + id: 1, + label: '根据间隔时间触发', + description: '当学生注册时触发自动化', + eventName: 'interval_time_passed', + valueType: Input + }, + { + id: 2, + label: '按照学生标签触发', + description: '当学生完成作业时触发自动化', + eventName: 'student_tag_matched', + valueType: Input + }, + { + id: 3, + label: '随机时间触发', + description: '当随机时间到达时触发自动化', + eventName: 'random_time_reached' + } +] + +// 触发器和行动项类型定义 +export type TriggerItem = { + id: number + eventName: string + value?: string + valueType?: React.ElementType +} + +export type ActionItem = { + id: number + eventName: string + value?: string + reason?: string + valueType?: React.ElementType +} + +export const allActions: TriggerDef[] = [ + { + id: 1, + label: '添加分数', + description: '为学生添加分数', + eventName: 'add_score', + valueType: Input, + renderConfig: { + component: Input, + props: { + placeholder: '请输入分数', + style: { width: '150px' } + } + } + }, + { + id: 2, + label: '添加标签', + description: '为学生添加标签', + eventName: 'add_tag', + valueType: Input, + renderConfig: { + component: Input, + props: { + placeholder: '请输入标签', + style: { width: '150px' } + } + } + }, + { + id: 3, + label: '发送通知', + description: '向学生发送通知', + eventName: 'send_notification', + valueType: Input, + renderConfig: { + component: Input, + props: { + placeholder: '请输入通知内容', + style: { width: '150px' } + } + } + }, + { + id: 4, + label: '设置学生状态', + description: '设置学生的状态', + eventName: 'set_student_status', + valueType: Select, + renderConfig: { + component: Select, + props: { + placeholder: '请选择状态', + style: { width: '150px' }, + options: [ + { label: '活跃', value: 'active' }, + { label: '不活跃', value: 'inactive' }, + { label: '请假', value: 'leave' } + ] + } + } + }, +] + +export class AutoScoreService extends Service { + constructor(ctx: ClientContext) { + super(ctx, 'autoScore') + } + + async getRules(): Promise<{ success: boolean; data?: AutoScoreRule[]; message?: string }> { + return await (window as any).api.invoke('auto-score:getRules', {}) + } + + async addRule( + rule: AutoScoreRuleInput + ): Promise<{ success: boolean; data?: number; message?: string }> { + return await (window as any).api.invoke('auto-score:addRule', rule) + } + + async updateRule( + rule: Partial & { id: number } + ): Promise<{ success: boolean; data?: boolean; message?: string }> { + return await (window as any).api.invoke('auto-score:updateRule', rule) + } + + async deleteRule( + ruleId: number + ): Promise<{ success: boolean; data?: boolean; message?: string }> { + return await (window as any).api.invoke('auto-score:deleteRule', ruleId) + } + + async toggleRule( + ruleId: number, + enabled: boolean + ): Promise<{ success: boolean; data?: boolean; message?: string }> { + return await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled }) + } + + async getStatus(): Promise<{ success: boolean; data?: { enabled: boolean }; message?: string }> { + return await (window as any).api.invoke('auto-score:getStatus', {}) + } + + // 触发器管理相关函数 + createTriggerItem(triggerList: TriggerItem[]): TriggerItem { + const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 + const defaultTrigger = allTriggers[0] + return { + id: nextId, + eventName: defaultTrigger.eventName, + valueType: defaultTrigger.valueType, + value: '' + } + } + + updateTriggerEvent(triggerList: TriggerItem[], id: number, eventName: string): TriggerItem[] { + const found = allTriggers.find((a) => a.eventName === eventName) + return triggerList.map((t) => + t.id === id ? { ...t, eventName, valueType: found?.valueType, value: '' } : t + ) + } + + updateTriggerValue(triggerList: TriggerItem[], id: number, value: string): TriggerItem[] { + return triggerList.map((t) => (t.id === id ? { ...t, value } : t)) + } + + deleteTriggerItem(triggerList: TriggerItem[], id: number): TriggerItem[] { + return triggerList.filter((t) => t.id !== id) + } + + // 行动管理相关函数 + createActionItem(actionList: ActionItem[]): ActionItem { + const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1 + const defaultAction = allActions[0] + return { + id: nextId, + eventName: defaultAction.eventName, + valueType: defaultAction.valueType, + value: '' + } + } + + updateActionEvent(actionList: ActionItem[], id: number, eventName: string): ActionItem[] { + const found = allActions.find((a) => a.eventName === eventName) + return actionList.map((a) => + a.id === id ? { ...a, eventName, valueType: found?.valueType, value: '' } : a + ) + } + + updateActionValue(actionList: ActionItem[], id: number, value: string): ActionItem[] { + return actionList.map((a) => (a.id === id ? { ...a, value } : a)) + } + + deleteActionItem(actionList: ActionItem[], id: number): ActionItem[] { + return actionList.filter((a) => a.id !== id) + } +} diff --git a/tsconfig.web.json b/tsconfig.web.json index 4afcfe9..5bb3f60 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,8 +5,10 @@ "src/renderer/src/**/*", "src/renderer/src/**/*.tsx", "src/preload/*.d.ts", - "src/preload/types.ts" -, "src/main/services/HttpService.ts" ], + "src/preload/types.ts", + "src/shared/**/*", + "src/main/services/HttpService.ts" + ], "compilerOptions": { "composite": true, "jsx": "react-jsx",