未完成:自动加分

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