重构AutoScoreManager 更方便添加actions和triggers

This commit is contained in:
NanGua-QWQ
2026-02-21 15:57:07 +08:00
parent db3f05c59d
commit 5837a2a6f2
23 changed files with 1063 additions and 413 deletions
+106 -37
View File
@@ -1,6 +1,7 @@
import { Service } from '../../shared/kernel'
import { MainContext } from '../context'
import { student } from '../repos/StudentRepository'
import { getTriggerLogic } from '../../shared/triggers'
interface AutoScoreRule {
id: number
@@ -129,7 +130,6 @@ export class AutoScoreService extends Service {
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,
@@ -138,7 +138,6 @@ export class AutoScoreService extends Service {
: undefined
}
})
// 如果有数据迁移,保存新格式
if (
data.rules.some(
(rule: any) => rule.intervalMinutes !== undefined || rule.scoreValue !== undefined
@@ -159,12 +158,10 @@ export class AutoScoreService extends Service {
}
private migrateRule(rule: any): AutoScoreRule {
// 如果已经是新格式,直接返回
if (!rule.intervalMinutes && !rule.scoreValue) {
return rule
}
// 迁移旧格式到新格式
const migratedRule: AutoScoreRule = {
id: rule.id,
enabled: rule.enabled,
@@ -175,7 +172,6 @@ export class AutoScoreService extends Service {
actions: rule.actions || []
}
// 将intervalMinutes迁移到triggers
if (
rule.intervalMinutes &&
!migratedRule.triggers?.find((t) => t.event === 'interval_time_passed')
@@ -187,7 +183,6 @@ export class AutoScoreService extends Service {
})
}
// 将scoreValue和reason迁移到actions
if (
rule.scoreValue !== undefined &&
!migratedRule.actions?.find((a) => a.event === 'add_score')
@@ -280,26 +275,29 @@ export class AutoScoreService extends Service {
this.timers.delete(rule.id)
}
// 从triggers中读取间隔时间
const intervalTrigger = rule.triggers?.find((t) => t.event === 'interval_time_passed')
const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0
const now = new Date()
let delayMs = 0
let primaryTrigger: { event: string; value?: string } | undefined
if (!intervalMinutes || intervalMinutes <= 0) {
this.logger.warn(`Rule ${rule.name} has no valid interval time, skipping timer`)
for (const trigger of rule.triggers || []) {
const logic = getTriggerLogic(trigger.event)
if (logic?.calculateNextTime) {
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
if (delayMs === 0 || result.delayMs < delayMs) {
delayMs = result.delayMs
primaryTrigger = trigger
}
}
}
if (!primaryTrigger) {
this.logger.warn(`Rule ${rule.name} has no valid triggers with timing logic, skipping`)
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) {
if (delayMs < 0) {
delayMs = 0
}
}
const timer = setTimeout(() => {
this.executeRule(rule)
@@ -307,21 +305,32 @@ export class AutoScoreService extends Service {
}, delayMs)
this.timers.set(rule.id, timer)
this.logger.info(`Rule ${rule.name} scheduled to execute in ${delayMs}ms`)
}
private setRuleInterval(rule: AutoScoreRule) {
// 从triggers中读取间隔时间
const intervalTrigger = rule.triggers?.find((t) => t.event === 'interval_time_passed')
const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0
const now = new Date()
let minDelayMs = Infinity
let primaryTrigger: { event: string; value?: string } | undefined
if (!intervalMinutes || intervalMinutes <= 0) {
for (const trigger of rule.triggers || []) {
const logic = getTriggerLogic(trigger.event)
if (logic?.calculateNextTime) {
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
if (result.delayMs < minDelayMs) {
minDelayMs = result.delayMs
primaryTrigger = trigger
}
}
}
if (!primaryTrigger || minDelayMs === Infinity) {
return
}
const intervalMs = intervalMinutes * 60 * 1000
const timer = setInterval(() => {
this.executeRule(rule)
}, intervalMs)
}, minDelayMs)
this.timers.set(rule.id, timer)
}
@@ -331,7 +340,6 @@ export class AutoScoreService extends Service {
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) {
@@ -347,17 +355,30 @@ 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 trigger of rule.triggers || []) {
const logic = getTriggerLogic(trigger.event)
if (logic?.check) {
const context = {
students: studentsToScore,
events: [],
rule: {
id: rule.id,
name: rule.name,
studentNames: rule.studentNames,
triggers: rule.triggers,
actions: rule.actions
},
now: new Date()
}
const result = logic.check(context, trigger.value || '')
if (result.matchedStudents && result.matchedStudents.length > 0) {
studentsToScore = result.matchedStudents
}
}
}
for (const student of studentsToScore) {
await eventRepo.create({
student_name: student.name,
reason_content: reason,
delta: scoreValue
})
for (const action of rule.actions || []) {
await this.executeAction(action, studentsToScore, rule.name)
}
rule.lastExecuted = new Date()
@@ -371,6 +392,54 @@ export class AutoScoreService extends Service {
}
}
private async executeAction(
action: { event: string; value?: string; reason?: string },
students: student[],
ruleName: string
) {
const eventRepo = this.mainCtx.events
switch (action.event) {
case 'add_score': {
const scoreValue = action.value ? parseInt(action.value, 10) : 0
const reason = action.reason || `自动化加分 - ${ruleName}`
for (const student of students) {
await eventRepo.create({
student_name: student.name,
reason_content: reason,
delta: scoreValue
})
}
break
}
case 'add_tag': {
const tagName = action.value
if (tagName) {
const studentRepo = this.mainCtx.students
for (const student of students) {
const currentTags = student.tags || []
if (!currentTags.includes(tagName)) {
await studentRepo.update(student.id, {
tags: [...currentTags, tagName]
})
}
}
}
break
}
case 'send_notification': {
this.logger.info(`Notification action: ${action.value}`)
break
}
case 'set_student_status': {
this.logger.info(`Set student status action: ${action.value} (not implemented - student type has no status field)`)
break
}
default:
this.logger.warn(`Unknown action event: ${action.event}`)
}
}
private stopRules() {
for (const [timer] of this.timers) {
clearTimeout(timer)
+114 -234
View File
@@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react'
import { AddIcon, Delete1Icon, MoveIcon } from 'tdesign-icons-react'
import { allTriggers, allActions, TriggerItem, ActionItem } from '../services/AutoScoreService'
import { AddIcon, MoveIcon } from 'tdesign-icons-react'
import { allTriggers, allActions, triggerRegistry, actionRegistry } from './com.automatically'
import type { TriggerItem, ActionItem } from './com.automatically/types'
import TriggerItemComponent from './com.automatically/TriggerItem'
import ActionItemComponent from './com.automatically/ActionItem'
import {
Card,
Form,
@@ -12,12 +15,11 @@ import {
Space,
Switch,
Popconfirm,
Radio,
Select,
TooltipLite
} from 'tdesign-react'
import Code from './Code'
import Code from './Code';
interface AutoScoreRule {
id: number
enabled: boolean
@@ -41,6 +43,8 @@ export const AutoScoreManager: React.FC = () => {
const [pageSize, setPageSize] = useState<number>(50)
const [form] = Form.useForm()
const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
const [actionList, setActionList] = useState<ActionItem[]>([])
const fetchRules = async () => {
if (!(window as any).api) return
@@ -92,35 +96,16 @@ export const AutoScoreManager: React.FC = () => {
return
}
// 验证触发器必填项
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 }))
@@ -138,7 +123,6 @@ export const AutoScoreManager: React.FC = () => {
actions: actionsPayload
}
// 权限检查:仅管理员可创建/更新自动化
try {
const authRes = await (window as any).api.authGetStatus()
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
@@ -152,19 +136,16 @@ export const AutoScoreManager: React.FC = () => {
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: '',
studentNames: ''
@@ -172,7 +153,7 @@ export const AutoScoreManager: React.FC = () => {
setEditingRuleId(null)
setTriggerList([])
setActionList([])
fetchRules() // 刷新自动化列表
fetchRules()
} else {
MessagePlugin.error(
res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
@@ -183,39 +164,30 @@ export const AutoScoreManager: React.FC = () => {
MessagePlugin.error(editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
}
}
const handleEdit = (rule: AutoScoreRule) => {
setEditingRuleId(rule.id)
form.setFieldsValue({
name: rule.name,
studentNames: rule.studentNames.join(', ')
})
// 如果后端返回了 triggers 字段,把它加载到 triggerList
if (rule.triggers && Array.isArray(rule.triggers)) {
const mapped = rule.triggers.map((t, idx) => {
const found = allTriggers.find((a) => a.eventName === t.event)
return {
const mapped = rule.triggers.map((t, idx) => ({
id: idx + 1,
eventName: t.event,
haveValue: !!found?.valueType,
value: t.value ?? ''
}
})
}))
setTriggerList(mapped)
} 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 {
const mapped = rule.actions.map((a, idx) => ({
id: idx + 1,
eventName: a.event,
valueType: found?.valueType,
value: a.value ?? '',
reason: a.reason
}
})
reason: a.reason ?? ''
}))
setActionList(mapped)
} else {
setActionList([])
@@ -224,7 +196,6 @@ export const AutoScoreManager: React.FC = () => {
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') {
@@ -239,7 +210,7 @@ export const AutoScoreManager: React.FC = () => {
const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId)
if (res.success) {
MessagePlugin.success('自动化删除成功')
fetchRules() // 刷新自动化列表
fetchRules()
} else {
MessagePlugin.error(res.message || '删除自动化失败')
}
@@ -251,7 +222,6 @@ export const AutoScoreManager: React.FC = () => {
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') {
@@ -266,7 +236,7 @@ export const AutoScoreManager: React.FC = () => {
const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled })
if (res.success) {
MessagePlugin.success(enabled ? '自动化已启用' : '自动化已禁用')
fetchRules() // 刷新自动化列表
fetchRules()
} else {
MessagePlugin.error(res.message || (enabled ? '启用自动化失败' : '禁用自动化失败'))
}
@@ -277,7 +247,6 @@ export const AutoScoreManager: React.FC = () => {
}
const handleResetForm = () => {
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
form.setFieldsValue({
name: '',
studentNames: ''
@@ -287,6 +256,73 @@ export const AutoScoreManager: React.FC = () => {
setActionList([])
}
const handleAddTrigger = () => {
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
const defaultTrigger = allTriggers.list[0]
if (!defaultTrigger) {
MessagePlugin.error('没有可用的触发器类型,请检查配置')
return
}
setTriggerList((prev) => [
...prev,
{
id: nextId,
eventName: defaultTrigger.eventName,
value: ''
}
])
}
const handleDeleteTrigger = (id: number) => {
setTriggerList((prev) => prev.filter((t) => t.id !== id))
}
const handleTriggerChange = (id: number, eventName: string) => {
setTriggerList((prev) =>
prev.map((t) => (t.id === id ? { ...t, eventName, value: '' } : t))
)
}
const handleTriggerValueChange = (id: number, value: string) => {
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value } : t)))
}
const handleAddAction = () => {
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
const defaultAction = allActions.list[0]
if (!defaultAction) {
MessagePlugin.error('没有可用的行动类型,请检查配置')
return
}
setActionList((prev) => [
...prev,
{
id: nextId,
eventName: defaultAction.eventName,
value: '',
reason: ''
}
])
}
const handleDeleteAction = (id: number) => {
setActionList((prev) => prev.filter((a) => a.id !== id))
}
const handleActionChange = (id: number, eventName: string) => {
setActionList((prev) =>
prev.map((a) => (a.id === id ? { ...a, eventName, value: '' } : a))
)
}
const handleActionValueChange = (id: number, value: string) => {
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a)))
}
const handleActionReasonChange = (id: number, reason: string) => {
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, reason } : a)))
}
const columns: PrimaryTableCol<AutoScoreRule>[] = [
{
colKey: 'drag',
@@ -316,7 +352,7 @@ export const AutoScoreManager: React.FC = () => {
return <span></span>
}
const triggerLabels = row.triggers.map((t) => {
const def = allTriggers.find((tr) => tr.eventName === t.event)
const def = triggerRegistry.get(t.event)
return def?.label || t.event
})
return (
@@ -340,7 +376,7 @@ export const AutoScoreManager: React.FC = () => {
return <span></span>
}
const actionLabels = row.actions.map((a) => {
const def = allActions.find((ac) => ac.eventName === a.event)
const def = actionRegistry.get(a.event)
return def?.label || a.event
})
return (
@@ -403,180 +439,34 @@ export const AutoScoreManager: React.FC = () => {
)
}
]
const onDragSort = (params: any) => setRules(params.newData)
const triggerOptions = allTriggers.map((t) => ({ label: t.label, value: t.eventName }))
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
const [actionList, setActionList] = useState<ActionItem[]>([])
const handleTriggerChange = (id: number, value: string) => {
const found = allTriggers.find((a) => a.eventName === value)
setTriggerList((prev) =>
prev.map((t) =>
t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : 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
const defaultTrigger = allTriggers[0]
setTriggerList((prev) => [
...prev,
{
id: nextId,
eventName: defaultTrigger.eventName,
valueType: defaultTrigger.valueType,
value: ''
}
])
}
// 行动管理相关函数
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) => (
<div key={triggerTest.id} style={{ display: 'flex', gap: 5 }}>
<Button
theme="default"
variant="text"
icon={<Delete1Icon strokeWidth={2.4} />}
onClick={() => handleDeleteTrigger(triggerTest.id)}
.map((item) => (
<TriggerItemComponent
key={item.id}
item={item}
onDelete={handleDeleteTrigger}
onChange={handleTriggerChange}
onValueChange={handleTriggerValueChange}
/>
<Select
value={triggerTest.eventName}
style={{ width: '200px' }}
options={triggerOptions}
placeholder="请选择触发规则"
onChange={(value) => handleTriggerChange(triggerTest.id, value as string)}
/>
{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}
</div>
))
const actionOptions = allActions.map((a) => ({ label: a.label, value: a.eventName }))
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)}
.map((item) => (
<ActionItemComponent
key={item.id}
item={item}
onDelete={handleDeleteAction}
onChange={handleActionChange}
onValueChange={handleActionValueChange}
onReasonChange={handleActionReasonChange}
/>
<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>
))
return (
<div style={{ padding: '24px' }}>
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}></h2>
@@ -670,18 +560,17 @@ export const AutoScoreManager: React.FC = () => {
</Space>
</Card>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Code code=
{(() => {
<Code
code={(() => {
if (editingRuleId !== null) {
// 显示当前编辑的规则
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues;
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [];
const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value }));
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues
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 currentRule = {
id: editingRuleId,
@@ -690,25 +579,16 @@ export const AutoScoreManager: React.FC = () => {
studentNames,
triggers: triggersPayload,
actions: actionsPayload
};
}
return JSON.stringify(currentRule, null, 2);
return JSON.stringify(currentRule, null, 2)
} else {
// 显示所有规则
return JSON.stringify(rules, null, 2);
return JSON.stringify(rules, null, 2)
}
})()}
language={'json'}/>
language={'json'}
/>
</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>
)
}
@@ -0,0 +1,52 @@
import React from 'react'
import { Button, Select } from 'tdesign-react'
import { Delete1Icon } from 'tdesign-icons-react'
import { actionRegistry, allActions } from './registry'
import type { ActionItem as ActionItemType } from './types'
interface ActionItemProps {
item: ActionItemType
onDelete: (id: number) => void
onChange: (id: number, eventName: string) => void
onValueChange: (id: number, value: string) => void
onReasonChange: (id: number, reason: string) => void
}
const ActionItem: React.FC<ActionItemProps> = ({
item,
onDelete,
onChange,
onValueChange,
onReasonChange
}) => {
const definition = actionRegistry.get(item.eventName)
const Component = definition?.component
return (
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
<Button
theme="default"
variant="text"
icon={<Delete1Icon strokeWidth={2.4} />}
onClick={() => onDelete(item.id)}
/>
<Select
value={item.eventName}
style={{ width: '200px' }}
options={allActions.options}
placeholder="请选择触发行动"
onChange={(value) => onChange(item.id, value as string)}
/>
{Component && (
<Component
value={item.value}
reason={item.reason}
onChange={(value) => onValueChange(item.id, value)}
onReasonChange={(reason) => onReasonChange(item.id, reason)}
/>
)}
</div>
)
}
export default ActionItem
@@ -0,0 +1,43 @@
import React from 'react'
import { Button, Select } from 'tdesign-react'
import { Delete1Icon } from 'tdesign-icons-react'
import { triggerRegistry, allTriggers } from './registry'
import type { TriggerItem as TriggerItemType } from './types'
interface TriggerItemProps {
item: TriggerItemType
onDelete: (id: number) => void
onChange: (id: number, eventName: string) => void
onValueChange: (id: number, value: string) => void
}
const TriggerItem: React.FC<TriggerItemProps> = ({ item, onDelete, onChange, onValueChange }) => {
const definition = triggerRegistry.get(item.eventName)
const Component = definition?.component
return (
<div style={{ display: 'flex', gap: 5 }}>
<Button
theme="default"
variant="text"
icon={<Delete1Icon strokeWidth={2.4} />}
onClick={() => onDelete(item.id)}
/>
<Select
value={item.eventName}
style={{ width: '200px' }}
options={allTriggers.options}
placeholder="请选择触发规则"
onChange={(value) => onChange(item.id, value as string)}
/>
{Component && (
<Component
value={item.value}
onChange={(value) => onValueChange(item.id, value)}
/>
)}
</div>
)
}
export default TriggerItem
@@ -0,0 +1,29 @@
import React from 'react'
import { Input } from 'tdesign-react'
import type { ActionComponentProps } from '../types'
export const eventName = 'add_score'
export const label = '添加分数'
export const description = '为学生添加分数'
export const hasReason = true
const AddScoreAction: React.FC<ActionComponentProps> = ({ value, reason, onChange, onReasonChange }) => {
return (
<>
<Input
placeholder="请输入分数"
style={{ width: '150px' }}
value={value ?? ''}
onChange={(v: any) => onChange(v ? String(v) : '')}
/>
<Input
placeholder="请输入理由"
style={{ width: '150px' }}
value={reason ?? ''}
onChange={(v: any) => onReasonChange?.(v ? String(v) : '')}
/>
</>
)
}
export default AddScoreAction
@@ -0,0 +1,21 @@
import React from 'react'
import { Input } from 'tdesign-react'
import type { ActionComponentProps } from '../types'
export const eventName = 'add_tag'
export const label = '添加标签'
export const description = '为学生添加标签'
export const hasReason = false
const AddTagAction: React.FC<ActionComponentProps> = ({ value, onChange }) => {
return (
<Input
placeholder="请输入标签"
style={{ width: '150px' }}
value={value ?? ''}
onChange={(v: any) => onChange(v ? String(v) : '')}
/>
)
}
export default AddTagAction
@@ -0,0 +1,21 @@
import React from 'react'
import { Input } from 'tdesign-react'
import type { ActionComponentProps } from '../types'
export const eventName = 'send_notification'
export const label = '发送通知'
export const description = '向学生发送通知'
export const hasReason = false
const SendNotificationAction: React.FC<ActionComponentProps> = ({ value, onChange }) => {
return (
<Input
placeholder="请输入通知内容"
style={{ width: '150px' }}
value={value ?? ''}
onChange={(v: any) => onChange(v ? String(v) : '')}
/>
)
}
export default SendNotificationAction
@@ -0,0 +1,28 @@
import React from 'react'
import { Select } from 'tdesign-react'
import type { ActionComponentProps } from '../types'
export const eventName = 'set_student_status'
export const label = '设置学生状态'
export const description = '设置学生的状态'
export const hasReason = false
const statusOptions = [
{ label: '活跃', value: 'active' },
{ label: '不活跃', value: 'inactive' },
{ label: '请假', value: 'leave' }
]
const SetStudentStatusAction: React.FC<ActionComponentProps> = ({ value, onChange }) => {
return (
<Select
placeholder="请选择状态"
style={{ width: '150px' }}
value={value ?? ''}
onChange={(v: any) => onChange(v ? String(v) : '')}
options={statusOptions}
/>
)
}
export default SetStudentStatusAction
@@ -0,0 +1,70 @@
import { actionRegistry } from '../registry'
import type { ActionDefinition } from '../types'
import AddScoreAction, {
eventName as addScoreEventName,
label as addScoreLabel,
description as addScoreDescription,
hasReason as addScoreHasReason
} from './AddScoreAction'
import AddTagAction, {
eventName as addTagEventName,
label as addTagLabel,
description as addTagDescription,
hasReason as addTagHasReason
} from './AddTagAction'
import SendNotificationAction, {
eventName as sendNotificationEventName,
label as sendNotificationLabel,
description as sendNotificationDescription,
hasReason as sendNotificationHasReason
} from './SendNotificationAction'
import SetStudentStatusAction, {
eventName as setStudentStatusEventName,
label as setStudentStatusLabel,
description as setStudentStatusDescription,
hasReason as setStudentStatusHasReason
} from './SetStudentStatusAction'
const actionDefinitions: ActionDefinition[] = [
{
eventName: addScoreEventName,
label: addScoreLabel,
description: addScoreDescription,
component: AddScoreAction,
hasReason: addScoreHasReason
},
{
eventName: addTagEventName,
label: addTagLabel,
description: addTagDescription,
component: AddTagAction,
hasReason: addTagHasReason
},
{
eventName: sendNotificationEventName,
label: sendNotificationLabel,
description: sendNotificationDescription,
component: SendNotificationAction,
hasReason: sendNotificationHasReason
},
{
eventName: setStudentStatusEventName,
label: setStudentStatusLabel,
description: setStudentStatusDescription,
component: SetStudentStatusAction,
hasReason: setStudentStatusHasReason
}
]
actionDefinitions.forEach((def) => actionRegistry.register(def))
export {
AddScoreAction,
AddTagAction,
SendNotificationAction,
SetStudentStatusAction
}
@@ -0,0 +1,17 @@
import './triggers'
import './actions'
export { triggerRegistry, actionRegistry, allTriggers, allActions } from './registry'
export type {
TriggerComponentProps,
ActionComponentProps,
TriggerDefinition,
ActionDefinition,
TriggerItem,
ActionItem
} from './types'
export { default as TriggerItemComponent } from './TriggerItem'
export { default as ActionItemComponent } from './ActionItem'
export { IntervalTimeTrigger, StudentTagTrigger, RandomTimeTrigger } from './triggers'
export { AddScoreAction, AddTagAction, SendNotificationAction, SetStudentStatusAction } from './actions'
@@ -0,0 +1,66 @@
import type { TriggerDefinition, ActionDefinition, TriggerLogic } from './types'
class TriggerRegistry {
private triggers: Map<string, TriggerDefinition> = new Map()
register(definition: TriggerDefinition) {
this.triggers.set(definition.eventName, definition)
}
get(eventName: string): TriggerDefinition | undefined {
return this.triggers.get(eventName)
}
getAll(): TriggerDefinition[] {
return Array.from(this.triggers.values())
}
getOptions() {
return this.getAll().map((t) => ({ label: t.label, value: t.eventName }))
}
getLogic(eventName: string): TriggerLogic | undefined {
return this.triggers.get(eventName)?.triggerLogic
}
}
class ActionRegistry {
private actions: Map<string, ActionDefinition> = new Map()
register(definition: ActionDefinition) {
this.actions.set(definition.eventName, definition)
}
get(eventName: string): ActionDefinition | undefined {
return this.actions.get(eventName)
}
getAll(): ActionDefinition[] {
return Array.from(this.actions.values())
}
getOptions() {
return this.getAll().map((a) => ({ label: a.label, value: a.eventName }))
}
}
export const triggerRegistry = new TriggerRegistry()
export const actionRegistry = new ActionRegistry()
export const allTriggers = {
get list() {
return triggerRegistry.getAll()
},
get options() {
return triggerRegistry.getOptions()
}
}
export const allActions = {
get list() {
return actionRegistry.getAll()
},
get options() {
return actionRegistry.getOptions()
}
}
@@ -0,0 +1,41 @@
import React from 'react'
import { InputNumber } from 'tdesign-react'
import type { TriggerComponentProps } from '../types'
export const eventName = 'interval_time_passed'
export const label = '根据间隔时间触发'
export const description = '当间隔时间到达时触发自动化'
export const triggerLogic = {
eventName,
label,
description,
validate: (value: string) => {
const minutes = parseInt(value, 10)
if (isNaN(minutes) || minutes <= 0) {
return { valid: false, message: '请输入有效的时间间隔(分钟)' }
}
return { valid: true }
}
}
const IntervalTimeTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
const numValue = value ? parseInt(value, 10) : undefined
const handleChange = (v: any) => {
const numV = typeof v === 'number' ? v : (v ? Number(v) : undefined)
onChange(numV !== undefined && numV !== null && !isNaN(numV) ? String(numV) : '')
}
return (
<InputNumber
placeholder="请输入时间间隔(分钟)"
style={{ width: '150px' }}
value={numValue}
onChange={handleChange}
min={1}
theme="column"
/>
)
}
export default IntervalTimeTrigger
@@ -0,0 +1,77 @@
import React from 'react'
import { InputNumber, Row, Col } from 'tdesign-react'
import type { TriggerComponentProps } from '../types'
export const eventName = 'random_time'
export const label = '随机时间触发'
export const description = '在指定时间范围内随机触发自动化'
export const triggerLogic = {
eventName,
label,
description,
validate: (value: string) => {
const minutes = parseInt(value, 10)
if (isNaN(minutes) || minutes <= 0) {
return { valid: false, message: '请输入有效的时间范围(分钟)' }
}
return { valid: true }
}
}
interface RandomTimeConfig {
minHour: number
maxHour: number
}
const RandomTimeTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
let config: RandomTimeConfig = { minHour: 9, maxHour: 18 }
try {
if (value) {
const parsed = JSON.parse(value)
config = { ...config, ...parsed }
}
} catch {}
const handleChange = (key: keyof RandomTimeConfig, v: any) => {
const numV = typeof v === 'number' ? v : (v ? Number(v) : undefined)
const newConfig = { ...config, [key]: numV ?? (key === 'minHour' ? 0 : 23) }
onChange(JSON.stringify(newConfig))
}
return (
<Row gutter={8}>
<Col>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}></span>
<InputNumber
placeholder="最小小时"
style={{ width: '70px' }}
value={config.minHour}
onChange={(v) => handleChange('minHour', v)}
min={0}
max={23}
theme="column"
/>
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}></span>
</div>
</Col>
<Col>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}></span>
<InputNumber
placeholder="最大小时"
style={{ width: '70px' }}
value={config.maxHour}
onChange={(v) => handleChange('maxHour', v)}
min={0}
max={23}
theme="column"
/>
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}></span>
</div>
</Col>
</Row>
)
}
export default RandomTimeTrigger
@@ -0,0 +1,50 @@
import React, { useState, useEffect } from 'react'
import { Select } from 'tdesign-react'
import type { TriggerComponentProps } from '../types'
export const eventName = 'student_tag_added'
export const label = '当学生添加标签时触发'
export const description = '当学生添加特定标签时触发自动化'
export const triggerLogic = {
eventName,
label,
description,
validate: (value: string) => {
if (!value || value.trim() === '') {
return { valid: false, message: '请输入标签名称' }
}
return { valid: true }
}
}
const StudentTagTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
const [tags, setTags] = useState<{ label: string; value: string }[]>([])
useEffect(() => {
const fetchTags = async () => {
try {
const res = await (window as any).api.invoke('tag:getAll', {})
if (res.success && res.data) {
setTags(res.data.map((tag: any) => ({ label: tag.name, value: tag.name })))
}
} catch (e) {
console.error('Failed to fetch tags', e)
}
}
fetchTags()
}, [])
return (
<Select
placeholder="请选择标签"
style={{ width: '150px' }}
value={value ?? ''}
onChange={(v: any) => onChange(v ? String(v) : '')}
options={tags}
filterable
clearable
/>
)
}
export default StudentTagTrigger
@@ -0,0 +1,55 @@
import { triggerRegistry } from '../registry'
import type { TriggerDefinition } from '../types'
import IntervalTimeTrigger, {
eventName as intervalEventName,
label as intervalLabel,
description as intervalDescription,
triggerLogic as intervalTriggerLogic
} from './IntervalTimeTrigger'
import StudentTagTrigger, {
eventName as studentTagEventName,
label as studentTagLabel,
description as studentTagDescription,
triggerLogic as studentTagTriggerLogic
} from './StudentTagTrigger'
import RandomTimeTrigger, {
eventName as randomTimeEventName,
label as randomTimeLabel,
description as randomTimeDescription,
triggerLogic as randomTimeTriggerLogic
} from './RandomTimeTrigger'
const triggerDefinitions: TriggerDefinition[] = [
{
eventName: intervalEventName,
label: intervalLabel,
description: intervalDescription,
component: IntervalTimeTrigger,
triggerLogic: intervalTriggerLogic
},
{
eventName: studentTagEventName,
label: studentTagLabel,
description: studentTagDescription,
component: StudentTagTrigger,
triggerLogic: studentTagTriggerLogic
},
{
eventName: randomTimeEventName,
label: randomTimeLabel,
description: randomTimeDescription,
component: RandomTimeTrigger,
triggerLogic: randomTimeTriggerLogic
}
]
triggerDefinitions.forEach((def) => triggerRegistry.register(def))
export {
IntervalTimeTrigger,
StudentTagTrigger,
RandomTimeTrigger
}
@@ -0,0 +1,51 @@
import React from 'react'
export interface TriggerComponentProps {
value: string
onChange: (value: string) => void
}
export interface ActionComponentProps {
value: string
reason: string
onChange: (value: string) => void
onReasonChange: (reason: string) => void
}
export interface TriggerLogic {
eventName: string
label: string
description: string
validate: (value: string) => { valid: boolean; message?: string }
calculateNextTime?: (value: string, lastExecuted: Date | undefined, now: Date) => { delayMs: number; nextExecuteTime: Date }
check?: (context: any, value: string) => { shouldExecute: boolean; message?: string }
}
export interface TriggerDefinition {
eventName: string
label: string
description: string
component: React.FC<TriggerComponentProps>
triggerLogic?: TriggerLogic
}
export interface ActionDefinition {
eventName: string
label: string
description: string
component: React.FC<ActionComponentProps>
hasReason?: boolean
}
export interface TriggerItem {
id: number
eventName: string
value: string
}
export interface ActionItem {
id: number
eventName: string
value: string
reason: string
}
+14 -130
View File
@@ -1,7 +1,7 @@
import React from 'react'
import { Service } from '../../../shared/kernel'
import { ClientContext } from '../ClientContext'
import { Input, Select } from 'tdesign-react'
import { allTriggers, allActions } from '../components/com.automatically'
import type { TriggerItem, ActionItem } from '../components/com.automatically/types'
export interface AutoScoreRule {
id: number
@@ -27,123 +27,8 @@ declare module '../../../shared/kernel' {
}
}
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 { allTriggers, allActions }
export type { TriggerItem, ActionItem }
export class AutoScoreService extends Service {
constructor(ctx: ClientContext) {
@@ -183,22 +68,19 @@ export class AutoScoreService extends Service {
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]
const defaultTrigger = allTriggers.list[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
t.id === id ? { ...t, eventName, value: '' } : t
)
}
@@ -210,22 +92,20 @@ export class AutoScoreService extends Service {
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]
const defaultAction = allActions.list[0]
return {
id: nextId,
eventName: defaultAction.eventName,
valueType: defaultAction.valueType,
value: ''
value: '',
reason: ''
}
}
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
a.id === id ? { ...a, eventName, value: '' } : a
)
}
@@ -233,6 +113,10 @@ export class AutoScoreService extends Service {
return actionList.map((a) => (a.id === id ? { ...a, value } : a))
}
updateActionReason(actionList: ActionItem[], id: number, reason: string): ActionItem[] {
return actionList.map((a) => (a.id === id ? { ...a, reason } : a))
}
deleteActionItem(actionList: ActionItem[], id: number): ActionItem[] {
return actionList.filter((a) => a.id !== id)
}
@@ -0,0 +1,52 @@
import type { TriggerLogic } from './types'
export const intervalTimeTrigger: TriggerLogic = {
eventName: 'interval_time_passed',
label: '根据间隔时间触发',
description: '当间隔时间到达时触发自动化',
validate: (value: string) => {
const minutes = parseInt(value, 10)
if (isNaN(minutes) || minutes <= 0) {
return { valid: false, message: '请输入有效的时间间隔(分钟)' }
}
return { valid: true }
},
calculateNextTime: (value: string, lastExecuted: Date | undefined, now: Date) => {
const intervalMinutes = parseInt(value, 10)
if (isNaN(intervalMinutes) || intervalMinutes <= 0) {
return { delayMs: 0, nextExecuteTime: now }
}
const intervalMs = intervalMinutes * 60 * 1000
let delayMs = intervalMs
if (lastExecuted) {
const timeSinceLastExecution = now.getTime() - lastExecuted.getTime()
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
if (timeSinceLastExecution >= intervalMs) {
delayMs = 0
}
}
const nextExecuteTime = new Date(now.getTime() + delayMs)
return { delayMs, nextExecuteTime }
},
check: (context, value) => {
const result = intervalTimeTrigger.calculateNextTime!(
value,
context.rule.triggers?.find((t) => t.event === 'interval_time_passed')
? context.now
: undefined,
context.now
)
return {
shouldExecute: result.delayMs === 0,
matchedStudents: context.students,
nextExecuteTime: result.nextExecuteTime,
delayMs: result.delayMs
}
}
}
+65
View File
@@ -0,0 +1,65 @@
import type { TriggerLogic } from './types'
export const randomTimeTrigger: TriggerLogic = {
eventName: 'random_time_reached',
label: '随机时间触发',
description: '当随机时间到达时触发自动化',
validate: (value: string) => {
if (!value) {
return { valid: true }
}
try {
const config = JSON.parse(value)
if (config.minHour !== undefined || config.maxHour !== undefined) {
const minHour = config.minHour ?? 0
const maxHour = config.maxHour ?? 23
if (minHour < 0 || minHour > 23 || maxHour < 0 || maxHour > 23) {
return { valid: false, message: '小时范围必须在0-23之间' }
}
if (minHour > maxHour) {
return { valid: false, message: '最小小时不能大于最大小时' }
}
}
return { valid: true }
} catch {
return { valid: false, message: '配置格式错误' }
}
},
calculateNextTime: (value: string, _lastExecuted: Date | undefined, now: Date) => {
let config = { minHour: 9, maxHour: 18 }
try {
if (value) {
const parsed = JSON.parse(value)
config = { ...config, ...parsed }
}
} catch {}
const minHour = config.minHour ?? 0
const maxHour = config.maxHour ?? 23
const randomHour = Math.floor(Math.random() * (maxHour - minHour + 1)) + minHour
const randomMinute = Math.floor(Math.random() * 60)
let targetDate = new Date(now)
targetDate.setHours(randomHour, randomMinute, 0, 0)
if (targetDate.getTime() <= now.getTime()) {
targetDate.setDate(targetDate.getDate() + 1)
}
const delayMs = targetDate.getTime() - now.getTime()
return { delayMs, nextExecuteTime: targetDate }
},
check: (context, value) => {
const result = randomTimeTrigger.calculateNextTime!(value, undefined, context.now)
return {
shouldExecute: result.delayMs === 0,
matchedStudents: context.students,
nextExecuteTime: result.nextExecuteTime,
delayMs: result.delayMs
}
}
}
+30
View File
@@ -0,0 +1,30 @@
import type { TriggerLogic } from './types'
export const studentTagTrigger: TriggerLogic = {
eventName: 'student_tag_matched',
label: '按照学生标签触发',
description: '当学生标签匹配时触发自动化',
validate: (value: string) => {
if (!value || value.trim() === '') {
return { valid: false, message: '请输入标签名称' }
}
return { valid: true }
},
check: (context, value) => {
const tagName = value.trim().toLowerCase()
const matchedStudents = context.students.filter((student: any) => {
if (student.tags && Array.isArray(student.tags)) {
return student.tags.some((tag: string) => tag.toLowerCase() === tagName)
}
return false
})
return {
shouldExecute: matchedStudents.length > 0,
matchedStudents,
nextExecuteTime: context.now
}
}
}
+17
View File
@@ -0,0 +1,17 @@
import type { TriggerLogic } from './types'
import { intervalTimeTrigger } from './IntervalTimeTrigger'
import { studentTagTrigger } from './StudentTagTrigger'
import { randomTimeTrigger } from './RandomTimeTrigger'
export * from './types'
export { intervalTimeTrigger, studentTagTrigger, randomTimeTrigger }
export const allTriggerLogics: TriggerLogic[] = [
intervalTimeTrigger,
studentTagTrigger,
randomTimeTrigger
]
export function getTriggerLogic(eventName: string): TriggerLogic | undefined {
return allTriggerLogics.find((t) => t.eventName === eventName)
}
+32
View File
@@ -0,0 +1,32 @@
export interface TriggerContext {
students: any[]
events: any[]
rule: {
id: number
name: string
studentNames: string[]
triggers?: { event: string; value?: string }[]
actions?: { event: string; value?: string; reason?: string }[]
}
now: Date
}
export interface TriggerResult {
shouldExecute: boolean
matchedStudents?: any[]
nextExecuteTime?: Date
delayMs?: number
}
export interface TriggerLogic {
eventName: string
label: string
description: string
validate: (value: string) => { valid: boolean; message?: string }
calculateNextTime?: (
value: string,
lastExecuted: Date | undefined,
now: Date
) => { delayMs: number; nextExecuteTime: Date }
check?: (context: TriggerContext, value: string) => TriggerResult
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"],