This commit is contained in:
Fox_block
2026-03-14 18:38:57 +08:00
parent ccdda86f43
commit 94d853f9d1
8 changed files with 135 additions and 120 deletions
+12
View File
@@ -0,0 +1,12 @@
---
alwaysApply: false
description: 任务完成前的检查
---
1.每次完成任务之前必须跑一遍测试(比如说npm系的typecheck/lint
2.每次完成任务之前必须跑一遍格式化(比如说npm系的prettier
3.任务完成时详细输出你所做的事情和需要用户确认的问题(如有)
4.每次规划任务必须按照步骤来,每个步骤结束后遵循第三条规定。
@@ -1,12 +1,11 @@
import { useState, useEffect } from 'react'
import { HolderOutlined, DeleteOutlined } from '@ant-design/icons'
import { HolderOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import RuleComponent from './autoScore/ruleComponent'
import {
Card,
Form,
Input,
InputNumber,
Button,
message,
Table,
@@ -297,50 +296,6 @@ export const AutoScoreManager: React.FC = () => {
setActionList([])
}
const handleAddTrigger = () => {
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
const defaultTrigger = TRIGGER_DEFINITIONS[0]
if (!defaultTrigger) {
messageApi.error(t('autoScore.noTriggerAvailable'))
return
}
setTriggerList((prev) => [
...prev,
{
id: nextId,
eventName: defaultTrigger.eventName,
value: '',
relation: 'AND'
}
])
}
const handleAddAction = () => {
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
const defaultAction = ACTION_DEFINITIONS[0]
if (!defaultAction) {
messageApi.error(t('autoScore.noActionAvailable'))
return
}
setActionList((prev) => [
...prev,
{
id: nextId,
eventName: defaultAction.eventName,
value: '',
reason: ''
}
])
}
const handleRemoveAction = (id: number) => {
setActionList((prev) => prev.filter((a) => a.id !== id))
}
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> = [
{
key: 'drag',
@@ -443,61 +398,6 @@ export const AutoScoreManager: React.FC = () => {
}
]
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}
+58 -3
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Segmented, Input, Tag, message, Typography, InputNumber } from 'antd'
import { Button, Segmented, Input, Tag, message, Typography, InputNumber, Space } from 'antd'
import { PlusOutlined, UploadOutlined, FileExcelOutlined } from '@ant-design/icons'
import { OOBEBackground } from './OOBEBackground'
import { useTheme } from '../../contexts/ThemeContext'
@@ -13,7 +13,7 @@ interface oobeProps {
onComplete: () => void
}
type oobeStep = 'language' | 'theme' | 'students' | 'reasons' | 'start'
type oobeStep = 'language' | 'theme' | 'password' | 'students' | 'reasons' | 'start'
interface studentItem {
name: string
@@ -86,10 +86,12 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
const [reasons, setReasons] = useState<reasonItem[]>([])
const [newReasonContent, setNewReasonContent] = useState('')
const [newReasonDelta, setNewReasonDelta] = useState(1)
const [adminPassword, setAdminPassword] = useState('')
const [pointsPassword, setPointsPassword] = useState('')
const fileInputRef = useRef<HTMLInputElement>(null)
const steps: oobeStep[] = ['language', 'theme', 'students', 'reasons', 'start']
const steps: oobeStep[] = ['language', 'theme', 'password', 'students', 'reasons', 'start']
const stepIndex = steps.indexOf(currentStep) + 1
const totalSteps = steps.length
@@ -315,6 +317,13 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
})
}
if (adminPassword || pointsPassword) {
await (window as any).api.authSetPasswords({
adminPassword: adminPassword || null,
pointsPassword: pointsPassword || null
})
}
const res = await (window as any).api.setSetting('is_wizard_completed', true)
if (!res?.success) throw new Error(res?.message || 'failed')
@@ -436,6 +445,52 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
</div>
)
case 'password':
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Typography.Text type="secondary">
{t('oobe.steps.password.description')}
</Typography.Text>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Typography.Text strong>{t('oobe.steps.password.adminPassword')}</Typography.Text>
<Typography.Text
type="secondary"
style={{ display: 'block', fontSize: 12, marginTop: 4 }}
>
{t('oobe.steps.password.adminPasswordHint')}
</Typography.Text>
<Input.Password
value={adminPassword}
onChange={(e) => setAdminPassword(e.target.value)}
placeholder={t('oobe.steps.password.passwordPlaceholder')}
maxLength={6}
style={{ marginTop: 8 }}
/>
</div>
<div>
<Typography.Text strong>{t('oobe.steps.password.pointsPassword')}</Typography.Text>
<Typography.Text
type="secondary"
style={{ display: 'block', fontSize: 12, marginTop: 4 }}
>
{t('oobe.steps.password.pointsPasswordHint')}
</Typography.Text>
<Input.Password
value={pointsPassword}
onChange={(e) => setPointsPassword(e.target.value)}
placeholder={t('oobe.steps.password.passwordPlaceholder')}
maxLength={6}
style={{ marginTop: 8 }}
/>
</div>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{t('oobe.steps.password.hint')}
</Typography.Text>
</Space>
</div>
)
case 'students':
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
@@ -1,8 +1,20 @@
import { Space, Input, InputNumber, Select, Button } from 'antd'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import type { ActionItem } from './types'
import { ACTION_DEFINITIONS, SCORE_RANGE } from './constants'
const SCORE_RANGE = { MIN: -999, MAX: 999 }
const ACTION_DEFINITIONS = [
{ eventName: 'add_score', labelKey: 'autoScore.actionAddScore' },
{ eventName: 'add_tag', labelKey: 'autoScore.actionAddTag' }
]
interface ActionItem {
id: number
eventName: string
value?: string
reason?: string
}
interface ActionComponentProps {
actions: ActionItem[]
@@ -1,5 +1,7 @@
.queryBuilder {
--querybuilder-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
--querybuilder-font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif;
--querybuilder-background: var(--ss-card-bg, #ffffff);
--querybuilder-color: var(--ss-text-main, rgba(0, 0, 0, 0.85));
--querybuilder-border-radius: 6px;
@@ -21,7 +21,7 @@ export interface AutoScoreRuleData {
actions: AutoScoreAction[]
}
export const validator = (r: RuleType) => !!r.value;
export const validator = (r: RuleType) => !!r.value
export const getFields = (t: (key: string) => string): Field[] => [
{
@@ -31,18 +31,30 @@ export const getFields = (t: (key: string) => string): Field[] => [
valueEditorType: 'multiselect',
values: tags.map((tag) => tag.name),
defaultValue: tags.length > 0 ? [tags[0].name] : [],
operators: defaultOperators.filter((op) => op.name === 'in'),
operators: defaultOperators.filter((op) => op.name === 'in')
},
{
name: 'interval_time_passed',
label: t('triggers.intervalTime.label'),
matchModes: ['all'],
subproperties: [
{ name: 'month', label: t('triggers.intervalTime.monthLabel'), inputType: 'number', datatype: 'month', operators: ['='] },
/* { name: 'week', label: t('triggers.intervalTime.weekLabel'), inputType: 'week', datatype: 'week', operators: ['='] },
*/ { name: 'time', label: t('triggers.intervalTime.timeLabel'), inputType: 'time', datatype: 'time', operators: ['='] },
],
{
name: 'month',
label: t('triggers.intervalTime.monthLabel'),
inputType: 'number',
datatype: 'month',
operators: ['=']
},
/* { name: 'week', label: t('triggers.intervalTime.weekLabel'), inputType: 'week', datatype: 'week', operators: ['='] },
*/ {
name: 'time',
label: t('triggers.intervalTime.timeLabel'),
inputType: 'time',
datatype: 'time',
operators: ['=']
}
]
}
]
export const defaultQuery: RuleGroupType = {
@@ -50,7 +62,7 @@ export const defaultQuery: RuleGroupType = {
rules: [{ field: 'interval_time_passed', operator: '=', value: '1440' }]
}
export function queryToAutoScoreRule(query: RuleGroupType): AutoScoreRuleData {
export function queryToAutoScoreRule(_query: RuleGroupType): AutoScoreRuleData {
const triggers: AutoScoreTrigger[] = []
/* const processRuleGroup = (group: RuleGroupType, relation: 'AND' | 'OR' = 'AND') => {
+11
View File
@@ -50,6 +50,16 @@
"title": "Set Theme",
"description": "Choose your preferred interface appearance",
"mode": "Mode",
"password": {
"title": "Set Password",
"description": "Set login password to protect your data (optional)",
"adminPassword": "Admin Password",
"adminPasswordHint": "Full access to all features, 6 digits",
"pointsPassword": "Points Password",
"pointsPasswordHint": "Points operations only, 6 digits",
"passwordPlaceholder": "Enter 6 digits",
"hint": "Leave empty to skip, you can configure in settings later"
},
"lightMode": "Light",
"darkMode": "Dark",
"primaryColor": "Primary Color",
@@ -111,6 +121,7 @@
"language": "Language",
"languageHint": "Select interface display language",
"theme": "Theme",
"password": "Set Password",
"themeMode": "Mode",
"lightMode": "Light",
"darkMode": "Dark",
+11
View File
@@ -50,6 +50,16 @@
"title": "设置主题",
"description": "选择您喜欢的界面外观",
"mode": "模式",
"password": {
"title": "设置密码",
"description": "设置登录密码以保护您的数据(可选)",
"adminPassword": "管理密码",
"adminPasswordHint": "可管理所有功能,6位数字",
"pointsPassword": "积分密码",
"pointsPasswordHint": "仅可操作积分,6位数字",
"passwordPlaceholder": "请输入6位数字",
"hint": "留空则不设置密码,之后可在设置中配置"
},
"lightMode": "浅色",
"darkMode": "深色",
"primaryColor": "主色",
@@ -111,6 +121,7 @@
"language": "语言",
"languageHint": "选择界面显示语言",
"theme": "主题",
"password": "设置密码",
"themeMode": "模式",
"lightMode": "浅色",
"darkMode": "深色",