mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
未完成:自动加分
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user