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",
"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",
+6
View File
@@ -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)
+1 -1
View File
@@ -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 {
+252 -75
View File
@@ -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,8 +635,9 @@ export const AutoScoreManager: React.FC = () => {
title={t('autoScore.whenTriggered')}
>
<Space orientation="vertical" style={{ width: '100%' }}>
{triggerItems}
<Button
{/* {triggerList.map((trigger, index) => renderTriggerItem(trigger, index))}
*/} <RuleComponent />
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={handleAddTrigger}
@@ -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}
+19 -1
View File
@@ -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": {
+20 -2
View File
@@ -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": {