未完成:自动加分

This commit is contained in:
NanGua-QWQ
2026-03-13 23:52:49 +08:00
parent 97fcc350aa
commit a88138da65
5 changed files with 77 additions and 169 deletions
@@ -1,20 +1,16 @@
import { Space, Input, InputNumber, Select, Button } from 'antd' import { Space, Input, InputNumber, Select, Button } from 'antd'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons' import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { AutoScoreAction } from './ruleBuilderUtils' import type { ActionItem } from './types'
import { ACTION_DEFINITIONS, SCORE_RANGE } from './constants'
interface ActionComponentProps { interface ActionComponentProps {
actions: AutoScoreAction[] actions: ActionItem[]
onAdd: () => void onAdd: () => void
onRemove: (index: number) => void onRemove: (id: number) => void
onChange: (index: number, field: keyof AutoScoreAction, value: any) => void onChange: (id: number, field: keyof ActionItem, value: string | number) => void
} }
const ACTION_DEFINITIONS = [
{ eventName: 'add_score', labelKey: 'autoScore.actionAddScore' },
{ eventName: 'add_tag', labelKey: 'autoScore.actionAddTag' }
]
export const ActionComponent: React.FC<ActionComponentProps> = ({ export const ActionComponent: React.FC<ActionComponentProps> = ({
actions, actions,
onAdd, onAdd,
@@ -23,16 +19,16 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => { const handleActionChange = (id: number, field: keyof ActionItem, value: string | number) => {
onChange(index, field, value) onChange(id, field, value)
} }
return ( return (
<div> <div>
<Space orientation="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
{actions.map((action, index) => ( {actions.map((action) => (
<div <div
key={index} key={action.id}
style={{ style={{
padding: '12px', padding: '12px',
border: '1px solid #d9d9d9', border: '1px solid #d9d9d9',
@@ -42,8 +38,8 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
> >
<Space style={{ width: '100%' }} wrap> <Space style={{ width: '100%' }} wrap>
<Select <Select
value={action.event} value={action.eventName}
onChange={(value) => handleActionChange(index, 'event', value)} onChange={(value) => handleActionChange(action.id, 'eventName', value)}
style={{ width: 150 }} style={{ width: 150 }}
options={ACTION_DEFINITIONS.map((d) => ({ options={ACTION_DEFINITIONS.map((d) => ({
label: t(d.labelKey), label: t(d.labelKey),
@@ -51,30 +47,30 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
}))} }))}
/> />
{action.event === 'add_score' && ( {action.eventName === 'add_score' && (
<InputNumber <InputNumber
value={action.value ? parseInt(action.value) : 0} value={action.value ? parseInt(action.value) : undefined}
onChange={(value) => handleActionChange(index, 'value', String(value || 0))} onChange={(value) => handleActionChange(action.id, 'value', String(value || 0))}
placeholder={t('autoScore.scoreLabel')} placeholder={t('autoScore.scorePlaceholder')}
min={-100} min={SCORE_RANGE.MIN}
max={100} max={SCORE_RANGE.MAX}
style={{ width: 120 }} style={{ width: 120 }}
/> />
)} )}
{action.event === 'add_tag' && ( {action.eventName === 'add_tag' && (
<Input <Input
value={action.value} value={action.value}
onChange={(e) => handleActionChange(index, 'value', e.target.value)} onChange={(e) => handleActionChange(action.id, 'value', e.target.value)}
placeholder={t('autoScore.tagNameLabel')} placeholder={t('autoScore.tagNamePlaceholder')}
style={{ width: 200 }} style={{ width: 200 }}
/> />
)} )}
<Input <Input
value={action.reason} value={action.reason}
onChange={(e) => handleActionChange(index, 'reason', e.target.value)} onChange={(e) => handleActionChange(action.id, 'reason', e.target.value)}
placeholder={t('autoScore.operationNoteLabel')} placeholder={t('autoScore.reasonPlaceholder')}
style={{ flex: 1, minWidth: 200 }} style={{ flex: 1, minWidth: 200 }}
/> />
@@ -82,14 +78,14 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
type="text" type="text"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={() => onRemove(index)} onClick={() => onRemove(action.id)}
/> />
</Space> </Space>
</div> </div>
))} ))}
<Button type="dashed" icon={<PlusOutlined />} onClick={onAdd} block> <Button type="dashed" icon={<PlusOutlined />} onClick={onAdd} block>
{t('autoScore.addOperation')} {t('autoScore.addAction')}
</Button> </Button>
</Space> </Space>
</div> </div>
@@ -1,9 +1,7 @@
import type { RuleGroupType, Field, Operator } from 'react-querybuilder' import type { RuleGroupType, Field, RuleType } from 'react-querybuilder'
import {defaultOperators} from 'react-querybuilder' import {defaultOperators} from 'react-querybuilder'
import { fetchAllTags } from '../TagEditorDialog' import { fetchAllTags } from '../TagEditorDialog'
import type { FullField, RuleType } from 'react-querybuilder';
import { toFullOption } from 'react-querybuilder';
const tags = await fetchAllTags() const tags = await fetchAllTags()
export interface AutoScoreTrigger { export interface AutoScoreTrigger {
@@ -18,68 +16,35 @@ export interface AutoScoreAction {
reason?: string reason?: string
} }
const musicalInstruments = fetchAllTags().then((tags) => tags.map((tag) => tag.name))
export interface AutoScoreRuleData { export interface AutoScoreRuleData {
triggers: AutoScoreTrigger[] triggers: AutoScoreTrigger[]
actions: AutoScoreAction[] actions: AutoScoreAction[]
} }
// Function to get fields with i18n support export const validator = (r: RuleType) => !!r.value;
export const getFields = (t: (key: string) => string): Field[] => [ export const getFields = (t: (key: string) => string): Field[] => [
{
name: 'interval_time_passed',
label: t('autoScore.triggerIntervalTime'),
placeholder: t('autoScore.intervalMinutesPlaceholder'),
inputType: 'date',
datatype: 'timestamp with time zone',
},
{ {
name: 'student_has_tag', name: 'student_has_tag',
label: t('autoScore.triggerStudentTag'), label: t('triggers.studentTag.label'),
placeholder: t('autoScore.tagNamesPlaceholder'), placeholder: t('triggers.studentTag.placeholder'),
valueEditorType: 'multiselect', valueEditorType: 'multiselect',
values: tags.map((tag) => tag.name), values: tags.map((tag) => tag.name),
defaultValue: tags.length > 0 ? [tags[0].name] : [], defaultValue: tags.length > 0 ? [tags[0].name] : [],
operators: defaultOperators.filter((op) => op.name === '='), operators: defaultOperators.filter((op) => op.name === 'in'),
}, },
{ {
name: 'tourStops', name: 'interval_time_passed',
label: 'Tour stops', label: t('triggers.intervalTime.label'),
matchModes: ['all'],
matchModes: true,
subproperties: [ subproperties: [
{ name: 'date', label: 'Date', inputType: 'date', datatype: 'date' }, { name: 'month', label: t('triggers.intervalTime.monthLabel'), inputType: 'number', datatype: 'month', operators: ['='] },
{ name: 'time', label: 'Time', inputType: 'time', datatype: 'time' }, /* { name: 'week', label: t('triggers.intervalTime.weekLabel'), inputType: 'week', datatype: 'week', operators: ['='] },
*/ { name: 'time', label: t('triggers.intervalTime.timeLabel'), inputType: 'time', datatype: 'time', operators: ['='] },
], ],
}, },
] ]
/* export const operators: Operator[] = [
{ name: '=', label: '=' },
{ name: 'contains', label: 'contains' },
{ name: 'between', label: 'between' }
] */
export const fields: FullField[] = (
[
{
name: 'interval_time_passed',
label: ('autoScore.triggerIntervalTime'),
placeholder: ('autoScore.intervalMinutesPlaceholder')
},
{
name: 'student_has_tag',
label: ('autoScore.triggerStudentTag'),
placeholder: ('autoScore.tagNamesPlaceholder'),
valueEditorType: 'multiselect',
values: tags.map((tag) => tag.name),
defaultValue: 'more_cowbell',
operators: defaultOperators.filter((op) => op.name === 'in'),
}
] satisfies Field[]
).map((o) => toFullOption(o));
export const defaultQuery: RuleGroupType = { export const defaultQuery: RuleGroupType = {
combinator: 'and', combinator: 'and',
rules: [{ field: 'interval_time_passed', operator: '=', value: '1440' }] rules: [{ field: 'interval_time_passed', operator: '=', value: '1440' }]
@@ -88,7 +53,7 @@ export const defaultQuery: RuleGroupType = {
export function queryToAutoScoreRule(query: RuleGroupType): AutoScoreRuleData { export function queryToAutoScoreRule(query: RuleGroupType): AutoScoreRuleData {
const triggers: AutoScoreTrigger[] = [] const triggers: AutoScoreTrigger[] = []
const processRuleGroup = (group: RuleGroupType, relation: 'AND' | 'OR' = 'AND') => { /* const processRuleGroup = (group: RuleGroupType, relation: 'AND' | 'OR' = 'AND') => {
group.rules.forEach((rule, index) => { group.rules.forEach((rule, index) => {
if ('rules' in rule) { if ('rules' in rule) {
processRuleGroup(rule, group.combinator === 'and' ? 'AND' : 'OR') processRuleGroup(rule, group.combinator === 'and' ? 'AND' : 'OR')
@@ -105,9 +70,9 @@ export function queryToAutoScoreRule(query: RuleGroupType): AutoScoreRuleData {
triggers.push(trigger) triggers.push(trigger)
} }
}) })
} } */
processRuleGroup(query) /* processRuleGroup(query) */
return { return {
triggers, triggers,
@@ -138,27 +103,3 @@ export function autoScoreRuleToQuery(ruleData: AutoScoreRuleData): RuleGroupType
rules: rules.length > 0 ? rules : defaultQuery.rules rules: rules.length > 0 ? rules : defaultQuery.rules
} }
} }
/* import type { Field, FullField, RuleType } from 'react-querybuilder';
import { defaultOperators, toFullOption } from 'react-querybuilder';
import { fetchAllTags } from '../TagEditorDialog'
const tags = await fetchAllTags()
export const fields: FullField[] = (
[
{
name: 'interval_time_passed',
label: ('autoScore.triggerIntervalTime'),
placeholder: ('autoScore.intervalMinutesPlaceholder')
},
{
name: 'student_has_tag',
label: ('autoScore.triggerStudentTag'),
placeholder: ('autoScore.tagNamesPlaceholder'),
valueEditorType: 'multiselect',
values: tags.map((tag) => tag.name),
defaultValue: 'more_cowbell',
operators: defaultOperators.filter((op) => op.name === 'in'),
}
] satisfies Field[]
).map((o) => toFullOption(o)); */
@@ -1,24 +1,21 @@
import { formatQuery, QueryBuilder, RuleGroupType } from 'react-querybuilder' import { formatQuery, QueryBuilder, RuleGroupType } from 'react-querybuilder'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { QueryBuilderDnD } from '@react-querybuilder/dnd'; import { QueryBuilderDnD } from '@react-querybuilder/dnd'
import * as ReactDnD from 'react-dnd'; import * as ReactDnD from 'react-dnd'
import { QueryBuilderAntD } from '@react-querybuilder/antd'; import { QueryBuilderAntD } from '@react-querybuilder/antd'
import * as ReactDndHtml5Backend from 'react-dnd-html5-backend'; import * as ReactDndHtml5Backend from 'react-dnd-html5-backend'
import * as ReactDndTouchBackend from 'react-dnd-touch-backend'; import * as ReactDndTouchBackend from 'react-dnd-touch-backend'
import { import {
getFields, getFields,
/* operators, */
defaultQuery, defaultQuery,
queryToAutoScoreRule,
autoScoreRuleToQuery, autoScoreRuleToQuery,
type AutoScoreRuleData, queryToAutoScoreRule,
AutoScoreAction type AutoScoreRuleData
} from './ruleBuilderUtils' } from './ruleBuilderUtils'
import { Card } from 'antd' import { Card } from 'antd'
import './ruleBuilderOverride.css' import './ruleBuilderOverride.css'
import { ActionComponent } from './actionComponent'
interface RuleComponentProps { interface RuleComponentProps {
initialData?: AutoScoreRuleData initialData?: AutoScoreRuleData
@@ -30,59 +27,36 @@ export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onCha
const [query, setQuery] = useState<RuleGroupType>( const [query, setQuery] = useState<RuleGroupType>(
initialData ? autoScoreRuleToQuery(initialData) : defaultQuery initialData ? autoScoreRuleToQuery(initialData) : defaultQuery
) )
/* const [actions, setActions] = useState<AutoScoreRuleData['actions']>(initialData?.actions || [])
useEffect(() => { useEffect(() => {
if (onChange) { if (initialData) {
const ruleData = queryToAutoScoreRule(query) setQuery(autoScoreRuleToQuery(initialData))
onChange({
...ruleData,
actions
})
} }
}, [query, actions]) }, [initialData])
const handleAddAction = () => {
setActions([...actions, { event: 'add_score', value: '5', reason: '' }])
}
const handleRemoveAction = (index: number) => {
setActions(actions.filter((_, i) => i !== index))
}
const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => {
const newActions = [...actions]
newActions[index] = { ...newActions[index], [field]: value }
setActions(newActions)
}
*/
const handleQueryChange = (newQuery: RuleGroupType) => { const handleQueryChange = (newQuery: RuleGroupType) => {
setQuery(newQuery) setQuery(newQuery)
if (onChange) {
const ruleData = queryToAutoScoreRule(newQuery)
onChange(ruleData)
} }
}
return ( return (
<div> <div>
<Card title={t('autoScore.triggerCondition')} style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}> <Card
title={t('autoScore.triggerCondition')}
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
>
<QueryBuilderDnD dnd={{ ...ReactDnD, ...ReactDndHtml5Backend, ...ReactDndTouchBackend }}> <QueryBuilderDnD dnd={{ ...ReactDnD, ...ReactDndHtml5Backend, ...ReactDndTouchBackend }}>
<QueryBuilderAntD> <QueryBuilderAntD>
<QueryBuilder <QueryBuilder fields={getFields(t)} query={query} onQueryChange={handleQueryChange} />
fields={getFields(t)}
/* operators={operators} */
query={query}
/* onQueryChange={handleQueryChange} */
onQueryChange={handleQueryChange} />
</QueryBuilderAntD> </QueryBuilderAntD>
</QueryBuilderDnD> </QueryBuilderDnD>
<div style={{ marginTop: '8px' }}> <div style={{ marginTop: '8px' }}>
<pre style={{ fontSize: '12px', color: '#999' }}>{formatQuery(query, 'json')}</pre> <pre style={{ fontSize: '12px', color: '#999' }}>{formatQuery(query, 'json')}</pre>
</div> </div>
</Card> </Card>
{/* <Card title={t('autoScore.executeAction')} style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<ActionComponent
actions={actions}
onAdd={handleAddAction}
onRemove={handleRemoveAction}
onChange={handleActionChange} />
</Card> */}
</div> </div>
) )
} }
+3 -1
View File
@@ -513,7 +513,9 @@
"tagsPlaceholder": "Enter tag name", "tagsPlaceholder": "Enter tag name",
"scorePlaceholder": "Enter score", "scorePlaceholder": "Enter score",
"tagNamePlaceholder": "Enter tag name", "tagNamePlaceholder": "Enter tag name",
"reasonPlaceholder": "Enter reason" "reasonPlaceholder": "Enter reason",
"relationAnd": "AND",
"relationOr": "OR"
}, },
"triggers": { "triggers": {
"studentTag": { "studentTag": {
+10 -15
View File
@@ -502,8 +502,6 @@
"scoreLabel": "分数", "scoreLabel": "分数",
"tagNameLabel": "标签名称", "tagNameLabel": "标签名称",
"operationNoteLabel": "操作说明(可选)", "operationNoteLabel": "操作说明(可选)",
"intervalMinutesPlaceholder": "例如:1440",
"tagNamesPlaceholder": "例如:优秀学生,班干部",
"triggerIntervalTime": "间隔时间", "triggerIntervalTime": "间隔时间",
"triggerStudentTag": "学生标签", "triggerStudentTag": "学生标签",
"actionAddScore": "加分", "actionAddScore": "加分",
@@ -513,11 +511,13 @@
"tagsPlaceholder": "请输入标签名称", "tagsPlaceholder": "请输入标签名称",
"scorePlaceholder": "请输入分数", "scorePlaceholder": "请输入分数",
"tagNamePlaceholder": "请输入标签名称", "tagNamePlaceholder": "请输入标签名称",
"reasonPlaceholder": "请输入理由" "reasonPlaceholder": "请输入理由",
"relationAnd": "并且",
"relationOr": "或者"
}, },
"triggers": { "triggers": {
"studentTag": { "studentTag": {
"label": "学生匹配标签时触发", "label": "学生标签",
"description": "当学生匹配特定标签时触发自动化", "description": "当学生匹配特定标签时触发自动化",
"placeholder": "请选择标签", "placeholder": "请选择标签",
"required": "请输入标签名称" "required": "请输入标签名称"
@@ -525,21 +525,16 @@
"randomTime": { "randomTime": {
"label": "随机时间触发", "label": "随机时间触发",
"description": "在指定时间范围内随机触发自动化", "description": "在指定时间范围内随机触发自动化",
"from": "从",
"to": "到",
"hour": "时",
"minHourPlaceholder": "最小小时",
"maxHourPlaceholder": "最大小时",
"required": "请输入有效的时间范围(分钟)" "required": "请输入有效的时间范围(分钟)"
}, },
"intervalTime": { "intervalTime": {
"label": "根据间隔时间触发", "label": "间隔时间",
"description": "当间隔时间到达时触发自动化", "description": "当间隔时间到达时触发自动化",
"days": "", "placeholder": "请输入时间间隔",
"minutes": "分钟", "required": "请输入有效的时间",
"placeholderMinutes": "请输入时间间隔(分钟)", "monthLabel": "月",
"placeholderDays": "请输入时间间隔(天)", "weekLabel": "周",
"required": "请输入有效的时间间隔(分钟)" "timeLabel": "时间"
} }
}, },
"actions": { "actions": {