feat: 自动化加分基本修复完毕 现在是全新的自动化qwqqq!

This commit is contained in:
NanGua-QWQ
2026-04-06 14:41:27 +08:00
parent 448df1a40b
commit ed4891210d
11 changed files with 1259 additions and 394 deletions
@@ -51,6 +51,7 @@ export type ActionDraftError = "action_required" | "score_required" | "tag_requi
const TRIGGER_FIELD_INTERVAL = "interval_minutes"
const TRIGGER_FIELD_TAG = "student_tag"
const TRIGGER_FIELD_SQL = "student_sql"
const TRIGGER_TYPE_INTERVAL = "interval_duration"
const TRIGGER_WIDGET_INTERVAL = "interval_duration"
const OP_EQUAL = "equal"
@@ -146,6 +147,24 @@ const ruleFromTrigger = (trigger: AutoScoreTrigger): JsonRule | null => {
}
}
if (
trigger.event === "query_sql" ||
trigger.event === "student_query_sql" ||
trigger.event === "student_sql"
) {
const sql = toStringValue(trigger.value).trim()
if (!sql) return null
return {
id: QbUtils.uuid(),
type: "rule",
properties: {
field: TRIGGER_FIELD_SQL,
operator: OP_EQUAL,
value: [sql],
},
}
}
return null
}
@@ -175,6 +194,15 @@ const triggerFromRule = (rule: JsonRule): AutoScoreTrigger | null => {
}
}
if (field === TRIGGER_FIELD_SQL) {
const sql = toStringValue(value).trim()
if (!sql) return null
return {
event: "query_sql",
value: sql,
}
}
return null
}
@@ -197,6 +225,25 @@ const collectTriggersFromItems = (items: JsonItem[] | undefined): AutoScoreTrigg
export const createTriggerQueryConfig = (t: TFunction, tagOptions: AutoScoreTagOption[]): Config =>
({
...AntdConfig,
conjunctions: {
...AntdConfig.conjunctions,
AND: {
...AntdConfig.conjunctions.AND,
label: t("autoScore.relationAnd"),
},
OR: {
...AntdConfig.conjunctions.OR,
label: t("autoScore.relationOr"),
},
},
operators: {
...AntdConfig.operators,
[OP_MULTISELECT_CONTAINS]: {
...AntdConfig.operators[OP_MULTISELECT_CONTAINS],
label: t("autoScore.operatorContains"),
labelForFormat: t("autoScore.operatorContains"),
},
},
widgets: {
...AntdConfig.widgets,
[TRIGGER_WIDGET_INTERVAL]: {
@@ -250,6 +297,15 @@ export const createTriggerQueryConfig = (t: TFunction, tagOptions: AutoScoreTagO
showSearch: true,
},
},
[TRIGGER_FIELD_SQL]: {
label: t("autoScore.triggerStudentSql"),
type: "text",
operators: [OP_EQUAL],
valueSources: ["value"],
fieldSettings: {
placeholder: t("autoScore.triggerStudentSqlPlaceholder"),
},
},
},
settings: {
...AntdConfig.settings,
@@ -257,6 +313,7 @@ export const createTriggerQueryConfig = (t: TFunction, tagOptions: AutoScoreTagO
compactMode: false,
renderSize: "medium",
showNot: true,
notLabel: t("autoScore.relationNot"),
forceShowConj: true,
canLeaveEmptyGroup: false,
canReorder: true,
@@ -267,6 +324,9 @@ export const createTriggerQueryConfig = (t: TFunction, tagOptions: AutoScoreTagO
export const createEmptyTriggerTree = (config: Config): ImmutableTree =>
QbUtils.checkTree(QbUtils.loadTree(buildEmptyGroup()), config)
export const normalizeTriggerTree = (tree: ImmutableTree, config: Config): ImmutableTree =>
QbUtils.checkTree(tree, config)
export const triggersToQueryTree = (
config: Config,
triggers: AutoScoreTrigger[]
+46 -3
View File
@@ -27,6 +27,7 @@ import {
createEmptyTriggerTree,
createTriggerQueryConfig,
hasUnsupportedTriggerLogic,
normalizeTriggerTree,
normalizeActionDrafts,
queryTreeToTriggers,
triggersToQueryTree,
@@ -54,6 +55,8 @@ interface AutoScoreManagerProps {
canEdit: boolean
}
const getRuleFileRelativePath = (ruleId: number) => `auto-score/rule-${ruleId}.json`
function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element {
const { t, i18n } = useTranslation()
const [form] = Form.useForm<RuleFormValues>()
@@ -92,6 +95,10 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
}
}, [currentPage, pageSize, rules.length])
useEffect(() => {
setTriggerTree((prevTree) => normalizeTriggerTree(prevTree, triggerConfig))
}, [triggerConfig])
const resetEditor = () => {
setEditingRuleId(null)
form.setFieldsValue({ name: "", studentNames: [] })
@@ -262,6 +269,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
const res = await api.autoScoreDeleteRule(ruleId)
if (res.success) {
await api.fsDeleteFile(getRuleFileRelativePath(ruleId), "automatic").catch(() => void 0)
messageApi.success(t("autoScore.deleteSuccess"))
if (editingRuleId === ruleId) {
resetEditor()
@@ -295,6 +303,38 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
}
}
const handleOpenRuleFile = async (rule: AutoScoreRule) => {
const api = (window as any).api
if (!api) return
try {
const relativePath = getRuleFileRelativePath(rule.id)
const fileContent = {
id: rule.id,
name: rule.name,
enabled: rule.enabled,
studentNames: rule.studentNames,
triggers: rule.triggers,
actions: rule.actions,
lastExecuted: rule.lastExecuted ?? null,
exportedAt: new Date().toISOString(),
}
const writeRes = await api.fsWriteJson(relativePath, fileContent, "automatic")
if (!writeRes?.success) {
throw new Error("prepare rule file failed")
}
const openRes = await api.fsOpenPath(relativePath, "automatic")
if (!openRes?.success) {
throw new Error(openRes?.message || "open rule file failed")
}
} catch {
messageApi.error(t("autoScore.openFileFailed"))
}
}
const formatLastExecuted = (value?: string | null) => {
if (!value) return t("autoScore.notExecuted")
const date = new Date(value)
@@ -358,12 +398,15 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
{
title: t("common.operation"),
key: "operation",
width: 140,
width: 220,
render: (_, row) => (
<Space size={4}>
<Button type="link" disabled={!canEdit} onClick={() => handleEdit(row)}>
{t("common.edit")}
</Button>
<Button type="link" onClick={() => handleOpenRuleFile(row).catch(() => void 0)}>
{t("autoScore.openFile")}
</Button>
<Popconfirm
title={t("autoScore.deleteConfirm")}
onConfirm={() => {
@@ -391,7 +434,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
type="warning"
showIcon
style={{ marginBottom: "16px" }}
message={t("autoScore.adminRequired")}
title={t("autoScore.adminRequired")}
/>
)}
@@ -403,7 +446,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
name="name"
rules={[{ required: true, message: t("autoScore.nameRequired") }]}
>
<Input placeholder={t("autoScore.namePlaceholder")} disabled={!canEdit} />
<Input placeholder={t("autoScore.namePlaceholder")} disabled={!canEdit} autoComplete="off"/>
</Form.Item>
<Form.Item label={t("autoScore.applicableStudents")} name="studentNames">
<Select
+8 -2
View File
@@ -814,9 +814,11 @@
"tagNamesPlaceholder": "e.g. excellent_student,class_monitor",
"triggerIntervalTime": "Interval Time",
"triggerStudentTag": "Student Tag",
"triggerStudentSql": "Student SQL",
"triggerStudentSqlPlaceholder": "Enter student SQL or a WHERE condition",
"actionAddScore": "Add Score",
"actionAddTag": "Add Tag",
"intervalAmountPlaceholder": "Enter interval amount",
"intervalAmountPlaceholder": "Enter interval time",
"intervalUnitMonth": "month(s) later",
"intervalUnitDay": "day(s) later",
"intervalUnitMinute": "minute(s) later",
@@ -827,7 +829,11 @@
"tagNamePlaceholder": "Enter tag name",
"reasonPlaceholder": "Enter reason",
"relationAnd": "AND",
"relationOr": "OR"
"relationOr": "OR",
"operatorContains": "Contains",
"relationNot": "Not",
"openFile": "Open File",
"openFileFailed": "Failed to open automation file"
},
"triggers": {
"studentTag": {
+9 -3
View File
@@ -812,9 +812,11 @@
"operationNoteLabel": "操作说明(可选)",
"triggerIntervalTime": "间隔时间",
"triggerStudentTag": "学生标签",
"triggerStudentSql": "学生 SQL 条件",
"triggerStudentSqlPlaceholder": "输入筛选学生的 SQL 或 WHERE 条件",
"actionAddScore": "加分",
"actionAddTag": "添加标签",
"intervalAmountPlaceholder": "请输入间隔数量",
"intervalAmountPlaceholder": "请输入间隔时间数值",
"intervalUnitMonth": "个月后",
"intervalUnitDay": "天后",
"intervalUnitMinute": "分钟后",
@@ -824,8 +826,12 @@
"scorePlaceholder": "请输入分数",
"tagNamePlaceholder": "请输入标签名称",
"reasonPlaceholder": "请输入理由",
"relationAnd": "且",
"relationOr": "或"
"relationAnd": "且",
"relationOr": "或",
"operatorContains": "包含",
"relationNot": "非",
"openFile": "打开文件",
"openFileFailed": "打开自动化文件失败"
},
"triggers": {
"studentTag": {
+5
View File
@@ -613,6 +613,11 @@ const api = {
folder?: "automatic" | "script"
): Promise<{ success: boolean; data: boolean }> =>
invoke("fs_file_exists", { relativePath, folder }),
fsOpenPath: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean; message?: string }> =>
invoke("fs_open_path", { relativePath, folder }),
// App
registerUrlProtocol: (): Promise<{