mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
NEW: 自动加分test
This commit is contained in:
@@ -41,6 +41,7 @@ function MainContent(): React.JSX.Element {
|
||||
if (p.startsWith('/leaderboard')) return 'leaderboard'
|
||||
if (p.startsWith('/settlements')) return 'settlements'
|
||||
if (p.startsWith('/reasons')) return 'reasons'
|
||||
if (p.startsWith('/auto-score')) return 'auto-score'
|
||||
if (p.startsWith('/settings')) return 'settings'
|
||||
return 'home'
|
||||
}, [location.pathname])
|
||||
@@ -103,6 +104,7 @@ function MainContent(): React.JSX.Element {
|
||||
if (key === 'leaderboard') navigate('/leaderboard')
|
||||
if (key === 'settlements') navigate('/settlements')
|
||||
if (key === 'reasons') navigate('/reasons')
|
||||
if (key === 'auto-score') navigate('/auto-score')
|
||||
if (key === 'settings') navigate('/settings')
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Button,
|
||||
MessagePlugin,
|
||||
Table,
|
||||
PrimaryTableCol,
|
||||
Tag,
|
||||
Space,
|
||||
Switch,
|
||||
Popconfirm
|
||||
} from 'tdesign-react'
|
||||
|
||||
interface AutoScoreRule {
|
||||
id: number
|
||||
enabled: boolean
|
||||
name: string
|
||||
intervalMinutes: number
|
||||
studentNames: string[]
|
||||
scoreValue: number
|
||||
reason: string
|
||||
lastExecuted?: string
|
||||
}
|
||||
|
||||
interface AutoScoreRuleFormValues {
|
||||
name: string
|
||||
intervalMinutes: number
|
||||
studentNames: string
|
||||
scoreValue: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
export const AutoScoreManager: React.FC = () => {
|
||||
const [rules, setRules] = useState<AutoScoreRule[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
|
||||
|
||||
const fetchRules = async () => {
|
||||
if (!(window as any).api) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
// 权限检查:确保当前为 admin
|
||||
try {
|
||||
const authRes = await (window as any).api.authGetStatus()
|
||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||
MessagePlugin.error('需要管理员权限以查看自动加分规则,请先登录管理员账号')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果权限检查失败,继续让后端返回更明确的错误
|
||||
console.warn('Auth check failed', e)
|
||||
}
|
||||
|
||||
const res = await (window as any).api.invoke('auto-score:getRules', {})
|
||||
if (res.success) {
|
||||
setRules(res.data)
|
||||
} else {
|
||||
MessagePlugin.error(res.message || '获取规则失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch auto score rules:', error)
|
||||
MessagePlugin.error('获取规则失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchRules()
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!(window as any).api) return
|
||||
|
||||
// 修复类型转换错误,使用更安全的方式获取表单值
|
||||
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues
|
||||
|
||||
if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
|
||||
MessagePlugin.warning('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
// 解析学生姓名列表(以逗号分隔)
|
||||
const studentNames = values.studentNames
|
||||
? values.studentNames.split(',').map(name => name.trim()).filter(name => name)
|
||||
: []
|
||||
|
||||
const ruleData = {
|
||||
enabled: true, // 默认启用新规则
|
||||
name: values.name,
|
||||
intervalMinutes: values.intervalMinutes,
|
||||
studentNames,
|
||||
scoreValue: values.scoreValue,
|
||||
reason: values.reason || `自动化加分 - ${values.name}`
|
||||
}
|
||||
|
||||
// 权限检查:仅管理员可创建/更新规则
|
||||
try {
|
||||
const authRes = await (window as any).api.authGetStatus()
|
||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||
MessagePlugin.error('需要管理员权限以创建或更新自动加分规则')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Auth check failed', e)
|
||||
}
|
||||
|
||||
try {
|
||||
let res
|
||||
if (editingRuleId !== null) {
|
||||
// 更新现有规则
|
||||
res = await (window as any).api.invoke('auto-score:updateRule', {
|
||||
id: editingRuleId,
|
||||
...ruleData
|
||||
})
|
||||
} else {
|
||||
// 创建新规则
|
||||
res = await (window as any).api.invoke('auto-score:addRule', ruleData)
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
MessagePlugin.success(editingRuleId !== null ? '规则更新成功' : '规则创建成功')
|
||||
form.reset()
|
||||
setEditingRuleId(null)
|
||||
fetchRules() // 刷新规则列表
|
||||
} else {
|
||||
MessagePlugin.error(res.message || (editingRuleId !== null ? '更新规则失败' : '创建规则失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to submit auto score rule:', error)
|
||||
MessagePlugin.error(editingRuleId !== null ? '更新规则失败' : '创建规则失败')
|
||||
}
|
||||
}
|
||||
const handleEdit = (rule: AutoScoreRule) => {
|
||||
setEditingRuleId(rule.id)
|
||||
form.setFieldsValue({
|
||||
name: rule.name,
|
||||
intervalMinutes: rule.intervalMinutes,
|
||||
studentNames: rule.studentNames.join(', '),
|
||||
scoreValue: rule.scoreValue,
|
||||
reason: rule.reason
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (ruleId: number) => {
|
||||
if (!(window as any).api) return
|
||||
// 权限检查
|
||||
try {
|
||||
const authRes = await (window as any).api.authGetStatus()
|
||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||
MessagePlugin.error('需要管理员权限以删除自动加分规则')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Auth check failed', e)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId)
|
||||
if (res.success) {
|
||||
MessagePlugin.success('规则删除成功')
|
||||
fetchRules() // 刷新规则列表
|
||||
} else {
|
||||
MessagePlugin.error(res.message || '删除规则失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete auto score rule:', error)
|
||||
MessagePlugin.error('删除规则失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggle = async (ruleId: number, enabled: boolean) => {
|
||||
if (!(window as any).api) return
|
||||
// 权限检查
|
||||
try {
|
||||
const authRes = await (window as any).api.authGetStatus()
|
||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||
MessagePlugin.error('需要管理员权限以启用/禁用自动加分规则')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Auth check failed', e)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled })
|
||||
if (res.success) {
|
||||
MessagePlugin.success(enabled ? '规则已启用' : '规则已禁用')
|
||||
fetchRules() // 刷新规则列表
|
||||
} else {
|
||||
MessagePlugin.error(res.message || (enabled ? '启用规则失败' : '禁用规则失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle auto score rule:', error)
|
||||
MessagePlugin.error(enabled ? '启用规则失败' : '禁用规则失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetForm = () => {
|
||||
form.reset()
|
||||
setEditingRuleId(null)
|
||||
}
|
||||
|
||||
const columns: PrimaryTableCol<AutoScoreRule>[] = [
|
||||
{
|
||||
colKey: 'enabled',
|
||||
title: '状态',
|
||||
width: 80,
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
value={row.enabled}
|
||||
onChange={(value) => handleToggle(row.id, value)}
|
||||
size="small"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{ colKey: 'name', title: '规则名称', width: 150 },
|
||||
{
|
||||
colKey: 'intervalMinutes',
|
||||
title: '间隔',
|
||||
width: 100,
|
||||
cell: ({ row }) => `${row.intervalMinutes} 分钟`
|
||||
},
|
||||
{
|
||||
colKey: 'scoreValue',
|
||||
title: '分值',
|
||||
width: 80,
|
||||
cell: ({ row }) => (
|
||||
<Tag theme={row.scoreValue > 0 ? 'success' : 'danger'} variant="light">
|
||||
{row.scoreValue > 0 ? `+${row.scoreValue}` : row.scoreValue}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
colKey: 'studentNames',
|
||||
title: '适用学生',
|
||||
width: 150,
|
||||
cell: ({ row }) => {
|
||||
if (row.studentNames.length === 0) {
|
||||
return <span>所有学生</span>
|
||||
}
|
||||
return <span title={row.studentNames.join(', ')}>{row.studentNames.length} 名学生</span>
|
||||
}
|
||||
},
|
||||
{ colKey: 'reason', title: '理由', ellipsis: true },
|
||||
{
|
||||
colKey: 'lastExecuted',
|
||||
title: '最后执行',
|
||||
width: 150,
|
||||
cell: ({ row }) => {
|
||||
if (!row.lastExecuted) return <span>未执行</span>
|
||||
try {
|
||||
const date = new Date(row.lastExecuted)
|
||||
return date.toLocaleString()
|
||||
} catch {
|
||||
return <span>无效时间</span>
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'operation',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
cell: ({ row }) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outline"
|
||||
onClick={() => handleEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
content="确定要删除这条规则吗?"
|
||||
onConfirm={() => handleDelete(row.id)}
|
||||
>
|
||||
<Button size="small" variant="outline" theme="danger">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}>自动化加分管理</h2>
|
||||
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Form
|
||||
form={form}
|
||||
labelWidth={100}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
||||
<Form.FormItem
|
||||
label="规则名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入规则名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:每日签到加分" />
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="间隔时间(分钟)"
|
||||
name="intervalMinutes"
|
||||
rules={[
|
||||
{ required: true, message: '请输入间隔时间' },
|
||||
{ min: 1, message: '间隔时间至少为1分钟' }
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} placeholder="例如:1440(每天)" />
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="加分值"
|
||||
name="scoreValue"
|
||||
rules={[{ required: true, message: '请输入加分值' }]}
|
||||
>
|
||||
<InputNumber placeholder="例如:1(每次加1分)" />
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="适用学生"
|
||||
name="studentNames"
|
||||
>
|
||||
<Input placeholder="留空表示所有学生,多个学生用逗号分隔" />
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="加分理由"
|
||||
name="reason"
|
||||
>
|
||||
<Input placeholder="例如:每日签到奖励" />
|
||||
</Form.FormItem>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
||||
<Button
|
||||
theme="primary"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{editingRuleId !== null ? '更新规则' : '添加规则'}
|
||||
</Button>
|
||||
<Button
|
||||
type="reset"
|
||||
variant="outline"
|
||||
>
|
||||
{editingRuleId !== null ? '取消编辑' : '重置表单'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Table
|
||||
data={rules}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ defaultPageSize: 10 }}
|
||||
style={{ color: 'var(--ss-text-main)' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}>
|
||||
<h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3>
|
||||
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
|
||||
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
|
||||
<li>间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次</li>
|
||||
<li>如果"适用学生"字段为空,则规则适用于所有学生</li>
|
||||
<li>可以随时启用/禁用规则,不会影响已保存的规则配置</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,9 @@ const Leaderboard = lazy(() => import('./Leaderboard').then((m) => ({ default: m
|
||||
const SettlementHistory = lazy(() =>
|
||||
import('./SettlementHistory').then((m) => ({ default: m.SettlementHistory }))
|
||||
)
|
||||
const AutoScoreManager = lazy(() =>
|
||||
import('./AutoScoreManager').then((m) => ({ default: m.AutoScoreManager }))
|
||||
)
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
@@ -121,6 +124,7 @@ export function ContentArea({
|
||||
<Route path="/leaderboard" element={<Leaderboard />} />
|
||||
<Route path="/settlements" element={<SettlementHistory />} />
|
||||
<Route path="/reasons" element={<ReasonManager canEdit={permission === 'admin'} />} />
|
||||
<Route path="/auto-score" element={<AutoScoreManager />} />
|
||||
<Route path="/settings" element={<Settings permission={permission} />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -20,6 +20,7 @@ type appSettings = {
|
||||
is_wizard_completed: boolean
|
||||
log_level: 'debug' | 'info' | 'warn' | 'error'
|
||||
window_zoom?: string
|
||||
auto_score_enabled?: boolean
|
||||
}
|
||||
|
||||
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
|
||||
@@ -98,6 +99,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
if (change?.key === 'is_wizard_completed')
|
||||
return { ...prev, is_wizard_completed: change.value }
|
||||
if (change?.key === 'window_zoom') return { ...prev, window_zoom: change.value }
|
||||
if (change?.key === 'auto_score_enabled') return { ...prev, auto_score_enabled: change.value }
|
||||
return prev
|
||||
})
|
||||
})
|
||||
@@ -261,6 +263,52 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onChange={(v) => setActiveTab(v as string)}>
|
||||
<Tabs.TabPanel value="auto-score" label="自动加分">
|
||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '12px' }}>自动加分功能</div>
|
||||
<div style={{ marginBottom: '16px', color: 'var(--ss-text-secondary)' }}>
|
||||
自动加分功能会按照设定的时间间隔自动为学生加分。
|
||||
</div>
|
||||
<Form labelWidth={120}>
|
||||
<Form.FormItem label="启用自动加分">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Button
|
||||
theme={settings.auto_score_enabled ? 'success' : 'danger'}
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
if (!(window as any).api) return
|
||||
const newValue = !settings.auto_score_enabled
|
||||
const res = await (window as any).api.setSetting('auto_score_enabled', newValue)
|
||||
if (res.success) {
|
||||
setSettings((prev) => ({ ...prev, auto_score_enabled: newValue }))
|
||||
MessagePlugin.success(`自动加分功能已${newValue ? '启用' : '禁用'}`)
|
||||
} else {
|
||||
MessagePlugin.error(res.message || `设置失败`)
|
||||
}
|
||||
}}
|
||||
disabled={!canAdmin}
|
||||
>
|
||||
{settings.auto_score_enabled ? '已启用' : '已禁用'}
|
||||
</Button>
|
||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>
|
||||
{settings.auto_score_enabled
|
||||
? '当前处于启用状态'
|
||||
: '当前处于禁用状态'}
|
||||
</span>
|
||||
</div>
|
||||
</Form.FormItem>
|
||||
</Form>
|
||||
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: 'var(--ss-bg-color)', borderRadius: '4px', border: '1px solid var(--ss-border-color)' }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: '8px' }}>使用说明:</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px', color: 'var(--ss-text-secondary)' }}>
|
||||
<li>启用功能后,系统将按照设定的规则自动为学生加分</li>
|
||||
<li>在"自动加分"菜单中可以创建和管理加分规则</li>
|
||||
<li>每个规则可以设置不同的时间间隔、加分值和适用学生</li>
|
||||
<li>请谨慎设置,避免积分增长过快</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</Tabs.TabPanel>
|
||||
<Tabs.TabPanel value="appearance" label="外观">
|
||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||
<Form labelWidth={120}>
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
HistoryIcon,
|
||||
RootListIcon,
|
||||
ViewListIcon,
|
||||
HomeIcon
|
||||
HomeIcon,
|
||||
MoneyIcon
|
||||
} from 'tdesign-icons-react'
|
||||
import appLogo from '../assets/logoHD.svg'
|
||||
|
||||
@@ -67,8 +68,7 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||
<Menu value={activeMenu} onChange={onMenuChange} style={{ width: '100%', border: 'none' }}>
|
||||
<Menu.MenuItem value="home" icon={<HomeIcon />}>
|
||||
主页
|
||||
<Menu.MenuItem value="home" icon={<HomeIcon />}> 主页
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="students" icon={<UserIcon />} disabled={permission !== 'admin'}>
|
||||
学生管理
|
||||
@@ -76,11 +76,11 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
||||
<Menu.MenuItem value="score" icon={<HistoryIcon />}>
|
||||
积分管理
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}>
|
||||
排行榜
|
||||
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}> 排行榜
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="settlements" icon={<HistoryIcon />}>
|
||||
结算历史
|
||||
<Menu.MenuItem value="auto-score" icon={<MoneyIcon />}> 自动加分
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="settlements" icon={<HistoryIcon />}> 结算历史
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="reasons" icon={<RootListIcon />} disabled={permission !== 'admin'}>
|
||||
理由管理
|
||||
|
||||
Reference in New Issue
Block a user