mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat(自动评分): 重构自动评分服务并添加规则管理界面
- 重构AutoScoreService,支持更灵活的触发器和行动配置 - 新增规则管理界面,支持触发器和行动的动态添加与编辑 - 将规则存储从设置迁移到独立JSON文件 - 在设置页面添加版权信息 - 仅在开发环境显示开发中提示 - 调整文件系统服务初始化顺序
This commit is contained in:
+6
-6
@@ -269,15 +269,15 @@ app.whenReady().then(async () => {
|
|||||||
TrayServiceToken,
|
TrayServiceToken,
|
||||||
(p) => new TrayService(p.get(MainContext), config.window)
|
(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(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext)))
|
||||||
services.addSingleton(
|
services.addSingleton(
|
||||||
HttpServerServiceToken,
|
HttpServerServiceToken,
|
||||||
(p) => new HttpServerService(p.get(MainContext))
|
(p) => new HttpServerService(p.get(MainContext))
|
||||||
)
|
)
|
||||||
services.addSingleton(
|
|
||||||
FileSystemServiceToken,
|
|
||||||
(p) => new FileSystemService(p.get(MainContext), config.configDir)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.configure(async (_builderContext, appCtx) => {
|
.configure(async (_builderContext, appCtx) => {
|
||||||
const services = appCtx.services
|
const services = appCtx.services
|
||||||
@@ -303,10 +303,10 @@ app.whenReady().then(async () => {
|
|||||||
const tray = services.get(TrayServiceToken) as TrayService
|
const tray = services.get(TrayServiceToken) as TrayService
|
||||||
tray.initialize()
|
tray.initialize()
|
||||||
}
|
}
|
||||||
const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService
|
|
||||||
autoScore.initialize?.()
|
|
||||||
services.get(HttpServerServiceToken)
|
services.get(HttpServerServiceToken)
|
||||||
services.get(FileSystemServiceToken)
|
services.get(FileSystemServiceToken)
|
||||||
|
const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService
|
||||||
|
await autoScore.initialize?.()
|
||||||
})
|
})
|
||||||
.configure(async (_builderContext, appCtx) => {
|
.configure(async (_builderContext, appCtx) => {
|
||||||
const services = appCtx.services
|
const services = appCtx.services
|
||||||
|
|||||||
@@ -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<AutoScoreRule[]>([])
|
|
||||||
const [students, setStudents] = useState<{ id: number; name: string }[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
const [pageSize, setPageSize] = useState<number>(50)
|
|
||||||
const [form] = Form.useForm()
|
|
||||||
const [editingRuleId, setEditingRuleId] = useState<number | null>(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<AutoScoreRule>[] = [
|
|
||||||
{
|
|
||||||
colKey: 'drag',
|
|
||||||
title: '排序',
|
|
||||||
cell: () => <MoveIcon />,
|
|
||||||
width: 60
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'enabled',
|
|
||||||
title: '状态',
|
|
||||||
width: 80,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Switch
|
|
||||||
value={row.enabled}
|
|
||||||
onChange={(value) => 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 }) => (
|
|
||||||
<Tag theme={row.scoreValue > 0 ? 'success' : 'danger'} variant="light">
|
|
||||||
{row.scoreValue > 0 ? `+${row.scoreValue}` : row.scoreValue}
|
|
||||||
</Tag>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'studentNames',
|
|
||||||
title: '适用学生',
|
|
||||||
width: 130,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (row.studentNames.length === 0) {
|
|
||||||
return <span>所有学生</span>
|
|
||||||
}
|
|
||||||
const studentList = row.studentNames.join(',\n')
|
|
||||||
return (
|
|
||||||
<TooltipLite content={studentList} showArrow placement="mouse" theme="default">
|
|
||||||
{row.studentNames.length} 名学生
|
|
||||||
</TooltipLite>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ colKey: 'reason', title: '理由', width: 130, ellipsis: true },
|
|
||||||
{
|
|
||||||
colKey: 'lastExecuted',
|
|
||||||
title: '最后执行',
|
|
||||||
width: 180,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row.lastExecuted) return <span>未执行</span>
|
|
||||||
try {
|
|
||||||
const date = new Date(row.lastExecuted)
|
|
||||||
return date.toLocaleString()
|
|
||||||
} catch {
|
|
||||||
return <span>无效时间</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'operation',
|
|
||||||
title: '操作',
|
|
||||||
width: 150,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Space>
|
|
||||||
<Button size="small" variant="outline" onClick={() => handleEdit(row)}>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm content="确定要删除这条自动化吗?" onConfirm={() => handleDelete(row.id)}>
|
|
||||||
<Button size="small" variant="outline" theme="danger">
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
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) => (
|
|
||||||
<div key={triggerTest.id} style={{ display: 'flex', gap: 5 }}>
|
|
||||||
<Button
|
|
||||||
theme="default"
|
|
||||||
variant="text"
|
|
||||||
icon={<Delete1Icon strokeWidth={2.4} />}
|
|
||||||
onClick={() => handleDeleteTrigger(triggerTest.id)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={triggerTest.triggerEvent.value}
|
|
||||||
style={{ width: '200px' }}
|
|
||||||
options={triggerOptions}
|
|
||||||
placeholder="请选择触发规则"
|
|
||||||
onChange={(value) => handleTriggerChange(triggerTest.id, value as string)}
|
|
||||||
/>
|
|
||||||
{triggerTest.haveValue === true ? (
|
|
||||||
<Input
|
|
||||||
placeholder="请输入Value"
|
|
||||||
style={{ width: '150px' }}
|
|
||||||
value={String(triggerTest.value)}
|
|
||||||
onChange={(v) =>
|
|
||||||
handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '24px' }}>
|
|
||||||
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}>自动化加分管理</h2>
|
|
||||||
|
|
||||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
|
||||||
<Form form={form} labelWidth={100} onReset={handleResetForm}>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
|
||||||
<Form.FormItem
|
|
||||||
label="自动化名称"
|
|
||||||
name="name"
|
|
||||||
rules={[{ required: true, message: '请输入自动化名称' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:每日签到加分" />
|
|
||||||
</Form.FormItem>
|
|
||||||
|
|
||||||
<Form.FormItem>
|
|
||||||
<Space>
|
|
||||||
<Form.FormItem
|
|
||||||
label="间隔时间"
|
|
||||||
name="intervalMinutes"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入间隔时间' },
|
|
||||||
{ min: 1, message: '间隔时间至少为1分钟' }
|
|
||||||
]}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<InputNumber min={1} placeholder="例如:1(每隔1分钟/天执行一次)" />
|
|
||||||
</Form.FormItem>
|
|
||||||
<Form.FormItem name="timeUnit" initialData="minutes" style={{ marginBottom: 0 }}>
|
|
||||||
<Radio.Group variant="default-filled">
|
|
||||||
<Radio.Button value="days">天</Radio.Button>
|
|
||||||
<Radio.Button value="minutes">分钟</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.FormItem>
|
|
||||||
</Space>
|
|
||||||
</Form.FormItem>
|
|
||||||
|
|
||||||
<Form.FormItem
|
|
||||||
label="加分值"
|
|
||||||
name="scoreValue"
|
|
||||||
rules={[{ required: true, message: '请输入加分值' }]}
|
|
||||||
>
|
|
||||||
<InputNumber placeholder="例如:1(每次加1分)" />
|
|
||||||
</Form.FormItem>
|
|
||||||
|
|
||||||
<Form.FormItem label="适用学生" name="studentNames">
|
|
||||||
<Select
|
|
||||||
filterable
|
|
||||||
multiple
|
|
||||||
placeholder="请选择或搜索学生(留空表示所有学生)"
|
|
||||||
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
|
||||||
/>
|
|
||||||
</Form.FormItem>
|
|
||||||
|
|
||||||
<Form.FormItem label="加分理由" name="reason">
|
|
||||||
<Input placeholder="例如:每日签到奖励" />
|
|
||||||
</Form.FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
|
||||||
<Button theme="primary" onClick={handleSubmit}>
|
|
||||||
{editingRuleId !== null ? '更新自动化' : '添加自动化'}
|
|
||||||
</Button>
|
|
||||||
<Button type="reset" variant="outline">
|
|
||||||
{editingRuleId !== null ? '取消编辑' : '重置表单'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
|
||||||
<Table
|
|
||||||
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
|
||||||
resizable
|
|
||||||
loading={loading}
|
|
||||||
dragSort="row-handler"
|
|
||||||
onDragSort={onDragSort}
|
|
||||||
pagination={{
|
|
||||||
current: currentPage,
|
|
||||||
pageSize,
|
|
||||||
total: rules.length,
|
|
||||||
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
|
||||||
onPageSizeChange: (size) => setPageSize(size)
|
|
||||||
}}
|
|
||||||
style={{ color: 'var(--ss-text-main)' }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
|
||||||
title="当以下事件触发时"
|
|
||||||
headerBordered
|
|
||||||
>
|
|
||||||
<Space style={{ display: 'grid' }}>
|
|
||||||
{triggerItems}
|
|
||||||
<Button
|
|
||||||
theme="default"
|
|
||||||
variant="text"
|
|
||||||
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
|
||||||
icon={<AddIcon strokeWidth={3} />}
|
|
||||||
onClick={handleAddTrigger}
|
|
||||||
>
|
|
||||||
添加触发器
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
|
||||||
<SyntaxHighlighter language="javascript" style={prism} showLineNumbers>
|
|
||||||
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</Card>
|
|
||||||
{/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}>
|
|
||||||
<h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3>
|
|
||||||
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
|
|
||||||
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
|
|
||||||
<li>间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次</li>
|
|
||||||
<li>如果"适用学生"字段为空,则自动化适用于所有学生</li>
|
|
||||||
<li>可以随时启用/禁用自动化,不会影响已保存的自动化配置</li>
|
|
||||||
</ul>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,26 @@ interface AutoScoreRule {
|
|||||||
id: number
|
id: number
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
name: string
|
name: string
|
||||||
intervalMinutes: number // 间隔分钟数
|
studentNames: string[]
|
||||||
studentNames: string[] // 学生姓名列表,空数组代表所有学生
|
lastExecuted?: Date
|
||||||
scoreValue: number // 每次加分值
|
triggers?: { event: string; value?: string }[]
|
||||||
reason: string // 加分理由
|
actions?: { event: string; value?: string; reason?: string }[]
|
||||||
lastExecuted?: Date // 最后执行时间
|
}
|
||||||
|
|
||||||
|
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' {
|
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 {
|
export class AutoScoreService extends Service {
|
||||||
private rules: AutoScoreRule[] = []
|
private rules: AutoScoreRule[] = []
|
||||||
private timers: Map<number, NodeJS.Timeout> = new Map()
|
private timers: Map<number, NodeJS.Timeout> = new Map()
|
||||||
@@ -27,8 +44,6 @@ export class AutoScoreService extends Service {
|
|||||||
constructor(ctx: MainContext) {
|
constructor(ctx: MainContext) {
|
||||||
super(ctx, 'autoScore')
|
super(ctx, 'autoScore')
|
||||||
this.registerIpc()
|
this.registerIpc()
|
||||||
this.loadRulesFromSettings()
|
|
||||||
this.startRules()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get mainCtx() {
|
private get mainCtx() {
|
||||||
@@ -102,9 +117,82 @@ export class AutoScoreService extends Service {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async loadRulesFromFile(): Promise<void> {
|
||||||
|
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<AutoScoreRulesFile>(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() {
|
private async loadRulesFromSettings() {
|
||||||
try {
|
try {
|
||||||
// 从设置中加载自动化规则
|
|
||||||
const settings = await this.mainCtx.settings.getAllRaw()
|
const settings = await this.mainCtx.settings.getAllRaw()
|
||||||
const autoScoreRulesStr = settings['auto_score_rules'] || '[]'
|
const autoScoreRulesStr = settings['auto_score_rules'] || '[]'
|
||||||
const rulesFromSettings = JSON.parse(autoScoreRulesStr)
|
const rulesFromSettings = JSON.parse(autoScoreRulesStr)
|
||||||
@@ -114,11 +202,40 @@ export class AutoScoreService extends Service {
|
|||||||
lastExecuted: rule.lastExecuted ? new Date(rule.lastExecuted) : undefined
|
lastExecuted: rule.lastExecuted ? new Date(rule.lastExecuted) : undefined
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} 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 = []
|
this.rules = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async saveRulesToFile(): Promise<void> {
|
||||||
|
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() {
|
private async saveRulesToSettings() {
|
||||||
try {
|
try {
|
||||||
const rulesToSave = this.rules.map(({ lastExecuted, ...rule }) => ({
|
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))
|
await this.mainCtx.settings.setRaw('auto_score_rules', JSON.stringify(rulesToSave))
|
||||||
} catch (error) {
|
} 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) {
|
private startRuleTimer(rule: AutoScoreRule) {
|
||||||
// 清除现有的定时器
|
|
||||||
if (this.timers.has(rule.id)) {
|
if (this.timers.has(rule.id)) {
|
||||||
clearTimeout(this.timers.get(rule.id)!)
|
clearTimeout(this.timers.get(rule.id)!)
|
||||||
this.timers.delete(rule.id)
|
this.timers.delete(rule.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算下次执行时间
|
// 从triggers中读取间隔时间
|
||||||
const now = new Date()
|
const intervalTrigger = rule.triggers?.find(t => t.event === 'interval_time_passed')
|
||||||
const intervalMs = rule.intervalMinutes * 60 * 1000
|
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
|
let delayMs = intervalMs
|
||||||
if (rule.lastExecuted) {
|
if (rule.lastExecuted) {
|
||||||
const timeSinceLastExecution = now.getTime() - rule.lastExecuted.getTime()
|
const timeSinceLastExecution = now.getTime() - rule.lastExecuted.getTime()
|
||||||
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
|
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
|
||||||
// 如果已经超过了间隔时间,立即执行
|
|
||||||
if (timeSinceLastExecution >= intervalMs) {
|
if (timeSinceLastExecution >= intervalMs) {
|
||||||
delayMs = 0
|
delayMs = 0
|
||||||
}
|
}
|
||||||
@@ -169,7 +291,6 @@ export class AutoScoreService extends Service {
|
|||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
this.executeRule(rule)
|
this.executeRule(rule)
|
||||||
// 设置重复执行
|
|
||||||
this.setRuleInterval(rule)
|
this.setRuleInterval(rule)
|
||||||
}, delayMs)
|
}, delayMs)
|
||||||
|
|
||||||
@@ -177,7 +298,15 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setRuleInterval(rule: AutoScoreRule) {
|
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(() => {
|
const timer = setInterval(() => {
|
||||||
this.executeRule(rule)
|
this.executeRule(rule)
|
||||||
}, intervalMs)
|
}, intervalMs)
|
||||||
@@ -187,18 +316,16 @@ export class AutoScoreService extends Service {
|
|||||||
|
|
||||||
private async executeRule(rule: AutoScoreRule) {
|
private async executeRule(rule: AutoScoreRule) {
|
||||||
try {
|
try {
|
||||||
console.log(`Executing auto score rule: ${rule.name}`)
|
this.logger.info(`Executing auto score rule: ${rule.name}`)
|
||||||
|
|
||||||
const studentRepo = this.mainCtx.students
|
const studentRepo = this.mainCtx.students
|
||||||
const eventRepo = this.mainCtx.events
|
const eventRepo = this.mainCtx.events
|
||||||
|
|
||||||
let studentsToScore: student[] = []
|
let studentsToScore: student[] = []
|
||||||
if (rule.studentNames.length === 0) {
|
if (rule.studentNames.length === 0) {
|
||||||
// 如果没有指定学生,对所有学生加分
|
|
||||||
const allStudents = await studentRepo.findAll()
|
const allStudents = await studentRepo.findAll()
|
||||||
studentsToScore = allStudents
|
studentsToScore = allStudents
|
||||||
} else {
|
} else {
|
||||||
// 否则只对指定的学生加分
|
|
||||||
const allStudents = await studentRepo.findAll()
|
const allStudents = await studentRepo.findAll()
|
||||||
for (const name of rule.studentNames) {
|
for (const name of rule.studentNames) {
|
||||||
const student = allStudents.find((s) => s.name === name)
|
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) {
|
for (const student of studentsToScore) {
|
||||||
await eventRepo.create({
|
await eventRepo.create({
|
||||||
student_name: student.name,
|
student_name: student.name,
|
||||||
reason_content: rule.reason || `自动化加分 - ${rule.name}`,
|
reason_content: reason,
|
||||||
delta: rule.scoreValue
|
delta: scoreValue
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新规则的最后执行时间
|
|
||||||
rule.lastExecuted = new Date()
|
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) {
|
} 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() {
|
private stopRules() {
|
||||||
for (const [_, timer] of this.timers) {
|
for (const [timer] of this.timers) {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
}
|
}
|
||||||
@@ -244,7 +376,7 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.rules.push(newRule)
|
this.rules.push(newRule)
|
||||||
await this.saveRulesToSettings()
|
await this.saveRulesToFile()
|
||||||
|
|
||||||
if (rule.enabled) {
|
if (rule.enabled) {
|
||||||
this.startRuleTimer(newRule)
|
this.startRuleTimer(newRule)
|
||||||
@@ -257,21 +389,17 @@ export class AutoScoreService extends Service {
|
|||||||
const index = this.rules.findIndex((r) => r.id === rule.id)
|
const index = this.rules.findIndex((r) => r.id === rule.id)
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|
||||||
// 停止旧的定时器
|
|
||||||
if (this.timers.has(rule.id)) {
|
if (this.timers.has(rule.id)) {
|
||||||
clearTimeout(this.timers.get(rule.id)!)
|
clearTimeout(this.timers.get(rule.id)!)
|
||||||
clearInterval(this.timers.get(rule.id)!)
|
clearInterval(this.timers.get(rule.id)!)
|
||||||
this.timers.delete(rule.id)
|
this.timers.delete(rule.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新规则
|
|
||||||
const updatedRule = { ...this.rules[index], ...rule }
|
const updatedRule = { ...this.rules[index], ...rule }
|
||||||
this.rules[index] = updatedRule
|
this.rules[index] = updatedRule
|
||||||
|
|
||||||
// 保存到设置
|
await this.saveRulesToFile()
|
||||||
await this.saveRulesToSettings()
|
|
||||||
|
|
||||||
// 如果规则已启用,启动新的定时器
|
|
||||||
if (updatedRule.enabled) {
|
if (updatedRule.enabled) {
|
||||||
this.startRuleTimer(updatedRule)
|
this.startRuleTimer(updatedRule)
|
||||||
}
|
}
|
||||||
@@ -283,16 +411,14 @@ export class AutoScoreService extends Service {
|
|||||||
const index = this.rules.findIndex((r) => r.id === ruleId)
|
const index = this.rules.findIndex((r) => r.id === ruleId)
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|
||||||
// 停止定时器
|
|
||||||
if (this.timers.has(ruleId)) {
|
if (this.timers.has(ruleId)) {
|
||||||
clearTimeout(this.timers.get(ruleId)!)
|
clearTimeout(this.timers.get(ruleId)!)
|
||||||
clearInterval(this.timers.get(ruleId)!)
|
clearInterval(this.timers.get(ruleId)!)
|
||||||
this.timers.delete(ruleId)
|
this.timers.delete(ruleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从数组中移除规则
|
|
||||||
this.rules.splice(index, 1)
|
this.rules.splice(index, 1)
|
||||||
await this.saveRulesToSettings()
|
await this.saveRulesToFile()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -303,19 +429,17 @@ export class AutoScoreService extends Service {
|
|||||||
|
|
||||||
rule.enabled = enabled
|
rule.enabled = enabled
|
||||||
|
|
||||||
// 停止现有定时器
|
|
||||||
if (this.timers.has(ruleId)) {
|
if (this.timers.has(ruleId)) {
|
||||||
clearTimeout(this.timers.get(ruleId)!)
|
clearTimeout(this.timers.get(ruleId)!)
|
||||||
clearInterval(this.timers.get(ruleId)!)
|
clearInterval(this.timers.get(ruleId)!)
|
||||||
this.timers.delete(ruleId)
|
this.timers.delete(ruleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果规则现在是启用的,启动定时器
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.startRuleTimer(rule)
|
this.startRuleTimer(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveRulesToSettings()
|
await this.saveRulesToFile()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,13 +453,12 @@ export class AutoScoreService extends Service {
|
|||||||
|
|
||||||
async restart() {
|
async restart() {
|
||||||
this.stopRules()
|
this.stopRules()
|
||||||
await this.loadRulesFromSettings()
|
await this.loadRulesFromFile()
|
||||||
this.startRules()
|
this.startRules()
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
// 确保服务正确初始化
|
await this.loadRulesFromFile()
|
||||||
await this.loadRulesFromSettings()
|
|
||||||
this.startRules()
|
this.startRules()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-28
@@ -140,37 +140,39 @@ function MainContent(): React.JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div
|
{import.meta.env.DEV ? (
|
||||||
style={{
|
<div
|
||||||
position: 'fixed',
|
|
||||||
display: 'flex',
|
|
||||||
bottom: '2px',
|
|
||||||
left: '20px',
|
|
||||||
opacity: 0.6,
|
|
||||||
zIndex: 9999
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
style={{
|
||||||
color: '#de2611',
|
position: 'fixed',
|
||||||
fontWeight: 'bold',
|
display: 'flex',
|
||||||
fontSize: '13px',
|
bottom: '2px',
|
||||||
pointerEvents: 'none'
|
left: '20px',
|
||||||
|
opacity: 0.6,
|
||||||
|
zIndex: 9999
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
开发中画面,不代表最终品质
|
<p
|
||||||
</p>
|
style={{
|
||||||
<p
|
color: '#de2611',
|
||||||
style={{
|
fontWeight: 'bold',
|
||||||
color: '#44474b',
|
fontSize: '13px',
|
||||||
fontWeight: 'bold',
|
pointerEvents: 'none'
|
||||||
fontSize: '13px',
|
}}
|
||||||
paddingLeft: '5px'
|
>
|
||||||
}}
|
开发中画面,不代表最终品质
|
||||||
>
|
</p>
|
||||||
SecScore Dev ({getPlatform()}-{getArchitecture()})
|
<p
|
||||||
</p>
|
style={{
|
||||||
</div>
|
color: '#44474b',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '13px',
|
||||||
|
paddingLeft: '5px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
SecScore Dev ({getPlatform()}-{getArchitecture()})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,43 +2,36 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import { AddIcon, Delete1Icon, MoveIcon } from 'tdesign-icons-react'
|
import { AddIcon, Delete1Icon, MoveIcon } from 'tdesign-icons-react'
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
|
import { allTriggers, allActions, TriggerItem, ActionItem } from '../services/AutoScoreService'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
|
||||||
Button,
|
Button,
|
||||||
MessagePlugin,
|
MessagePlugin,
|
||||||
Table,
|
Table,
|
||||||
PrimaryTableCol,
|
PrimaryTableCol,
|
||||||
Tag,
|
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Radio,
|
Radio,
|
||||||
Select,
|
Select,
|
||||||
TooltipLite,
|
TooltipLite
|
||||||
DatePicker
|
|
||||||
} from 'tdesign-react'
|
} from 'tdesign-react'
|
||||||
|
|
||||||
interface AutoScoreRule {
|
interface AutoScoreRule {
|
||||||
id: number
|
id: number
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
name: string
|
name: string
|
||||||
intervalMinutes: number
|
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
scoreValue: number
|
|
||||||
reason: string
|
|
||||||
lastExecuted?: string
|
lastExecuted?: string
|
||||||
triggers?: { event: string; value?: string }[]
|
triggers?: { event: string; value?: string }[]
|
||||||
|
actions?: { event: string; value?: string; reason?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutoScoreRuleFormValues {
|
interface AutoScoreRuleFormValues {
|
||||||
name: string
|
name: string
|
||||||
intervalMinutes: number
|
|
||||||
studentNames: string
|
studentNames: string
|
||||||
scoreValue: number
|
|
||||||
reason: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AutoScoreManager: React.FC = () => {
|
export const AutoScoreManager: React.FC = () => {
|
||||||
@@ -55,7 +48,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
// 权限检查:确保当前为 admin
|
|
||||||
try {
|
try {
|
||||||
const authRes = await (window as any).api.authGetStatus()
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
@@ -64,7 +56,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果权限检查失败,继续让后端返回更明确的错误
|
|
||||||
console.warn('Auth check failed', e)
|
console.warn('Auth check failed', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,45 +86,61 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!(window as any).api) return
|
if (!(window as any).api) return
|
||||||
|
|
||||||
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & {
|
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues
|
||||||
timeUnit: string
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
|
if (!values.name) {
|
||||||
MessagePlugin.warning('请填写完整信息')
|
MessagePlugin.warning('请填写自动化名称')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据单位转换间隔时间
|
// 验证触发器必填项
|
||||||
const intervalMinutes =
|
if (triggerList.length === 0) {
|
||||||
values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes
|
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 是数组类型
|
// 确保 studentNames 是数组类型
|
||||||
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
||||||
|
|
||||||
const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value }))
|
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 = {
|
const ruleData = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
intervalMinutes,
|
|
||||||
studentNames,
|
studentNames,
|
||||||
scoreValue: values.scoreValue,
|
triggers: triggersPayload,
|
||||||
reason: values.reason || `自动化加分 - ${values.name}`,
|
actions: actionsPayload
|
||||||
triggers: triggersPayload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权限检查:仅管理员可创建/更新自动化
|
// 权限检查:仅管理员可创建/更新自动化
|
||||||
try {
|
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()
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
MessagePlugin.error('需要管理员权限以创建或更新自动加分自动化')
|
MessagePlugin.error('需要管理员权限以创建或更新自动加分自动化')
|
||||||
@@ -161,14 +168,11 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: '',
|
name: '',
|
||||||
intervalMinutes: undefined,
|
studentNames: ''
|
||||||
studentNames: '',
|
|
||||||
scoreValue: undefined,
|
|
||||||
reason: '',
|
|
||||||
timeUnit: 'minutes'
|
|
||||||
})
|
})
|
||||||
setEditingRuleId(null)
|
setEditingRuleId(null)
|
||||||
setTriggerList([])
|
setTriggerList([])
|
||||||
|
setActionList([])
|
||||||
fetchRules() // 刷新自动化列表
|
fetchRules() // 刷新自动化列表
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(
|
MessagePlugin.error(
|
||||||
@@ -184,10 +188,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
setEditingRuleId(rule.id)
|
setEditingRuleId(rule.id)
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: rule.name,
|
name: rule.name,
|
||||||
intervalMinutes: rule.intervalMinutes,
|
studentNames: rule.studentNames.join(', ')
|
||||||
studentNames: rule.studentNames.join(', '),
|
|
||||||
scoreValue: rule.scoreValue,
|
|
||||||
reason: rule.reason
|
|
||||||
})
|
})
|
||||||
// 如果后端返回了 triggers 字段,把它加载到 triggerList
|
// 如果后端返回了 triggers 字段,把它加载到 triggerList
|
||||||
if (rule.triggers && Array.isArray(rule.triggers)) {
|
if (rule.triggers && Array.isArray(rule.triggers)) {
|
||||||
@@ -204,6 +205,22 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
setTriggerList([])
|
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) => {
|
const handleDelete = async (ruleId: number) => {
|
||||||
@@ -264,12 +281,11 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: '',
|
name: '',
|
||||||
intervalMinutes: undefined,
|
studentNames: ''
|
||||||
studentNames: '',
|
|
||||||
scoreValue: undefined,
|
|
||||||
reason: ''
|
|
||||||
})
|
})
|
||||||
setEditingRuleId(null)
|
setEditingRuleId(null)
|
||||||
|
setTriggerList([])
|
||||||
|
setActionList([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: PrimaryTableCol<AutoScoreRule>[] = [
|
const columns: PrimaryTableCol<AutoScoreRule>[] = [
|
||||||
@@ -293,32 +309,49 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{ colKey: 'name', title: '自动化名称', width: 150 },
|
{ colKey: 'name', title: '自动化名称', width: 150 },
|
||||||
{
|
{
|
||||||
colKey: 'intervalMinutes',
|
colKey: 'triggers',
|
||||||
title: '间隔',
|
title: '触发器',
|
||||||
width: 100,
|
width: 150,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const isDays = row.intervalMinutes >= 1440
|
if (!row.triggers || row.triggers.length === 0) {
|
||||||
const value = isDays ? row.intervalMinutes / 1440 : row.intervalMinutes
|
return <span>无</span>
|
||||||
const unit = isDays ? '天' : '分钟'
|
}
|
||||||
return `${value} ${unit}`
|
const triggerLabels = row.triggers.map((t) => {
|
||||||
|
const def = allTriggers.find((tr) => tr.eventName === t.event)
|
||||||
|
return def?.label || t.event
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<TooltipLite content={triggerLabels.join(', ')} showArrow placement="mouse" theme="default">
|
||||||
|
{row.triggers.length} 个触发器
|
||||||
|
</TooltipLite>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
colKey: 'scoreValue',
|
colKey: 'actions',
|
||||||
title: '分值',
|
title: '行动',
|
||||||
width: 80,
|
width: 150,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Tag theme={row.scoreValue > 0 ? 'success' : 'danger'} variant="light">
|
if (!row.actions || row.actions.length === 0) {
|
||||||
{row.scoreValue > 0 ? `+${row.scoreValue}` : row.scoreValue}
|
return <span>无</span>
|
||||||
</Tag>
|
}
|
||||||
)
|
const actionLabels = row.actions.map((a) => {
|
||||||
|
const def = allActions.find((ac) => ac.eventName === a.event)
|
||||||
|
return def?.label || a.event
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<TooltipLite content={actionLabels.join(', ')} showArrow placement="mouse" theme="default">
|
||||||
|
{row.actions.length} 个行动
|
||||||
|
</TooltipLite>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
colKey: 'studentNames',
|
colKey: 'studentNames',
|
||||||
title: '适用学生',
|
title: '适用学生',
|
||||||
width: 130,
|
width: 130,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
if (row.studentNames.length === 0) {
|
if (!row.studentNames || row.studentNames.length === 0) {
|
||||||
return <span>所有学生</span>
|
return <span>所有学生</span>
|
||||||
}
|
}
|
||||||
const studentList = row.studentNames.join(',\n')
|
const studentList = row.studentNames.join(',\n')
|
||||||
@@ -329,7 +362,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ colKey: 'reason', title: '理由', width: 130, ellipsis: true },
|
|
||||||
{
|
{
|
||||||
colKey: 'lastExecuted',
|
colKey: 'lastExecuted',
|
||||||
title: '最后执行',
|
title: '最后执行',
|
||||||
@@ -364,47 +396,10 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
]
|
]
|
||||||
const onDragSort = (params: any) => setRules(params.newData)
|
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 }))
|
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<TriggerItem[]>([])
|
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
|
||||||
|
const [actionList, setActionList] = useState<ActionItem[]>([])
|
||||||
|
|
||||||
const handleTriggerChange = (id: number, value: string) => {
|
const handleTriggerChange = (id: number, value: string) => {
|
||||||
const found = allTriggers.find((a) => a.eventName === value)
|
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
|
const triggerItems = triggerList
|
||||||
.filter((t) => t.eventName !== null)
|
.filter((t) => t.eventName !== null)
|
||||||
.map((triggerTest) => (
|
.map((triggerTest) => (
|
||||||
@@ -454,25 +481,87 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
placeholder="请选择触发规则"
|
placeholder="请选择触发规则"
|
||||||
onChange={(value) => handleTriggerChange(triggerTest.id, value as string)}
|
onChange={(value) => handleTriggerChange(triggerTest.id, value as string)}
|
||||||
/>
|
/>
|
||||||
{triggerTest.valueType ? (
|
{triggerTest.valueType
|
||||||
triggerTest.eventName === 'interval_time_passed' ? (
|
? React.createElement(triggerTest.valueType, {
|
||||||
<DatePicker
|
placeholder:
|
||||||
placeholder="请选择日期"
|
triggerTest.eventName === 'interval_time_passed' ? '请选择日期' : '请输入时间间隔(天)',
|
||||||
style={{ width: '150px' }}
|
style: { width: '150px' },
|
||||||
value={triggerTest.value ? new Date(triggerTest.value) : undefined}
|
value:
|
||||||
onChange={(v) => handleValueChange(triggerTest.id, v ? String(v) : '')}
|
triggerTest.eventName === 'interval_time_passed'
|
||||||
/>
|
? triggerTest.value
|
||||||
) : (
|
? new Date(triggerTest.value)
|
||||||
<Input
|
: undefined
|
||||||
placeholder="请输入Value"
|
: String(triggerTest.value ?? ''),
|
||||||
style={{ width: '150px' }}
|
onChange: (v: any) => handleValueChange(triggerTest.id, v ? String(v) : '')
|
||||||
value={String(triggerTest.value ?? '')}
|
})
|
||||||
onChange={(v) =>
|
: null}
|
||||||
handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))
|
</div>
|
||||||
}
|
))
|
||||||
/>
|
|
||||||
)
|
const actionOptions = allActions.map((a) => ({ label: a.label, value: a.eventName }))
|
||||||
) : null}
|
|
||||||
|
const actionItems = actionList
|
||||||
|
.filter((a) => a.eventName !== null)
|
||||||
|
.map((action) => (
|
||||||
|
<div key={action.id} style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||||
|
<Button
|
||||||
|
theme="default"
|
||||||
|
variant="text"
|
||||||
|
icon={<Delete1Icon strokeWidth={2.4} />}
|
||||||
|
onClick={() => handleDeleteAction(action.id)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={action.eventName}
|
||||||
|
style={{ width: '200px' }}
|
||||||
|
options={actionOptions}
|
||||||
|
placeholder="请选择触发行动"
|
||||||
|
onChange={(value) => handleActionChange(action.id, value as string)}
|
||||||
|
/>
|
||||||
|
{(() => {
|
||||||
|
const actionDef = allActions.find(a => a.eventName === action.eventName);
|
||||||
|
const renderConfig = actionDef?.renderConfig;
|
||||||
|
|
||||||
|
// 特殊处理Radio组件
|
||||||
|
if (action.valueType === Radio || renderConfig?.component === Radio) {
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
value={action.value || 'email'}
|
||||||
|
onChange={(v: any) => handleActionValueChange(action.id, v ? String(v) : '')}
|
||||||
|
{...renderConfig?.props}
|
||||||
|
>
|
||||||
|
{renderConfig?.props?.options?.map((option: any) => (
|
||||||
|
<Radio.Button key={option.value} value={option.value}>{option.label}</Radio.Button>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
} else if (renderConfig?.component) {
|
||||||
|
return React.createElement(renderConfig.component, {
|
||||||
|
value: String(action.value ?? ''),
|
||||||
|
onChange: (v: any) => handleActionValueChange(action.id, v ? String(v) : ''),
|
||||||
|
...renderConfig.props
|
||||||
|
});
|
||||||
|
} else if (action.valueType) {
|
||||||
|
return React.createElement(action.valueType, {
|
||||||
|
placeholder: '请输入Value',
|
||||||
|
style: { width: '150px' },
|
||||||
|
value: String(action.value ?? ''),
|
||||||
|
onChange: (v: any) => handleActionValueChange(action.id, v ? String(v) : '')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
{action.eventName === 'add_score' && (
|
||||||
|
<Input
|
||||||
|
placeholder="请输入理由"
|
||||||
|
style={{ width: '150px' }}
|
||||||
|
value={action.reason || ''}
|
||||||
|
onChange={(v: any) => {
|
||||||
|
setActionList((prev) =>
|
||||||
|
prev.map((a) => (a.id === action.id ? { ...a, reason: v } : a))
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -491,36 +580,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
<Input placeholder="例如:每日签到加分" />
|
<Input placeholder="例如:每日签到加分" />
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
|
|
||||||
<Form.FormItem>
|
|
||||||
<Space>
|
|
||||||
<Form.FormItem
|
|
||||||
label="间隔时间"
|
|
||||||
name="intervalMinutes"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入间隔时间' },
|
|
||||||
{ min: 1, message: '间隔时间至少为1分钟/天' }
|
|
||||||
]}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<InputNumber min={1} placeholder="例如:1(每隔1分钟/天执行一次)" />
|
|
||||||
</Form.FormItem>
|
|
||||||
<Form.FormItem name="timeUnit" initialData="minutes" style={{ marginBottom: 0 }}>
|
|
||||||
<Radio.Group variant="default-filled">
|
|
||||||
<Radio.Button value="days">天</Radio.Button>
|
|
||||||
<Radio.Button value="minutes">分钟</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.FormItem>
|
|
||||||
</Space>
|
|
||||||
</Form.FormItem>
|
|
||||||
|
|
||||||
<Form.FormItem
|
|
||||||
label="加分值"
|
|
||||||
name="scoreValue"
|
|
||||||
rules={[{ required: true, message: '请输入加分值' }]}
|
|
||||||
>
|
|
||||||
<InputNumber placeholder="例如:1(每次加1分)" />
|
|
||||||
</Form.FormItem>
|
|
||||||
|
|
||||||
<Form.FormItem label="适用学生" name="studentNames">
|
<Form.FormItem label="适用学生" name="studentNames">
|
||||||
<Select
|
<Select
|
||||||
filterable
|
filterable
|
||||||
@@ -529,10 +588,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
||||||
/>
|
/>
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
|
|
||||||
<Form.FormItem label="加分理由" name="reason">
|
|
||||||
<Input placeholder="例如:每日签到奖励" />
|
|
||||||
</Form.FormItem>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
||||||
@@ -567,7 +622,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
|
|
||||||
<Card
|
<Card
|
||||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||||
title="当以下事件触发时"
|
title="当以下规则触发时"
|
||||||
headerBordered
|
headerBordered
|
||||||
>
|
>
|
||||||
<Space style={{ display: 'grid' }}>
|
<Space style={{ display: 'grid' }}>
|
||||||
@@ -579,7 +634,26 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
icon={<AddIcon strokeWidth={3} />}
|
icon={<AddIcon strokeWidth={3} />}
|
||||||
onClick={handleAddTrigger}
|
onClick={handleAddTrigger}
|
||||||
>
|
>
|
||||||
添加触发器
|
添加规则
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||||
|
title="满足规则时触发的行动"
|
||||||
|
headerBordered
|
||||||
|
>
|
||||||
|
<Space style={{ display: 'grid' }}>
|
||||||
|
{actionItems}
|
||||||
|
<Button
|
||||||
|
theme="default"
|
||||||
|
variant="text"
|
||||||
|
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
||||||
|
icon={<AddIcon strokeWidth={3} />}
|
||||||
|
onClick={handleAddAction}
|
||||||
|
>
|
||||||
|
添加行动
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
setHttpLoading(false)
|
setHttpLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
|
<div style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
|
||||||
<div
|
<div
|
||||||
@@ -799,6 +799,8 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr', rowGap: '10px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr', rowGap: '10px' }}>
|
||||||
<div style={{ color: 'var(--ss-text-secondary)' }}>版本</div>
|
<div style={{ color: 'var(--ss-text-secondary)' }}>版本</div>
|
||||||
<div>v1.0.0</div>
|
<div>v1.0.0</div>
|
||||||
|
<div style={{ color: 'var(--ss-text-secondary)' }}>版权</div>
|
||||||
|
<div>{'SecScore遵循GPL3.0协议——' + 'CopyRight © 2025-' + currentYear + ' SECTL'}</div>
|
||||||
<div style={{ color: 'var(--ss-text-secondary)' }}>Electron</div>
|
<div style={{ color: 'var(--ss-text-secondary)' }}>Electron</div>
|
||||||
<div>{(window as any).electron?.process?.versions?.electron || '-'}</div>
|
<div>{(window as any).electron?.process?.versions?.electron || '-'}</div>
|
||||||
<div style={{ color: 'var(--ss-text-secondary)' }}>Chromium</div>
|
<div style={{ color: 'var(--ss-text-secondary)' }}>Chromium</div>
|
||||||
|
|||||||
@@ -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<string, any>
|
||||||
|
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<AutoScoreRule> & { 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -5,8 +5,10 @@
|
|||||||
"src/renderer/src/**/*",
|
"src/renderer/src/**/*",
|
||||||
"src/renderer/src/**/*.tsx",
|
"src/renderer/src/**/*.tsx",
|
||||||
"src/preload/*.d.ts",
|
"src/preload/*.d.ts",
|
||||||
"src/preload/types.ts"
|
"src/preload/types.ts",
|
||||||
, "src/main/services/HttpService.ts" ],
|
"src/shared/**/*",
|
||||||
|
"src/main/services/HttpService.ts"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user