Alapha:自动加分

This commit is contained in:
NanGua-QWQ
2026-03-07 22:49:15 +08:00
parent 31e2305d0e
commit 7100a30f1b
11 changed files with 356 additions and 118 deletions
+2
View File
@@ -41,11 +41,13 @@
"es-object-atoms": "^1.1.1", "es-object-atoms": "^1.1.1",
"express": "^5.2.1", "express": "^5.2.1",
"i18next": "^25.8.14", "i18next": "^25.8.14",
"json-rules-engine": "^7.3.1",
"mica-electron": "^1.5.16", "mica-electron": "^1.5.16",
"os": "^0.1.2", "os": "^0.1.2",
"pg": "^8.19.0", "pg": "^8.19.0",
"pinyin-pro": "^3.27.0", "pinyin-pro": "^3.27.0",
"react-i18next": "^16.5.6", "react-i18next": "^16.5.6",
"react-querybuilder": "^8.14.0",
"react-router-dom": "^6.28.0", "react-router-dom": "^6.28.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"typeorm": "^0.3.27", "typeorm": "^0.3.27",
+6
View File
@@ -50,6 +50,9 @@ importers:
i18next: i18next:
specifier: ^25.8.14 specifier: ^25.8.14
version: 25.8.14(typescript@5.9.3) version: 25.8.14(typescript@5.9.3)
json-rules-engine:
specifier: ^7.3.1
version: 7.3.1
mica-electron: mica-electron:
specifier: ^1.5.16 specifier: ^1.5.16
version: 1.5.16 version: 1.5.16
@@ -63,6 +66,9 @@ importers:
react-i18next: react-i18next:
specifier: ^16.5.6 specifier: ^16.5.6
version: 16.5.6(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) version: 16.5.6(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
react-querybuilder:
specifier: ^8.14.0
version: 8.14.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1)
react-router-dom: react-router-dom:
specifier: ^6.28.0 specifier: ^6.28.0
version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+1 -1
View File
@@ -4,7 +4,7 @@ import { HashRouter, useLocation, useNavigate, Routes, Route } from 'react-route
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Sidebar } from './components/Sidebar' import { Sidebar } from './components/Sidebar'
import { ContentArea } from './components/ContentArea' import { ContentArea } from './components/ContentArea'
import { OOBE } from './components/OOBE' import { OOBE } from './components/OOBE/OOBE'
import { ThemeProvider, useTheme } from './contexts/ThemeContext' import { ThemeProvider, useTheme } from './contexts/ThemeContext'
function MainContent(): React.JSX.Element { function MainContent(): React.JSX.Element {
+253 -76
View File
@@ -1,14 +1,12 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { PlusOutlined, HolderOutlined } from '@ant-design/icons' import { PlusOutlined, HolderOutlined, DeleteOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { allTriggers, allActions, triggerRegistry, actionRegistry } from './com.automatically' import RuleComponent from './autoScore/ruleComponent'
import type { TriggerItem, ActionItem } from './com.automatically/types'
import TriggerItemComponent from './com.automatically/TriggerItem'
import ActionItemComponent from './com.automatically/ActionItem'
import { import {
Card, Card,
Form, Form,
Input, Input,
InputNumber,
Button, Button,
message, message,
Table, Table,
@@ -21,38 +19,84 @@ import {
} from 'antd' } from 'antd'
import type { ColumnsType } from 'antd/es/table' import type { ColumnsType } from 'antd/es/table'
import { RuleComponent } from './autoScore/ruleComponent'
import type { AutoScoreRuleData } from './autoScore/ruleBuilderUtils'
interface AutoScoreRule { interface AutoScoreRule {
id: number id: number
enabled: boolean enabled: boolean
name: string name: string
studentNames: string[] studentNames: string[]
lastExecuted?: string lastExecuted?: string
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[] triggers?: TriggerItem[]
actions?: { event: string; value?: string; reason?: string }[] actions?: ActionItem[]
}
interface TriggerItem {
id: number
eventName: string
value: string
relation: 'AND' | 'OR'
}
interface ActionItem {
id: number
eventName: string
value: string
reason: string
}
interface TagItem {
id: number
name: string
} }
interface AutoScoreRuleFormValues { interface AutoScoreRuleFormValues {
name: string name: string
studentNames: string studentNames: string[]
} }
const TRIGGER_DEFINITIONS = [
{ eventName: 'interval_time_passed', labelKey: 'autoScore.triggerIntervalTime' },
{ eventName: 'student_has_tag', labelKey: 'autoScore.triggerStudentTag' }
]
const ACTION_DEFINITIONS = [
{ eventName: 'add_score', labelKey: 'autoScore.actionAddScore' },
{ eventName: 'add_tag', labelKey: 'autoScore.actionAddTag' }
]
export const AutoScoreManager: React.FC = () => { export const AutoScoreManager: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [rules, setRules] = useState<AutoScoreRule[]>([]) const [rules, setRules] = useState<AutoScoreRule[]>([])
const [allTags, setAllTags] = useState<TagItem[]>([])
const [students, setStudents] = useState<{ id: number; name: string }[]>([]) const [students, setStudents] = useState<{ id: number; name: string }[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState<number>(50) const [pageSize, setPageSize] = useState<number>(50)
const [form] = Form.useForm() const [form] = Form.useForm()
const [editingRuleId, setEditingRuleId] = useState<number | null>(null) const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
const [ruleData, setRuleData] = useState<AutoScoreRuleData>({ const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
triggers: [], const [actionList, setActionList] = useState<ActionItem[]>([])
actions: []
})
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const getTriggerLabel = (eventName: string): string => {
const def = TRIGGER_DEFINITIONS.find((d) => d.eventName === eventName)
return def ? t(def.labelKey) : eventName
}
const getActionLabel = (eventName: string): string => {
const def = ACTION_DEFINITIONS.find((d) => d.eventName === eventName)
return def ? t(def.labelKey) : eventName
}
async function fetchAllTags() {
if (!(window as any).api) return
try {
const res = await (window as any).api.tagsGetAll()
if (res.success && res.data) {
setAllTags(res.data)
}
} catch (e) {
console.error('Failed to fetch tags:', e)
messageApi.error(t('tags.fetchFailed'))
}
}
const fetchRules = async () => { const fetchRules = async () => {
if (!(window as any).api) return if (!(window as any).api) return
@@ -116,12 +160,24 @@ export const AutoScoreManager: React.FC = () => {
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
const triggers = triggerList.map((t) => ({
event: t.eventName,
value: t.value,
relation: t.relation
}))
const actions = actionList.map((a) => ({
event: a.eventName,
value: a.value,
reason: a.reason
}))
const ruleDataToSubmit = { const ruleDataToSubmit = {
enabled: true, enabled: true,
name: values.name, name: values.name,
studentNames, studentNames,
triggers: ruleData.triggers, triggers,
actions: ruleData.actions actions
} }
try { try {
@@ -151,10 +207,11 @@ export const AutoScoreManager: React.FC = () => {
) )
form.setFieldsValue({ form.setFieldsValue({
name: '', name: '',
studentNames: '' studentNames: []
}) })
setEditingRuleId(null) setEditingRuleId(null)
setRuleData({ triggers: [], actions: [] }) setTriggerList([])
setActionList([])
fetchRules() fetchRules()
} else { } else {
messageApi.error( messageApi.error(
@@ -176,10 +233,22 @@ export const AutoScoreManager: React.FC = () => {
name: rule.name, name: rule.name,
studentNames: rule.studentNames studentNames: rule.studentNames
}) })
setRuleData({ setTriggerList(
triggers: rule.triggers || [], (rule.triggers || []).map((t, index) => ({
actions: rule.actions || [] id: index + 1,
}) eventName: t.eventName,
value: t.value || '',
relation: t.relation || 'AND'
}))
)
setActionList(
(rule.actions || []).map((a, index) => ({
id: index + 1,
eventName: a.eventName,
value: a.value || '',
reason: a.reason || ''
}))
)
} }
const handleDelete = async (ruleId: number) => { const handleDelete = async (ruleId: number) => {
@@ -239,7 +308,7 @@ export const AutoScoreManager: React.FC = () => {
const handleResetForm = () => { const handleResetForm = () => {
form.setFieldsValue({ form.setFieldsValue({
name: '', name: '',
studentNames: '' studentNames: []
}) })
setEditingRuleId(null) setEditingRuleId(null)
setTriggerList([]) setTriggerList([])
@@ -248,7 +317,7 @@ export const AutoScoreManager: React.FC = () => {
const handleAddTrigger = () => { const handleAddTrigger = () => {
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
const defaultTrigger = allTriggers.list[0] const defaultTrigger = TRIGGER_DEFINITIONS[0]
if (!defaultTrigger) { if (!defaultTrigger) {
messageApi.error(t('autoScore.noTriggerAvailable')) messageApi.error(t('autoScore.noTriggerAvailable'))
return return
@@ -264,25 +333,23 @@ export const AutoScoreManager: React.FC = () => {
]) ])
} }
const handleDeleteTrigger = (id: number) => { const handleRemoveTrigger = (id: number) => {
setTriggerList((prev) => prev.filter((t) => t.id !== id)) setTriggerList((prev) => prev.filter((t) => t.id !== id))
} }
const handleTriggerChange = (id: number, eventName: string) => { const handleTriggerChange = (
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, eventName, value: '' } : t))) id: number,
} field: keyof TriggerItem,
value: string | number | 'AND' | 'OR'
const handleTriggerValueChange = (id: number, value: string) => { ) => {
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value } : t))) setTriggerList((prev) =>
} prev.map((t) => (t.id === id ? { ...t, [field]: value } : t))
)
const handleTriggerRelationChange = (id: number, relation: 'AND' | 'OR') => {
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, relation } : t)))
} }
const handleAddAction = () => { const handleAddAction = () => {
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1 const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
const defaultAction = allActions.list[0] const defaultAction = ACTION_DEFINITIONS[0]
if (!defaultAction) { if (!defaultAction) {
messageApi.error(t('autoScore.noActionAvailable')) messageApi.error(t('autoScore.noActionAvailable'))
return return
@@ -298,20 +365,18 @@ export const AutoScoreManager: React.FC = () => {
]) ])
} }
const handleDeleteAction = (id: number) => { const handleRemoveAction = (id: number) => {
setActionList((prev) => prev.filter((a) => a.id !== id)) setActionList((prev) => prev.filter((a) => a.id !== id))
} }
const handleActionChange = (id: number, eventName: string) => { const handleActionChange = (
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, eventName, value: '' } : a))) id: number,
} field: keyof ActionItem,
value: string | number
const handleActionValueChange = (id: number, value: string) => { ) => {
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a))) setActionList((prev) =>
} prev.map((a) => (a.id === id ? { ...a, [field]: value } : a))
)
const handleActionReasonChange = (id: number, reason: string) => {
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, reason } : a)))
} }
const columns: ColumnsType<AutoScoreRule> = [ const columns: ColumnsType<AutoScoreRule> = [
@@ -340,10 +405,7 @@ export const AutoScoreManager: React.FC = () => {
if (!triggers || triggers.length === 0) { if (!triggers || triggers.length === 0) {
return <span>{t('common.none')}</span> return <span>{t('common.none')}</span>
} }
const triggerLabels = triggers.map((t) => { const triggerLabels = triggers.map((t) => getTriggerLabel(t.eventName))
const def = triggerRegistry.get(t.event)
return def?.label || t.event
})
return ( return (
<Tooltip title={triggerLabels.join(', ')}> <Tooltip title={triggerLabels.join(', ')}>
<span>{t('autoScore.triggerCount', { count: triggers.length })}</span> <span>{t('autoScore.triggerCount', { count: triggers.length })}</span>
@@ -360,10 +422,7 @@ export const AutoScoreManager: React.FC = () => {
if (!actions || actions.length === 0) { if (!actions || actions.length === 0) {
return <span>{t('common.none')}</span> return <span>{t('common.none')}</span>
} }
const actionLabels = actions.map((a) => { const actionLabels = actions.map((a) => getActionLabel(a.eventName))
const def = actionRegistry.get(a.event)
return def?.label || a.event
})
return ( return (
<Tooltip title={actionLabels.join(', ')}> <Tooltip title={actionLabels.join(', ')}>
<span>{t('autoScore.actionCount', { count: actions.length })}</span> <span>{t('autoScore.actionCount', { count: actions.length })}</span>
@@ -422,6 +481,127 @@ export const AutoScoreManager: React.FC = () => {
} }
] ]
const renderTriggerItem = (trigger: TriggerItem, index: number) => (
<div
key={trigger.id}
style={{
padding: '12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
backgroundColor: 'var(--ss-card-bg)',
marginBottom: '8px'
}}
>
<Space wrap>
{index > 0 && (
<Select
value={trigger.relation}
onChange={(value) => handleTriggerChange(trigger.id, 'relation', value)}
style={{ width: 80 }}
options={[
{ label: 'AND', value: 'AND' },
{ label: 'OR', value: 'OR' }
]}
/>
)}
<Select
value={trigger.eventName}
onChange={(value) => handleTriggerChange(trigger.id, 'eventName', value)}
style={{ width: 150 }}
options={TRIGGER_DEFINITIONS.map((d) => ({
label: t(d.labelKey),
value: d.eventName
}))}
/>
{trigger.eventName === 'interval_time_passed' && (
<InputNumber
value={trigger.value ? parseInt(trigger.value) : undefined}
onChange={(value) => handleTriggerChange(trigger.id, 'value', String(value || 0))}
placeholder={t('autoScore.minutesPlaceholder')}
min={1}
style={{ width: 120 }}
addonAfter={t('autoScore.minutes')}
/>
)}
{trigger.eventName === 'student_has_tag' && (
<Select
mode="multiple"
allowClear
value={trigger.value}
onChange={(value) => handleTriggerChange(trigger.id, 'value', value)}
onClick={fetchAllTags}
style={{ width: 380 }}
options={allTags.map((tag) => ({
label: tag.name,
value: tag.name
}))}
/>
)}
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveTrigger(trigger.id)}
/>
</Space>
</div>
)
const renderActionItem = (action: ActionItem) => (
<div
key={action.id}
style={{
padding: '12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
backgroundColor: 'var(--ss-card-bg)',
marginBottom: '8px'
}}
>
<Space wrap>
<Select
value={action.eventName}
onChange={(value) => handleActionChange(action.id, 'eventName', value)}
style={{ width: 150 }}
options={ACTION_DEFINITIONS.map((d) => ({
label: t(d.labelKey),
value: d.eventName
}))}
/>
{action.eventName === 'add_score' && (
<InputNumber
value={action.value ? parseInt(action.value) : undefined}
onChange={(value) => handleActionChange(action.id, 'value', String(value || 0))}
placeholder={t('autoScore.scorePlaceholder')}
min={-100}
max={100}
style={{ width: 120 }}
/>
)}
{action.eventName === 'add_tag' && (
<Input
value={action.value}
onChange={(e) => handleActionChange(action.id, 'value', e.target.value)}
placeholder={t('autoScore.tagNamePlaceholder')}
style={{ width: 200 }}
/>
)}
<Input
value={action.reason}
onChange={(e) => handleActionChange(action.id, 'reason', e.target.value)}
placeholder={t('autoScore.reasonPlaceholder')}
style={{ width: 200 }}
/>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveAction(action.id)}
/>
</Space>
</div>
)
return ( return (
<div style={{ padding: '24px' }}> <div style={{ padding: '24px' }}>
{contextHolder} {contextHolder}
@@ -447,21 +627,6 @@ export const AutoScoreManager: React.FC = () => {
/> />
</Form.Item> </Form.Item>
</div> </div>
<div style={{ marginTop: '16px' }}>
<RuleComponent initialData={ruleData} onChange={(data) => setRuleData(data)} />
</div>
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
<Button type="primary" onClick={handleSubmit}>
{editingRuleId !== null
? t('autoScore.updateAutomation')
: t('autoScore.addAutomation')}
</Button>
<Button onClick={handleResetForm}>
{editingRuleId !== null ? t('autoScore.cancelEdit') : t('autoScore.resetForm')}
</Button>
</div>
</Form> </Form>
</Card> </Card>
@@ -470,8 +635,9 @@ export const AutoScoreManager: React.FC = () => {
title={t('autoScore.whenTriggered')} title={t('autoScore.whenTriggered')}
> >
<Space orientation="vertical" style={{ width: '100%' }}> <Space orientation="vertical" style={{ width: '100%' }}>
{triggerItems} {/* {triggerList.map((trigger, index) => renderTriggerItem(trigger, index))}
<Button */} <RuleComponent />
<Button
type="dashed" type="dashed"
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={handleAddTrigger} onClick={handleAddTrigger}
@@ -486,8 +652,8 @@ export const AutoScoreManager: React.FC = () => {
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }} style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
title={t('autoScore.triggeredActions')} title={t('autoScore.triggeredActions')}
> >
<Space orientation="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
{actionItems} {actionList.map((action) => renderActionItem(action))}
<Button <Button
type="dashed" type="dashed"
icon={<PlusOutlined />} icon={<PlusOutlined />}
@@ -499,6 +665,17 @@ export const AutoScoreManager: React.FC = () => {
</Space> </Space>
</Card> </Card>
<div style={{ marginBottom: '24px', display: 'flex', gap: '12px' }}>
<Button type="primary" onClick={handleSubmit}>
{editingRuleId !== null
? t('autoScore.updateAutomation')
: t('autoScore.addAutomation')}
</Button>
<Button onClick={handleResetForm}>
{editingRuleId !== null ? t('autoScore.cancelEdit') : t('autoScore.resetForm')}
</Button>
</div>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}> <Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Table <Table
dataSource={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)} dataSource={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
@@ -524,4 +701,4 @@ export const AutoScoreManager: React.FC = () => {
</Card> </Card>
</div> </div>
) )
} }
@@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next'
import { Button, Segmented, Input, Tag, message, Typography, InputNumber } from 'antd' import { Button, Segmented, Input, Tag, message, Typography, InputNumber } from 'antd'
import { PlusOutlined, UploadOutlined, FileExcelOutlined } from '@ant-design/icons' import { PlusOutlined, UploadOutlined, FileExcelOutlined } from '@ant-design/icons'
import { OOBEBackground } from './OOBEBackground' import { OOBEBackground } from './OOBEBackground'
import { useTheme } from '../contexts/ThemeContext' import { useTheme } from '../../contexts/ThemeContext'
import { changeLanguage, AppLanguage, languageOptions } from '../i18n' import { changeLanguage, AppLanguage, languageOptions } from '../../i18n'
import type { themeConfig } from '../../../preload/types' import type { themeConfig } from '../../../../preload/types'
import logoSvg from '../assets/logoHD.svg' import logoSvg from '../../assets/logoHD.svg'
interface oobeProps { interface oobeProps {
visible: boolean visible: boolean
@@ -1,5 +1,6 @@
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 type { AutoScoreAction } from './ruleBuilderUtils' import type { AutoScoreAction } from './ruleBuilderUtils'
interface ActionComponentProps { interface ActionComponentProps {
@@ -9,19 +10,26 @@ interface ActionComponentProps {
onChange: (index: number, field: keyof AutoScoreAction, value: any) => void onChange: (index: number, field: keyof AutoScoreAction, value: any) => 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,
onRemove, onRemove,
onChange onChange
}) => { }) => {
const { t } = useTranslation()
const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => { const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => {
onChange(index, field, value) onChange(index, field, value)
} }
return ( return (
<div> <div>
<Space direction="vertical" style={{ width: '100%' }}> <Space orientation="vertical" style={{ width: '100%' }}>
{actions.map((action, index) => ( {actions.map((action, index) => (
<div <div
key={index} key={index}
@@ -29,7 +37,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
padding: '12px', padding: '12px',
border: '1px solid #d9d9d9', border: '1px solid #d9d9d9',
borderRadius: '6px', borderRadius: '6px',
backgroundColor: '#fafafa' backgroundColor: 'var(--ss-card-bg)'
}} }}
> >
<Space style={{ width: '100%' }} wrap> <Space style={{ width: '100%' }} wrap>
@@ -37,12 +45,9 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
value={action.event} value={action.event}
onChange={(value) => handleActionChange(index, 'event', value)} onChange={(value) => handleActionChange(index, 'event', value)}
style={{ width: 150 }} style={{ width: 150 }}
options={Object.entries({ options={ACTION_DEFINITIONS.map((d) => ({
add_score: { label: '加分', description: '为学生增加分数' }, label: t(d.labelKey),
add_tag: { label: '添加标签', description: '为学生添加标签' } value: d.eventName
}).map(([key, val]) => ({
label: val.label,
value: key
}))} }))}
/> />
@@ -50,7 +55,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
<InputNumber <InputNumber
value={action.value ? parseInt(action.value) : 0} value={action.value ? parseInt(action.value) : 0}
onChange={(value) => handleActionChange(index, 'value', String(value || 0))} onChange={(value) => handleActionChange(index, 'value', String(value || 0))}
placeholder="分数" placeholder={t('autoScore.scoreLabel')}
min={-100} min={-100}
max={100} max={100}
style={{ width: 120 }} style={{ width: 120 }}
@@ -61,7 +66,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
<Input <Input
value={action.value} value={action.value}
onChange={(e) => handleActionChange(index, 'value', e.target.value)} onChange={(e) => handleActionChange(index, 'value', e.target.value)}
placeholder="标签名称" placeholder={t('autoScore.tagNameLabel')}
style={{ width: 200 }} style={{ width: 200 }}
/> />
)} )}
@@ -69,7 +74,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
<Input <Input
value={action.reason} value={action.reason}
onChange={(e) => handleActionChange(index, 'reason', e.target.value)} onChange={(e) => handleActionChange(index, 'reason', e.target.value)}
placeholder="操作说明(可选)" placeholder={t('autoScore.operationNoteLabel')}
style={{ flex: 1, minWidth: 200 }} style={{ flex: 1, minWidth: 200 }}
/> />
@@ -84,11 +89,11 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
))} ))}
<Button type="dashed" icon={<PlusOutlined />} onClick={onAdd} block> <Button type="dashed" icon={<PlusOutlined />} onClick={onAdd} block>
{t('autoScore.addOperation')}
</Button> </Button>
</Space> </Space>
</div> </div>
) )
} }
export default ActionComponent export default ActionComponent
@@ -17,36 +17,46 @@ export interface AutoScoreRuleData {
actions: AutoScoreAction[] actions: AutoScoreAction[]
} }
export const TRIGGER_TYPES = { // i18n key definitions for triggers and actions
export const TRIGGER_TYPE_KEYS = {
interval_time_passed: { interval_time_passed: {
label: '间隔时间', labelKey: 'autoScore.triggerIntervalTime',
description: '每隔指定时间自动触发' descriptionKey: 'triggers.intervalTime.description'
}, },
student_has_tag: { student_has_tag: {
label: '学生标签', labelKey: 'autoScore.triggerStudentTag',
description: '当学生拥有指定标签时触发' descriptionKey: 'triggers.studentTag.description'
} }
} }
export const ACTION_TYPES = { export const ACTION_TYPE_KEYS = {
add_score: { add_score: {
label: '加分', labelKey: 'autoScore.actionAddScore',
description: '为学生增加分数' descriptionKey: 'actions.addScore.description'
}, },
add_tag: { add_tag: {
label: '添加标签', labelKey: 'autoScore.actionAddTag',
description: '为学生添加标签' descriptionKey: 'actions.addTag.description'
} }
} }
export const fields: Field[] = [ // Function to get fields with i18n support
{ name: 'interval_time_passed', label: '间隔时间(分钟)', placeholder: '例如:1440' }, export const getFields = (t: (key: string) => string): Field[] => [
{ name: 'student_has_tag', label: '学生标签', placeholder: '例如:优秀学生,班干部' } {
name: 'interval_time_passed',
label: t('autoScore.triggerIntervalTime'),
placeholder: t('autoScore.intervalMinutesPlaceholder')
},
{
name: 'student_has_tag',
label: t('autoScore.triggerStudentTag'),
placeholder: t('autoScore.tagNamesPlaceholder')
}
] ]
export const operators: Operator[] = [ export const operators: Operator[] = [
{ name: '=', label: '等于' }, { name: '=', label: '=' },
{ name: 'contains', label: '包含' } { name: 'contains', label: 'contains' }
] ]
export const defaultQuery: RuleGroupType = { export const defaultQuery: RuleGroupType = {
@@ -106,4 +116,4 @@ export function autoScoreRuleToQuery(ruleData: AutoScoreRuleData): RuleGroupType
combinator: currentCombinator, combinator: currentCombinator,
rules: rules.length > 0 ? rules : defaultQuery.rules rules: rules.length > 0 ? rules : defaultQuery.rules
} }
} }
@@ -1,7 +1,8 @@
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 { import {
fields, getFields,
operators, operators,
defaultQuery, defaultQuery,
queryToAutoScoreRule, queryToAutoScoreRule,
@@ -20,6 +21,7 @@ interface RuleComponentProps {
} }
export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onChange }) => { export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onChange }) => {
const { t } = useTranslation()
const [query, setQuery] = useState<RuleGroupType>( const [query, setQuery] = useState<RuleGroupType>(
initialData ? autoScoreRuleToQuery(initialData) : defaultQuery initialData ? autoScoreRuleToQuery(initialData) : defaultQuery
) )
@@ -55,9 +57,9 @@ export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onCha
return ( return (
<div> <div>
<Card title="触发条件" style={{ marginBottom: '16px' }}> <Card title={t('autoScore.triggerCondition')} style={{ marginBottom: '16px' }}>
<QueryBuilder <QueryBuilder
fields={fields} fields={getFields(t)}
operators={operators} operators={operators}
query={query} query={query}
onQueryChange={handleQueryChange} onQueryChange={handleQueryChange}
@@ -67,7 +69,7 @@ export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onCha
</div> </div>
</Card> </Card>
<Card title="执行操作"> <Card title={t('autoScore.executeAction')}>
<ActionComponent <ActionComponent
actions={actions} actions={actions}
onAdd={handleAddAction} onAdd={handleAddAction}
@@ -79,4 +81,4 @@ export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onCha
) )
} }
export default RuleComponent export default RuleComponent
+19 -1
View File
@@ -495,7 +495,25 @@
"triggerCount": "{{count}} triggers", "triggerCount": "{{count}} triggers",
"actionCount": "{{count}} actions", "actionCount": "{{count}} actions",
"studentCount": "{{count}} students", "studentCount": "{{count}} students",
"studentPlaceholder": "Please select or search students (leave empty for all students)" "studentPlaceholder": "Please select or search students (leave empty for all students)",
"triggerCondition": "Trigger Condition",
"executeAction": "Execute Action",
"addOperation": "Add Operation",
"scoreLabel": "Score",
"tagNameLabel": "Tag Name",
"operationNoteLabel": "Operation Note (Optional)",
"intervalMinutesPlaceholder": "e.g. 1440",
"tagNamesPlaceholder": "e.g. excellent_student,class_monitor",
"triggerIntervalTime": "Interval Time",
"triggerStudentTag": "Student Tag",
"actionAddScore": "Add Score",
"actionAddTag": "Add Tag",
"minutesPlaceholder": "Enter minutes",
"minutes": "minutes",
"tagsPlaceholder": "Enter tag name",
"scorePlaceholder": "Enter score",
"tagNamePlaceholder": "Enter tag name",
"reasonPlaceholder": "Enter reason"
}, },
"triggers": { "triggers": {
"studentTag": { "studentTag": {
+20 -2
View File
@@ -246,7 +246,7 @@
"leaderboard": "排行榜", "leaderboard": "排行榜",
"settlements": "结算历史", "settlements": "结算历史",
"reasons": "理由管理", "reasons": "理由管理",
"autoScore": "自动分", "autoScore": "自动分",
"settings": "系统设置", "settings": "系统设置",
"remoteDb": "远程数据库", "remoteDb": "远程数据库",
"refreshStatus": "刷新状态", "refreshStatus": "刷新状态",
@@ -495,7 +495,25 @@
"triggerCount": "{{count}} 个触发器", "triggerCount": "{{count}} 个触发器",
"actionCount": "{{count}} 个行动", "actionCount": "{{count}} 个行动",
"studentCount": "{{count}} 名学生", "studentCount": "{{count}} 名学生",
"studentPlaceholder": "请选择或搜索学生(留空表示所有学生)" "studentPlaceholder": "请选择或搜索学生(留空表示所有学生)",
"triggerCondition": "触发条件",
"executeAction": "执行操作",
"addOperation": "添加操作",
"scoreLabel": "分数",
"tagNameLabel": "标签名称",
"operationNoteLabel": "操作说明(可选)",
"intervalMinutesPlaceholder": "例如:1440",
"tagNamesPlaceholder": "例如:优秀学生,班干部",
"triggerIntervalTime": "间隔时间",
"triggerStudentTag": "学生标签",
"actionAddScore": "加分",
"actionAddTag": "添加标签",
"minutesPlaceholder": "请输入分钟数",
"minutes": "分钟",
"tagsPlaceholder": "请输入标签名称",
"scorePlaceholder": "请输入分数",
"tagNamePlaceholder": "请输入标签名称",
"reasonPlaceholder": "请输入理由"
}, },
"triggers": { "triggers": {
"studentTag": { "studentTag": {