mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
Alapha:自动加分
This commit is contained in:
@@ -41,11 +41,13 @@
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"express": "^5.2.1",
|
||||
"i18next": "^25.8.14",
|
||||
"json-rules-engine": "^7.3.1",
|
||||
"mica-electron": "^1.5.16",
|
||||
"os": "^0.1.2",
|
||||
"pg": "^8.19.0",
|
||||
"pinyin-pro": "^3.27.0",
|
||||
"react-i18next": "^16.5.6",
|
||||
"react-querybuilder": "^8.14.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"typeorm": "^0.3.27",
|
||||
|
||||
Generated
+6
@@ -50,6 +50,9 @@ importers:
|
||||
i18next:
|
||||
specifier: ^25.8.14
|
||||
version: 25.8.14(typescript@5.9.3)
|
||||
json-rules-engine:
|
||||
specifier: ^7.3.1
|
||||
version: 7.3.1
|
||||
mica-electron:
|
||||
specifier: ^1.5.16
|
||||
version: 1.5.16
|
||||
@@ -63,6 +66,9 @@ importers:
|
||||
react-i18next:
|
||||
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)
|
||||
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:
|
||||
specifier: ^6.28.0
|
||||
version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HashRouter, useLocation, useNavigate, Routes, Route } from 'react-route
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import { ContentArea } from './components/ContentArea'
|
||||
import { OOBE } from './components/OOBE'
|
||||
import { OOBE } from './components/OOBE/OOBE'
|
||||
import { ThemeProvider, useTheme } from './contexts/ThemeContext'
|
||||
|
||||
function MainContent(): React.JSX.Element {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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 { allTriggers, allActions, triggerRegistry, actionRegistry } from './com.automatically'
|
||||
import type { TriggerItem, ActionItem } from './com.automatically/types'
|
||||
import TriggerItemComponent from './com.automatically/TriggerItem'
|
||||
import ActionItemComponent from './com.automatically/ActionItem'
|
||||
import RuleComponent from './autoScore/ruleComponent'
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Button,
|
||||
message,
|
||||
Table,
|
||||
@@ -21,38 +19,84 @@ import {
|
||||
} from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
|
||||
import { RuleComponent } from './autoScore/ruleComponent'
|
||||
import type { AutoScoreRuleData } from './autoScore/ruleBuilderUtils'
|
||||
|
||||
interface AutoScoreRule {
|
||||
id: number
|
||||
enabled: boolean
|
||||
name: string
|
||||
studentNames: string[]
|
||||
lastExecuted?: string
|
||||
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
||||
actions?: { event: string; value?: string; reason?: string }[]
|
||||
triggers?: TriggerItem[]
|
||||
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 {
|
||||
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 = () => {
|
||||
const { t } = useTranslation()
|
||||
const [rules, setRules] = useState<AutoScoreRule[]>([])
|
||||
const [allTags, setAllTags] = useState<TagItem[]>([])
|
||||
const [students, setStudents] = useState<{ id: number; name: string }[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState<number>(50)
|
||||
const [form] = Form.useForm()
|
||||
const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
|
||||
const [ruleData, setRuleData] = useState<AutoScoreRuleData>({
|
||||
triggers: [],
|
||||
actions: []
|
||||
})
|
||||
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
|
||||
const [actionList, setActionList] = useState<ActionItem[]>([])
|
||||
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 () => {
|
||||
if (!(window as any).api) return
|
||||
@@ -116,12 +160,24 @@ export const AutoScoreManager: React.FC = () => {
|
||||
|
||||
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 = {
|
||||
enabled: true,
|
||||
name: values.name,
|
||||
studentNames,
|
||||
triggers: ruleData.triggers,
|
||||
actions: ruleData.actions
|
||||
triggers,
|
||||
actions
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -151,10 +207,11 @@ export const AutoScoreManager: React.FC = () => {
|
||||
)
|
||||
form.setFieldsValue({
|
||||
name: '',
|
||||
studentNames: ''
|
||||
studentNames: []
|
||||
})
|
||||
setEditingRuleId(null)
|
||||
setRuleData({ triggers: [], actions: [] })
|
||||
setTriggerList([])
|
||||
setActionList([])
|
||||
fetchRules()
|
||||
} else {
|
||||
messageApi.error(
|
||||
@@ -176,10 +233,22 @@ export const AutoScoreManager: React.FC = () => {
|
||||
name: rule.name,
|
||||
studentNames: rule.studentNames
|
||||
})
|
||||
setRuleData({
|
||||
triggers: rule.triggers || [],
|
||||
actions: rule.actions || []
|
||||
})
|
||||
setTriggerList(
|
||||
(rule.triggers || []).map((t, index) => ({
|
||||
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) => {
|
||||
@@ -239,7 +308,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
const handleResetForm = () => {
|
||||
form.setFieldsValue({
|
||||
name: '',
|
||||
studentNames: ''
|
||||
studentNames: []
|
||||
})
|
||||
setEditingRuleId(null)
|
||||
setTriggerList([])
|
||||
@@ -248,7 +317,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
|
||||
const handleAddTrigger = () => {
|
||||
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) {
|
||||
messageApi.error(t('autoScore.noTriggerAvailable'))
|
||||
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))
|
||||
}
|
||||
|
||||
const handleTriggerChange = (id: number, eventName: string) => {
|
||||
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, eventName, value: '' } : t)))
|
||||
}
|
||||
|
||||
const handleTriggerValueChange = (id: number, value: string) => {
|
||||
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value } : t)))
|
||||
}
|
||||
|
||||
const handleTriggerRelationChange = (id: number, relation: 'AND' | 'OR') => {
|
||||
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, relation } : t)))
|
||||
const handleTriggerChange = (
|
||||
id: number,
|
||||
field: keyof TriggerItem,
|
||||
value: string | number | 'AND' | 'OR'
|
||||
) => {
|
||||
setTriggerList((prev) =>
|
||||
prev.map((t) => (t.id === id ? { ...t, [field]: value } : t))
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddAction = () => {
|
||||
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) {
|
||||
messageApi.error(t('autoScore.noActionAvailable'))
|
||||
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))
|
||||
}
|
||||
|
||||
const handleActionChange = (id: number, eventName: string) => {
|
||||
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, eventName, value: '' } : a)))
|
||||
}
|
||||
|
||||
const handleActionValueChange = (id: number, value: string) => {
|
||||
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a)))
|
||||
}
|
||||
|
||||
const handleActionReasonChange = (id: number, reason: string) => {
|
||||
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, reason } : a)))
|
||||
const handleActionChange = (
|
||||
id: number,
|
||||
field: keyof ActionItem,
|
||||
value: string | number
|
||||
) => {
|
||||
setActionList((prev) =>
|
||||
prev.map((a) => (a.id === id ? { ...a, [field]: value } : a))
|
||||
)
|
||||
}
|
||||
|
||||
const columns: ColumnsType<AutoScoreRule> = [
|
||||
@@ -340,10 +405,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
if (!triggers || triggers.length === 0) {
|
||||
return <span>{t('common.none')}</span>
|
||||
}
|
||||
const triggerLabels = triggers.map((t) => {
|
||||
const def = triggerRegistry.get(t.event)
|
||||
return def?.label || t.event
|
||||
})
|
||||
const triggerLabels = triggers.map((t) => getTriggerLabel(t.eventName))
|
||||
return (
|
||||
<Tooltip title={triggerLabels.join(', ')}>
|
||||
<span>{t('autoScore.triggerCount', { count: triggers.length })}</span>
|
||||
@@ -360,10 +422,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
if (!actions || actions.length === 0) {
|
||||
return <span>{t('common.none')}</span>
|
||||
}
|
||||
const actionLabels = actions.map((a) => {
|
||||
const def = actionRegistry.get(a.event)
|
||||
return def?.label || a.event
|
||||
})
|
||||
const actionLabels = actions.map((a) => getActionLabel(a.eventName))
|
||||
return (
|
||||
<Tooltip title={actionLabels.join(', ')}>
|
||||
<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 (
|
||||
<div style={{ padding: '24px' }}>
|
||||
{contextHolder}
|
||||
@@ -447,21 +627,6 @@ export const AutoScoreManager: React.FC = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
@@ -470,7 +635,8 @@ export const AutoScoreManager: React.FC = () => {
|
||||
title={t('autoScore.whenTriggered')}
|
||||
>
|
||||
<Space orientation="vertical" style={{ width: '100%' }}>
|
||||
{triggerItems}
|
||||
{/* {triggerList.map((trigger, index) => renderTriggerItem(trigger, index))}
|
||||
*/} <RuleComponent />
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
@@ -486,8 +652,8 @@ export const AutoScoreManager: React.FC = () => {
|
||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||
title={t('autoScore.triggeredActions')}
|
||||
>
|
||||
<Space orientation="vertical" style={{ width: '100%' }}>
|
||||
{actionItems}
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{actionList.map((action) => renderActionItem(action))}
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
@@ -499,6 +665,17 @@ export const AutoScoreManager: React.FC = () => {
|
||||
</Space>
|
||||
</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)' }}>
|
||||
<Table
|
||||
dataSource={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Button, Segmented, Input, Tag, message, Typography, InputNumber } from 'antd'
|
||||
import { PlusOutlined, UploadOutlined, FileExcelOutlined } from '@ant-design/icons'
|
||||
import { OOBEBackground } from './OOBEBackground'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import { changeLanguage, AppLanguage, languageOptions } from '../i18n'
|
||||
import type { themeConfig } from '../../../preload/types'
|
||||
import logoSvg from '../assets/logoHD.svg'
|
||||
import { useTheme } from '../../contexts/ThemeContext'
|
||||
import { changeLanguage, AppLanguage, languageOptions } from '../../i18n'
|
||||
import type { themeConfig } from '../../../../preload/types'
|
||||
import logoSvg from '../../assets/logoHD.svg'
|
||||
|
||||
interface oobeProps {
|
||||
visible: boolean
|
||||
@@ -1,5 +1,6 @@
|
||||
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'
|
||||
|
||||
interface ActionComponentProps {
|
||||
@@ -9,19 +10,26 @@ interface ActionComponentProps {
|
||||
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> = ({
|
||||
actions,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => {
|
||||
onChange(index, field, value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space orientation="vertical" style={{ width: '100%' }}>
|
||||
{actions.map((action, index) => (
|
||||
<div
|
||||
key={index}
|
||||
@@ -29,7 +37,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
|
||||
padding: '12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#fafafa'
|
||||
backgroundColor: 'var(--ss-card-bg)'
|
||||
}}
|
||||
>
|
||||
<Space style={{ width: '100%' }} wrap>
|
||||
@@ -37,12 +45,9 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
|
||||
value={action.event}
|
||||
onChange={(value) => handleActionChange(index, 'event', value)}
|
||||
style={{ width: 150 }}
|
||||
options={Object.entries({
|
||||
add_score: { label: '加分', description: '为学生增加分数' },
|
||||
add_tag: { label: '添加标签', description: '为学生添加标签' }
|
||||
}).map(([key, val]) => ({
|
||||
label: val.label,
|
||||
value: key
|
||||
options={ACTION_DEFINITIONS.map((d) => ({
|
||||
label: t(d.labelKey),
|
||||
value: d.eventName
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -50,7 +55,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
|
||||
<InputNumber
|
||||
value={action.value ? parseInt(action.value) : 0}
|
||||
onChange={(value) => handleActionChange(index, 'value', String(value || 0))}
|
||||
placeholder="分数"
|
||||
placeholder={t('autoScore.scoreLabel')}
|
||||
min={-100}
|
||||
max={100}
|
||||
style={{ width: 120 }}
|
||||
@@ -61,7 +66,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
|
||||
<Input
|
||||
value={action.value}
|
||||
onChange={(e) => handleActionChange(index, 'value', e.target.value)}
|
||||
placeholder="标签名称"
|
||||
placeholder={t('autoScore.tagNameLabel')}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
)}
|
||||
@@ -69,7 +74,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
|
||||
<Input
|
||||
value={action.reason}
|
||||
onChange={(e) => handleActionChange(index, 'reason', e.target.value)}
|
||||
placeholder="操作说明(可选)"
|
||||
placeholder={t('autoScore.operationNoteLabel')}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
/>
|
||||
|
||||
@@ -84,7 +89,7 @@ export const ActionComponent: React.FC<ActionComponentProps> = ({
|
||||
))}
|
||||
|
||||
<Button type="dashed" icon={<PlusOutlined />} onClick={onAdd} block>
|
||||
添加操作
|
||||
{t('autoScore.addOperation')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@@ -17,36 +17,46 @@ export interface AutoScoreRuleData {
|
||||
actions: AutoScoreAction[]
|
||||
}
|
||||
|
||||
export const TRIGGER_TYPES = {
|
||||
// i18n key definitions for triggers and actions
|
||||
export const TRIGGER_TYPE_KEYS = {
|
||||
interval_time_passed: {
|
||||
label: '间隔时间',
|
||||
description: '每隔指定时间自动触发'
|
||||
labelKey: 'autoScore.triggerIntervalTime',
|
||||
descriptionKey: 'triggers.intervalTime.description'
|
||||
},
|
||||
student_has_tag: {
|
||||
label: '学生标签',
|
||||
description: '当学生拥有指定标签时触发'
|
||||
labelKey: 'autoScore.triggerStudentTag',
|
||||
descriptionKey: 'triggers.studentTag.description'
|
||||
}
|
||||
}
|
||||
|
||||
export const ACTION_TYPES = {
|
||||
export const ACTION_TYPE_KEYS = {
|
||||
add_score: {
|
||||
label: '加分',
|
||||
description: '为学生增加分数'
|
||||
labelKey: 'autoScore.actionAddScore',
|
||||
descriptionKey: 'actions.addScore.description'
|
||||
},
|
||||
add_tag: {
|
||||
label: '添加标签',
|
||||
description: '为学生添加标签'
|
||||
labelKey: 'autoScore.actionAddTag',
|
||||
descriptionKey: 'actions.addTag.description'
|
||||
}
|
||||
}
|
||||
|
||||
export const fields: Field[] = [
|
||||
{ name: 'interval_time_passed', label: '间隔时间(分钟)', placeholder: '例如:1440' },
|
||||
{ name: 'student_has_tag', label: '学生标签', placeholder: '例如:优秀学生,班干部' }
|
||||
// Function to get fields with i18n support
|
||||
export const getFields = (t: (key: string) => string): Field[] => [
|
||||
{
|
||||
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[] = [
|
||||
{ name: '=', label: '等于' },
|
||||
{ name: 'contains', label: '包含' }
|
||||
{ name: '=', label: '=' },
|
||||
{ name: 'contains', label: 'contains' }
|
||||
]
|
||||
|
||||
export const defaultQuery: RuleGroupType = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { formatQuery, QueryBuilder, RuleGroupType } from 'react-querybuilder'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
fields,
|
||||
getFields,
|
||||
operators,
|
||||
defaultQuery,
|
||||
queryToAutoScoreRule,
|
||||
@@ -20,6 +21,7 @@ interface RuleComponentProps {
|
||||
}
|
||||
|
||||
export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const [query, setQuery] = useState<RuleGroupType>(
|
||||
initialData ? autoScoreRuleToQuery(initialData) : defaultQuery
|
||||
)
|
||||
@@ -55,9 +57,9 @@ export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onCha
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card title="触发条件" style={{ marginBottom: '16px' }}>
|
||||
<Card title={t('autoScore.triggerCondition')} style={{ marginBottom: '16px' }}>
|
||||
<QueryBuilder
|
||||
fields={fields}
|
||||
fields={getFields(t)}
|
||||
operators={operators}
|
||||
query={query}
|
||||
onQueryChange={handleQueryChange}
|
||||
@@ -67,7 +69,7 @@ export const RuleComponent: React.FC<RuleComponentProps> = ({ initialData, onCha
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="执行操作">
|
||||
<Card title={t('autoScore.executeAction')}>
|
||||
<ActionComponent
|
||||
actions={actions}
|
||||
onAdd={handleAddAction}
|
||||
|
||||
@@ -495,7 +495,25 @@
|
||||
"triggerCount": "{{count}} triggers",
|
||||
"actionCount": "{{count}} actions",
|
||||
"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": {
|
||||
"studentTag": {
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
"leaderboard": "排行榜",
|
||||
"settlements": "结算历史",
|
||||
"reasons": "理由管理",
|
||||
"autoScore": "自动评分",
|
||||
"autoScore": "自动加分",
|
||||
"settings": "系统设置",
|
||||
"remoteDb": "远程数据库",
|
||||
"refreshStatus": "刷新状态",
|
||||
@@ -495,7 +495,25 @@
|
||||
"triggerCount": "{{count}} 个触发器",
|
||||
"actionCount": "{{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": {
|
||||
"studentTag": {
|
||||
|
||||
Reference in New Issue
Block a user