mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
@@ -14,7 +14,8 @@ export {
|
|||||||
SettlementRepositoryToken,
|
SettlementRepositoryToken,
|
||||||
ThemeServiceToken,
|
ThemeServiceToken,
|
||||||
WindowManagerToken,
|
WindowManagerToken,
|
||||||
TrayServiceToken
|
TrayServiceToken,
|
||||||
|
AutoScoreServiceToken
|
||||||
} from './tokens'
|
} from './tokens'
|
||||||
export type {
|
export type {
|
||||||
appRuntimeContext,
|
appRuntimeContext,
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export const SettlementRepositoryToken = Symbol.for('secscore.settlementReposito
|
|||||||
export const ThemeServiceToken = Symbol.for('secscore.themeService')
|
export const ThemeServiceToken = Symbol.for('secscore.themeService')
|
||||||
export const WindowManagerToken = Symbol.for('secscore.windowManager')
|
export const WindowManagerToken = Symbol.for('secscore.windowManager')
|
||||||
export const TrayServiceToken = Symbol.for('secscore.trayService')
|
export const TrayServiceToken = Symbol.for('secscore.trayService')
|
||||||
|
export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService')
|
||||||
|
|||||||
+29
-1
@@ -15,6 +15,7 @@ import { DataService } from './services/DataService'
|
|||||||
import { ThemeService } from './services/ThemeService'
|
import { ThemeService } from './services/ThemeService'
|
||||||
import { WindowManager, type windowManagerOptions } from './services/WindowManager'
|
import { WindowManager, type windowManagerOptions } from './services/WindowManager'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
|
import { AutoScoreService } from './services/AutoScoreService'
|
||||||
import { StudentRepository } from './repos/StudentRepository'
|
import { StudentRepository } from './repos/StudentRepository'
|
||||||
import { ReasonRepository } from './repos/ReasonRepository'
|
import { ReasonRepository } from './repos/ReasonRepository'
|
||||||
import { EventRepository } from './repos/EventRepository'
|
import { EventRepository } from './repos/EventRepository'
|
||||||
@@ -33,7 +34,8 @@ import {
|
|||||||
StudentRepositoryToken,
|
StudentRepositoryToken,
|
||||||
ThemeServiceToken,
|
ThemeServiceToken,
|
||||||
WindowManagerToken,
|
WindowManagerToken,
|
||||||
TrayServiceToken
|
TrayServiceToken,
|
||||||
|
AutoScoreServiceToken
|
||||||
} from './hosting'
|
} from './hosting'
|
||||||
|
|
||||||
type mainAppConfig = {
|
type mainAppConfig = {
|
||||||
@@ -251,6 +253,32 @@ app.whenReady().then(async () => {
|
|||||||
TrayServiceToken,
|
TrayServiceToken,
|
||||||
(p) => new TrayService(p.get(MainContext), config.window)
|
(p) => new TrayService(p.get(MainContext), config.window)
|
||||||
)
|
)
|
||||||
|
services.addSingleton(
|
||||||
|
AutoScoreServiceToken,
|
||||||
|
(p) => new AutoScoreService(p.get(MainContext))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.configure(async (_builderContext, appCtx) => {
|
||||||
|
const services = appCtx.services
|
||||||
|
services.get(LoggerToken)
|
||||||
|
const db = services.get(DbManagerToken) as DbManager
|
||||||
|
await db.initialize()
|
||||||
|
const settings = services.get(SettingsStoreToken) as SettingsService
|
||||||
|
await settings.initialize()
|
||||||
|
services.get(SecurityServiceToken)
|
||||||
|
services.get(PermissionServiceToken)
|
||||||
|
services.get(AuthService)
|
||||||
|
services.get(DataService)
|
||||||
|
services.get(StudentRepositoryToken)
|
||||||
|
services.get(ReasonRepositoryToken)
|
||||||
|
services.get(EventRepositoryToken)
|
||||||
|
services.get(SettlementRepositoryToken)
|
||||||
|
services.get(ThemeServiceToken)
|
||||||
|
services.get(WindowManagerToken)
|
||||||
|
const tray = services.get(TrayServiceToken) as TrayService
|
||||||
|
tray.initialize()
|
||||||
|
const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService
|
||||||
|
autoScore.initialize?.()
|
||||||
})
|
})
|
||||||
.configure(async (_builderContext, appCtx) => {
|
.configure(async (_builderContext, appCtx) => {
|
||||||
const services = appCtx.services
|
const services = appCtx.services
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
import { Service } from '../../shared/kernel'
|
||||||
|
import { MainContext } from '../context'
|
||||||
|
import { student } from '../repos/StudentRepository'
|
||||||
|
|
||||||
|
interface AutoScoreRule {
|
||||||
|
id: number
|
||||||
|
enabled: boolean
|
||||||
|
name: string
|
||||||
|
intervalMinutes: number // 间隔分钟数
|
||||||
|
studentNames: string[] // 学生姓名列表,空数组代表所有学生
|
||||||
|
scoreValue: number // 每次加分值
|
||||||
|
reason: string // 加分理由
|
||||||
|
lastExecuted?: Date // 最后执行时间
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '../../shared/kernel' {
|
||||||
|
interface Context {
|
||||||
|
autoScore: AutoScoreService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AutoScoreService extends Service {
|
||||||
|
private rules: AutoScoreRule[] = []
|
||||||
|
private timers: Map<number, NodeJS.Timeout> = new Map()
|
||||||
|
private initialized = false
|
||||||
|
|
||||||
|
constructor(ctx: MainContext) {
|
||||||
|
super(ctx, 'autoScore')
|
||||||
|
this.registerIpc()
|
||||||
|
this.loadRulesFromSettings()
|
||||||
|
this.startRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
private get mainCtx() {
|
||||||
|
return this.ctx as MainContext
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerIpc() {
|
||||||
|
this.mainCtx.handle('auto-score:getRules', async (event) => {
|
||||||
|
try {
|
||||||
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
|
return { success: false, message: 'Permission denied' }
|
||||||
|
return { success: true, data: this.getRules() }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainCtx.handle('auto-score:addRule', async (event, rule) => {
|
||||||
|
try {
|
||||||
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
|
return { success: false, message: 'Permission denied' }
|
||||||
|
const id = await this.addRule(rule)
|
||||||
|
return { success: true, data: id }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainCtx.handle('auto-score:updateRule', async (event, rule) => {
|
||||||
|
try {
|
||||||
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
|
return { success: false, message: 'Permission denied' }
|
||||||
|
const success = await this.updateRule(rule)
|
||||||
|
return { success, data: success }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainCtx.handle('auto-score:deleteRule', async (event, ruleId) => {
|
||||||
|
try {
|
||||||
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
|
return { success: false, message: 'Permission denied' }
|
||||||
|
const success = await this.deleteRule(ruleId)
|
||||||
|
return { success, data: success }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainCtx.handle('auto-score:toggleRule', async (event, params) => {
|
||||||
|
try {
|
||||||
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
|
return { success: false, message: 'Permission denied' }
|
||||||
|
const { ruleId, enabled } = params
|
||||||
|
const success = await this.toggleRule(ruleId, enabled)
|
||||||
|
return { success, data: success }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainCtx.handle('auto-score:getStatus', async (event) => {
|
||||||
|
try {
|
||||||
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
|
return { success: false, message: 'Permission denied' }
|
||||||
|
return { success: true, data: { enabled: this.isEnabled() } }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadRulesFromSettings() {
|
||||||
|
try {
|
||||||
|
// 从设置中加载自动化规则
|
||||||
|
const settings = await this.mainCtx.settings.getAllRaw()
|
||||||
|
const autoScoreRulesStr = settings['auto_score_rules'] || '[]'
|
||||||
|
const rulesFromSettings = JSON.parse(autoScoreRulesStr)
|
||||||
|
|
||||||
|
this.rules = rulesFromSettings.map((rule: any) => ({
|
||||||
|
...rule,
|
||||||
|
lastExecuted: rule.lastExecuted ? new Date(rule.lastExecuted) : undefined
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load auto score rules from settings:', error)
|
||||||
|
this.rules = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveRulesToSettings() {
|
||||||
|
try {
|
||||||
|
const rulesToSave = this.rules.map(({ lastExecuted, ...rule }) => ({
|
||||||
|
...rule,
|
||||||
|
lastExecuted: lastExecuted?.toISOString()
|
||||||
|
}))
|
||||||
|
await this.mainCtx.settings.setRaw('auto_score_rules', JSON.stringify(rulesToSave))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save auto score rules to settings:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startRules() {
|
||||||
|
if (this.initialized) {
|
||||||
|
this.stopRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rule of this.rules) {
|
||||||
|
if (rule.enabled) {
|
||||||
|
this.startRuleTimer(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private startRuleTimer(rule: AutoScoreRule) {
|
||||||
|
// 清除现有的定时器
|
||||||
|
if (this.timers.has(rule.id)) {
|
||||||
|
clearTimeout(this.timers.get(rule.id)!)
|
||||||
|
this.timers.delete(rule.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下次执行时间
|
||||||
|
const now = new Date()
|
||||||
|
const intervalMs = rule.intervalMinutes * 60 * 1000
|
||||||
|
|
||||||
|
// 如果规则之前执行过,计算从上次执行到现在需要等待的时间
|
||||||
|
let delayMs = intervalMs
|
||||||
|
if (rule.lastExecuted) {
|
||||||
|
const timeSinceLastExecution = now.getTime() - rule.lastExecuted.getTime()
|
||||||
|
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
|
||||||
|
// 如果已经超过了间隔时间,立即执行
|
||||||
|
if (timeSinceLastExecution >= intervalMs) {
|
||||||
|
delayMs = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.executeRule(rule)
|
||||||
|
// 设置重复执行
|
||||||
|
this.setRuleInterval(rule)
|
||||||
|
}, delayMs)
|
||||||
|
|
||||||
|
this.timers.set(rule.id, timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private setRuleInterval(rule: AutoScoreRule) {
|
||||||
|
const intervalMs = rule.intervalMinutes * 60 * 1000
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
this.executeRule(rule)
|
||||||
|
}, intervalMs)
|
||||||
|
|
||||||
|
this.timers.set(rule.id, timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeRule(rule: AutoScoreRule) {
|
||||||
|
try {
|
||||||
|
console.log(`Executing auto score rule: ${rule.name}`)
|
||||||
|
|
||||||
|
const studentRepo = this.mainCtx.students
|
||||||
|
const eventRepo = this.mainCtx.events
|
||||||
|
|
||||||
|
let studentsToScore: student[] = []
|
||||||
|
if (rule.studentNames.length === 0) {
|
||||||
|
// 如果没有指定学生,对所有学生加分
|
||||||
|
const allStudents = await studentRepo.findAll()
|
||||||
|
studentsToScore = allStudents
|
||||||
|
} else {
|
||||||
|
// 否则只对指定的学生加分
|
||||||
|
const allStudents = await studentRepo.findAll()
|
||||||
|
for (const name of rule.studentNames) {
|
||||||
|
const student = allStudents.find(s => s.name === name)
|
||||||
|
if (student) {
|
||||||
|
studentsToScore.push(student)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个学生添加积分事件
|
||||||
|
for (const student of studentsToScore) {
|
||||||
|
await eventRepo.create({
|
||||||
|
student_name: student.name,
|
||||||
|
reason_content: rule.reason || `自动化加分 - ${rule.name}`,
|
||||||
|
delta: rule.scoreValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新规则的最后执行时间
|
||||||
|
rule.lastExecuted = new Date()
|
||||||
|
await this.saveRulesToSettings()
|
||||||
|
|
||||||
|
console.log(`Auto score rule executed successfully for ${studentsToScore.length} students`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to execute auto score rule ${rule.name}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopRules() {
|
||||||
|
for (const [_, timer] of this.timers) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
this.timers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRule(rule: Omit<AutoScoreRule, 'id' | 'lastExecuted'>): Promise<number> {
|
||||||
|
const newId = this.rules.length > 0 ? Math.max(...this.rules.map(r => r.id)) + 1 : 1
|
||||||
|
const newRule: AutoScoreRule = {
|
||||||
|
...rule,
|
||||||
|
id: newId,
|
||||||
|
lastExecuted: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rules.push(newRule)
|
||||||
|
await this.saveRulesToSettings()
|
||||||
|
|
||||||
|
if (rule.enabled) {
|
||||||
|
this.startRuleTimer(newRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newId
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRule(rule: Partial<AutoScoreRule> & { id: number }): Promise<boolean> {
|
||||||
|
const index = this.rules.findIndex(r => r.id === rule.id)
|
||||||
|
if (index === -1) return false
|
||||||
|
|
||||||
|
// 停止旧的定时器
|
||||||
|
if (this.timers.has(rule.id)) {
|
||||||
|
clearTimeout(this.timers.get(rule.id)!)
|
||||||
|
clearInterval(this.timers.get(rule.id)!)
|
||||||
|
this.timers.delete(rule.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新规则
|
||||||
|
const updatedRule = { ...this.rules[index], ...rule }
|
||||||
|
this.rules[index] = updatedRule
|
||||||
|
|
||||||
|
// 保存到设置
|
||||||
|
await this.saveRulesToSettings()
|
||||||
|
|
||||||
|
// 如果规则已启用,启动新的定时器
|
||||||
|
if (updatedRule.enabled) {
|
||||||
|
this.startRuleTimer(updatedRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRule(ruleId: number): Promise<boolean> {
|
||||||
|
const index = this.rules.findIndex(r => r.id === ruleId)
|
||||||
|
if (index === -1) return false
|
||||||
|
|
||||||
|
// 停止定时器
|
||||||
|
if (this.timers.has(ruleId)) {
|
||||||
|
clearTimeout(this.timers.get(ruleId)!)
|
||||||
|
clearInterval(this.timers.get(ruleId)!)
|
||||||
|
this.timers.delete(ruleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数组中移除规则
|
||||||
|
this.rules.splice(index, 1)
|
||||||
|
await this.saveRulesToSettings()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleRule(ruleId: number, enabled: boolean): Promise<boolean> {
|
||||||
|
const rule = this.rules.find(r => r.id === ruleId)
|
||||||
|
if (!rule) return false
|
||||||
|
|
||||||
|
rule.enabled = enabled
|
||||||
|
|
||||||
|
// 停止现有定时器
|
||||||
|
if (this.timers.has(ruleId)) {
|
||||||
|
clearTimeout(this.timers.get(ruleId)!)
|
||||||
|
clearInterval(this.timers.get(ruleId)!)
|
||||||
|
this.timers.delete(ruleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果规则现在是启用的,启动定时器
|
||||||
|
if (enabled) {
|
||||||
|
this.startRuleTimer(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveRulesToSettings()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getRules(): AutoScoreRule[] {
|
||||||
|
return [...this.rules]
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return this.rules.some(rule => rule.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
async restart() {
|
||||||
|
this.stopRules()
|
||||||
|
await this.loadRulesFromSettings()
|
||||||
|
this.startRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
// 确保服务正确初始化
|
||||||
|
await this.loadRulesFromSettings()
|
||||||
|
this.startRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
async dispose(): Promise<void> {
|
||||||
|
this.stopRules()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,16 @@ export class SettingsService extends Service {
|
|||||||
defaultValue: 'rounded',
|
defaultValue: 'rounded',
|
||||||
writePermission: 'admin',
|
writePermission: 'admin',
|
||||||
validate: (v) => v === 'rounded' || v === 'small' || v === 'square'
|
validate: (v) => v === 'rounded' || v === 'small' || v === 'square'
|
||||||
|
},
|
||||||
|
auto_score_enabled: {
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
writePermission: 'admin'
|
||||||
|
},
|
||||||
|
auto_score_rules: {
|
||||||
|
kind: 'json',
|
||||||
|
defaultValue: [],
|
||||||
|
writePermission: 'admin'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ const api = {
|
|||||||
ipcRenderer.invoke('log:write', payload),
|
ipcRenderer.invoke('log:write', payload),
|
||||||
|
|
||||||
registerUrlProtocol: () => ipcRenderer.invoke('app:register-url-protocol')
|
registerUrlProtocol: () => ipcRenderer.invoke('app:register-url-protocol')
|
||||||
|
,
|
||||||
|
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
|
||||||
|
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type settingsSpec = {
|
|||||||
window_theme: 'auto' | 'dark' | 'light'
|
window_theme: 'auto' | 'dark' | 'light'
|
||||||
window_effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
window_effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
||||||
window_radius: 'rounded' | 'small' | 'square'
|
window_radius: 'rounded' | 'small' | 'square'
|
||||||
|
auto_score_enabled: boolean
|
||||||
|
auto_score_rules: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type settingsKey = keyof settingsSpec
|
export type settingsKey = keyof settingsSpec
|
||||||
@@ -152,4 +154,6 @@ export interface electronApi {
|
|||||||
}) => Promise<ipcResponse<void>>
|
}) => Promise<ipcResponse<void>>
|
||||||
|
|
||||||
registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>>
|
registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>>
|
||||||
|
// Generic invoke wrapper (minimal compatibility API)
|
||||||
|
invoke?: (channel: string, ...args: any[]) => Promise<any>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function MainContent(): React.JSX.Element {
|
|||||||
if (p.startsWith('/leaderboard')) return 'leaderboard'
|
if (p.startsWith('/leaderboard')) return 'leaderboard'
|
||||||
if (p.startsWith('/settlements')) return 'settlements'
|
if (p.startsWith('/settlements')) return 'settlements'
|
||||||
if (p.startsWith('/reasons')) return 'reasons'
|
if (p.startsWith('/reasons')) return 'reasons'
|
||||||
|
if (p.startsWith('/auto-score')) return 'auto-score'
|
||||||
if (p.startsWith('/settings')) return 'settings'
|
if (p.startsWith('/settings')) return 'settings'
|
||||||
return 'home'
|
return 'home'
|
||||||
}, [location.pathname])
|
}, [location.pathname])
|
||||||
@@ -103,6 +104,7 @@ function MainContent(): React.JSX.Element {
|
|||||||
if (key === 'leaderboard') navigate('/leaderboard')
|
if (key === 'leaderboard') navigate('/leaderboard')
|
||||||
if (key === 'settlements') navigate('/settlements')
|
if (key === 'settlements') navigate('/settlements')
|
||||||
if (key === 'reasons') navigate('/reasons')
|
if (key === 'reasons') navigate('/reasons')
|
||||||
|
if (key === 'auto-score') navigate('/auto-score')
|
||||||
if (key === 'settings') navigate('/settings')
|
if (key === 'settings') navigate('/settings')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,440 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Button,
|
||||||
|
MessagePlugin,
|
||||||
|
Table,
|
||||||
|
PrimaryTableCol,
|
||||||
|
Tag,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Popconfirm,
|
||||||
|
Radio,
|
||||||
|
Select,
|
||||||
|
TooltipLite
|
||||||
|
} 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 [students, setStudents] = useState<{ id: number; name: string }[]>([])
|
||||||
|
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 [rulesRes, studentsRes] = await Promise.all([
|
||||||
|
(window as any).api.invoke('auto-score:getRules', {}),
|
||||||
|
(window as any).api.queryStudents({})
|
||||||
|
])
|
||||||
|
if (rulesRes.success) {
|
||||||
|
setRules(rulesRes.data)
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(rulesRes.message || '获取规则失败')
|
||||||
|
}
|
||||||
|
if (studentsRes.success) {
|
||||||
|
setStudents(studentsRes.data)
|
||||||
|
}
|
||||||
|
} 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 & { timeUnit: string };
|
||||||
|
|
||||||
|
if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
|
||||||
|
MessagePlugin.warning('请填写完整信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据单位转换间隔时间
|
||||||
|
const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes;
|
||||||
|
|
||||||
|
// 确保 studentNames 是数组类型
|
||||||
|
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [];
|
||||||
|
|
||||||
|
const ruleData = {
|
||||||
|
enabled: true,
|
||||||
|
name: values.name,
|
||||||
|
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() 导致的栈溢出
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: '',
|
||||||
|
intervalMinutes: undefined,
|
||||||
|
studentNames: '',
|
||||||
|
scoreValue: undefined,
|
||||||
|
reason: '',
|
||||||
|
timeUnit: 'minutes'
|
||||||
|
})
|
||||||
|
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() 导致的栈溢出
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: '',
|
||||||
|
intervalMinutes: undefined,
|
||||||
|
studentNames: '',
|
||||||
|
scoreValue: undefined,
|
||||||
|
reason: ''
|
||||||
|
})
|
||||||
|
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 }) => {
|
||||||
|
const isDays = row.intervalMinutes >= 1440
|
||||||
|
const value = isDays ? row.intervalMinutes / 1440 : row.intervalMinutes
|
||||||
|
const unit = isDays ? '天' : '分钟'
|
||||||
|
return `${value} ${unit}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
const studentList = row.studentNames.join(',\n')
|
||||||
|
return (
|
||||||
|
<TooltipLite
|
||||||
|
content={studentList}
|
||||||
|
showArrow
|
||||||
|
placement="mouse"
|
||||||
|
theme="default"
|
||||||
|
>
|
||||||
|
{row.studentNames.length} 名学生
|
||||||
|
</TooltipLite>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 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>
|
||||||
|
<Space>
|
||||||
|
<Form.FormItem
|
||||||
|
label="间隔时间"
|
||||||
|
name="intervalMinutes"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入间隔时间' },
|
||||||
|
{ min: 1, message: '间隔时间至少为1分钟' }
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} placeholder="例如:1(每隔1分钟/天执行一次)" />
|
||||||
|
</Form.FormItem>
|
||||||
|
<Form.FormItem name="timeUnit" initialData="minutes" style={{ marginBottom: 0 }}>
|
||||||
|
<Radio.Group variant="default-filled">
|
||||||
|
<Radio.Button value="days">天</Radio.Button>
|
||||||
|
<Radio.Button value="minutes">分钟</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.FormItem>
|
||||||
|
</Space>
|
||||||
|
</Form.FormItem>
|
||||||
|
|
||||||
|
<Form.FormItem
|
||||||
|
label="加分值"
|
||||||
|
name="scoreValue"
|
||||||
|
rules={[{ required: true, message: '请输入加分值' }]}
|
||||||
|
>
|
||||||
|
<InputNumber placeholder="例如:1(每次加1分)" />
|
||||||
|
</Form.FormItem>
|
||||||
|
|
||||||
|
<Form.FormItem
|
||||||
|
label="适用学生"
|
||||||
|
name="studentNames"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
filterable
|
||||||
|
multiple
|
||||||
|
placeholder="请选择或搜索学生(留空表示所有学生)"
|
||||||
|
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
||||||
|
/>
|
||||||
|
</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(() =>
|
const SettlementHistory = lazy(() =>
|
||||||
import('./SettlementHistory').then((m) => ({ default: m.SettlementHistory }))
|
import('./SettlementHistory').then((m) => ({ default: m.SettlementHistory }))
|
||||||
)
|
)
|
||||||
|
const AutoScoreManager = lazy(() =>
|
||||||
|
import('./AutoScoreManager').then((m) => ({ default: m.AutoScoreManager }))
|
||||||
|
)
|
||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
@@ -121,6 +124,7 @@ export function ContentArea({
|
|||||||
<Route path="/leaderboard" element={<Leaderboard />} />
|
<Route path="/leaderboard" element={<Leaderboard />} />
|
||||||
<Route path="/settlements" element={<SettlementHistory />} />
|
<Route path="/settlements" element={<SettlementHistory />} />
|
||||||
<Route path="/reasons" element={<ReasonManager canEdit={permission === 'admin'} />} />
|
<Route path="/reasons" element={<ReasonManager canEdit={permission === 'admin'} />} />
|
||||||
|
<Route path="/auto-score" element={<AutoScoreManager />} />
|
||||||
<Route path="/settings" element={<Settings permission={permission} />} />
|
<Route path="/settings" element={<Settings permission={permission} />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -138,7 +138,10 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const values = form.getFieldsValue(true) as any
|
const values = form.getFieldsValue(true) as any
|
||||||
if (!values.student_name || !values.reason_content) {
|
|
||||||
|
// 支持多选学生
|
||||||
|
const studentNames = Array.isArray(values.student_name) ? values.student_name : [values.student_name]
|
||||||
|
if (!studentNames || studentNames.length === 0 || !values.reason_content) {
|
||||||
MessagePlugin.warning('请填写完整信息')
|
MessagePlugin.warning('请填写完整信息')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -161,26 +164,39 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
: Math.abs(deltaInput)
|
: Math.abs(deltaInput)
|
||||||
: Number(selectedReason?.delta ?? 0)
|
: Number(selectedReason?.delta ?? 0)
|
||||||
|
|
||||||
const res = await (window as any).api.createEvent({
|
// 为每个选中的学生创建事件
|
||||||
student_name: values.student_name,
|
try {
|
||||||
reason_content: values.reason_content,
|
let successCount = 0
|
||||||
delta: delta
|
for (const studentName of studentNames) {
|
||||||
})
|
const res = await (window as any).api.createEvent({
|
||||||
|
student_name: studentName,
|
||||||
|
reason_content: values.reason_content,
|
||||||
|
delta: delta
|
||||||
|
})
|
||||||
|
if (res.success) {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (res.success) {
|
if (successCount === studentNames.length) {
|
||||||
MessagePlugin.success('积分提交成功')
|
MessagePlugin.success(`已为 ${successCount} 名学生提交积分`)
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
delta: undefined,
|
student_name: [],
|
||||||
reason_content: '',
|
delta: undefined,
|
||||||
reason_id: undefined,
|
reason_content: '',
|
||||||
type: 'add'
|
reason_id: undefined,
|
||||||
})
|
type: 'add'
|
||||||
fetchData()
|
})
|
||||||
emitDataUpdated('events')
|
fetchData()
|
||||||
} else {
|
emitDataUpdated('events')
|
||||||
MessagePlugin.error(res.message || '提交失败')
|
} else {
|
||||||
|
MessagePlugin.warning(`成功提交 ${successCount}/${studentNames.length} 名学生的积分`)
|
||||||
|
fetchData()
|
||||||
|
emitDataUpdated('events')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSubmitLoading(false)
|
||||||
}
|
}
|
||||||
setSubmitLoading(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUndo = async (uuid: string) => {
|
const handleUndo = async (uuid: string) => {
|
||||||
@@ -250,6 +266,7 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
<Form.FormItem label="姓名" name="student_name">
|
<Form.FormItem label="姓名" name="student_name">
|
||||||
<Select
|
<Select
|
||||||
filterable
|
filterable
|
||||||
|
multiple
|
||||||
placeholder="请选择或搜索学生"
|
placeholder="请选择或搜索学生"
|
||||||
filter={(filterWords, option) =>
|
filter={(filterWords, option) =>
|
||||||
matchStudentName(getOptionLabel(option), filterWords)
|
matchStudentName(getOptionLabel(option), filterWords)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type appSettings = {
|
|||||||
is_wizard_completed: boolean
|
is_wizard_completed: boolean
|
||||||
log_level: 'debug' | 'info' | 'warn' | 'error'
|
log_level: 'debug' | 'info' | 'warn' | 'error'
|
||||||
window_zoom?: string
|
window_zoom?: string
|
||||||
|
auto_score_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
|
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')
|
if (change?.key === 'is_wizard_completed')
|
||||||
return { ...prev, is_wizard_completed: change.value }
|
return { ...prev, is_wizard_completed: change.value }
|
||||||
if (change?.key === 'window_zoom') return { ...prev, window_zoom: 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
|
return prev
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -261,6 +263,52 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs value={activeTab} onChange={(v) => setActiveTab(v as string)}>
|
<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="外观">
|
<Tabs.TabPanel value="appearance" label="外观">
|
||||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||||
<Form labelWidth={120}>
|
<Form labelWidth={120}>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
RootListIcon,
|
RootListIcon,
|
||||||
ViewListIcon,
|
ViewListIcon,
|
||||||
HomeIcon
|
HomeIcon,
|
||||||
|
MoneyIcon
|
||||||
} from 'tdesign-icons-react'
|
} from 'tdesign-icons-react'
|
||||||
import appLogo from '../assets/logoHD.svg'
|
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' }}>
|
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Menu value={activeMenu} onChange={onMenuChange} style={{ width: '100%', border: 'none' }}>
|
<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>
|
||||||
<Menu.MenuItem value="students" icon={<UserIcon />} disabled={permission !== 'admin'}>
|
<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 value="score" icon={<HistoryIcon />}>
|
||||||
积分管理
|
积分管理
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}>
|
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}> 排行榜
|
||||||
排行榜
|
|
||||||
</Menu.MenuItem>
|
</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>
|
||||||
<Menu.MenuItem value="reasons" icon={<RootListIcon />} disabled={permission !== 'admin'}>
|
<Menu.MenuItem value="reasons" icon={<RootListIcon />} disabled={permission !== 'admin'}>
|
||||||
理由管理
|
理由管理
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user