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",
|
"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",
|
||||||
|
|||||||
Generated
+6
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user