diff --git a/src/main/db/DbManager.ts b/src/main/db/DbManager.ts index 7ffd8cf..f41067c 100644 --- a/src/main/db/DbManager.ts +++ b/src/main/db/DbManager.ts @@ -29,7 +29,15 @@ export class DbManager extends Service { this.dataSource = new DataSource({ type: 'better-sqlite3', database: dbPath, - entities: [StudentEntity, ReasonEntity, ScoreEventEntity, SettlementEntity, SettingEntity, TagEntity, StudentTagEntity], + entities: [ + StudentEntity, + ReasonEntity, + ScoreEventEntity, + SettlementEntity, + SettingEntity, + TagEntity, + StudentTagEntity + ], migrations, synchronize: false, logging: false diff --git a/src/main/db/entities/StudentEntity.ts b/src/main/db/entities/StudentEntity.ts index 57ae2bd..07907d5 100644 --- a/src/main/db/entities/StudentEntity.ts +++ b/src/main/db/entities/StudentEntity.ts @@ -11,7 +11,7 @@ export class StudentEntity { @Column({ type: 'text', default: '[]' }) tags!: string - + @Column({ type: 'integer', default: 0 }) score!: number diff --git a/src/main/db/entities/StudentTagEntity.ts b/src/main/db/entities/StudentTagEntity.ts index dbc5222..e7733e0 100644 --- a/src/main/db/entities/StudentTagEntity.ts +++ b/src/main/db/entities/StudentTagEntity.ts @@ -24,4 +24,4 @@ export class StudentTagEntity { @Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' }) created_at!: string -} \ No newline at end of file +} diff --git a/src/main/db/entities/TagEntity.ts b/src/main/db/entities/TagEntity.ts index ec1371e..6cd78b1 100644 --- a/src/main/db/entities/TagEntity.ts +++ b/src/main/db/entities/TagEntity.ts @@ -13,4 +13,4 @@ export class TagEntity { @Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' }) updated_at!: string -} \ No newline at end of file +} diff --git a/src/main/db/migrations/AddTagsSchema2026020600000.ts b/src/main/db/migrations/AddTagsSchema2026020600000.ts index 6e4bb3f..7a3275f 100644 --- a/src/main/db/migrations/AddTagsSchema2026020600000.ts +++ b/src/main/db/migrations/AddTagsSchema2026020600000.ts @@ -50,4 +50,4 @@ export class AddTagsSchema2026020600000 implements MigrationInterface { await queryRunner.query(`DROP TABLE IF EXISTS "student_tags"`) await queryRunner.query(`DROP TABLE IF EXISTS "tags"`) } -} \ No newline at end of file +} diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts index 7e115b5..4b37ad8 100644 --- a/src/main/hosting/tokens.ts +++ b/src/main/hosting/tokens.ts @@ -16,4 +16,4 @@ export const WindowManagerToken = Symbol.for('secscore.windowManager') export const TrayServiceToken = Symbol.for('secscore.trayService') export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService') export const HttpServerServiceToken = Symbol.for('secscore.httpServerService') -export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService') \ No newline at end of file +export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService') diff --git a/src/main/index.ts b/src/main/index.ts index 129ca4b..b7d06ba 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -237,7 +237,10 @@ app.whenReady().then(async () => { (p) => new PermissionService(p.get(MainContext)) ) services.addSingleton(AuthService, (p) => new AuthService(p.get(MainContext))) - services.addSingleton(DataService, (p) => new DataService(p.get(MainContext), p.get(TagRepositoryToken))) + services.addSingleton( + DataService, + (p) => new DataService(p.get(MainContext), p.get(TagRepositoryToken)) + ) services.addSingleton( StudentRepositoryToken, @@ -266,10 +269,7 @@ app.whenReady().then(async () => { TrayServiceToken, (p) => new TrayService(p.get(MainContext), config.window) ) - services.addSingleton( - AutoScoreServiceToken, - (p) => new AutoScoreService(p.get(MainContext)) - ) + services.addSingleton(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext))) services.addSingleton( HttpServerServiceToken, (p) => new HttpServerService(p.get(MainContext)) @@ -294,7 +294,9 @@ app.whenReady().then(async () => { services.get(ReasonRepositoryToken) services.get(EventRepositoryToken) services.get(SettlementRepositoryToken) - const theme = services.get(ThemeServiceToken) as import('./services/ThemeService').ThemeService + const theme = services.get( + ThemeServiceToken + ) as import('./services/ThemeService').ThemeService await theme.init() if (!process.env.HEADLESS) { services.get(WindowManagerToken) @@ -321,7 +323,9 @@ app.whenReady().then(async () => { services.get(ReasonRepositoryToken) services.get(EventRepositoryToken) services.get(SettlementRepositoryToken) - const theme = services.get(ThemeServiceToken) as import('./services/ThemeService').ThemeService + const theme = services.get( + ThemeServiceToken + ) as import('./services/ThemeService').ThemeService await theme.init() if (!process.env.HEADLESS) { services.get(WindowManagerToken) diff --git a/src/main/repos/StudentRepository.ts b/src/main/repos/StudentRepository.ts index feda4d6..a0e8130 100644 --- a/src/main/repos/StudentRepository.ts +++ b/src/main/repos/StudentRepository.ts @@ -80,7 +80,7 @@ export class StudentRepository extends Service { order: { score: 'DESC', name: 'ASC' } }) - return entities.map(entity => { + return entities.map((entity) => { let tags: string[] = [] try { tags = Array.isArray(entity.tags) ? entity.tags : JSON.parse(entity.tags || '[]') @@ -117,8 +117,8 @@ export class StudentRepository extends Service { if (key === 'id') continue if (key === 'tags') { next[key] = JSON.stringify(val || []) - } else { - next[key] = val + } else { + next[key] = val } } next.updated_at = new Date().toISOString() diff --git a/src/main/repos/TagRepository.ts b/src/main/repos/TagRepository.ts index 78ae889..9fc4549 100644 --- a/src/main/repos/TagRepository.ts +++ b/src/main/repos/TagRepository.ts @@ -43,7 +43,7 @@ export class TagRepository { .orderBy('st.created_at', 'ASC') .getMany() - return relations.map(r => r.tag).filter(Boolean) + return relations.map((r) => r.tag).filter(Boolean) } async addTagToStudent(studentId: number, tagId: number): Promise { @@ -76,4 +76,4 @@ export class TagRepository { await this.studentTagRepo.save(relation) } } -} \ No newline at end of file +} diff --git a/src/main/services/AutoScoreManager.tsx b/src/main/services/AutoScoreManager.tsx index 993ef3c..2da6ca6 100644 --- a/src/main/services/AutoScoreManager.tsx +++ b/src/main/services/AutoScoreManager.tsx @@ -50,7 +50,7 @@ export const AutoScoreManager: React.FC = () => { const fetchRules = async () => { if (!(window as any).api) return - + setLoading(true) try { // 权限检查:确保当前为 admin @@ -91,20 +91,23 @@ export const AutoScoreManager: React.FC = () => { }, []) const handleSubmit = async () => { - if (!(window as any).api) return; + if (!(window as any).api) return - const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & { timeUnit: string }; + const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & { + timeUnit: string + } if (!values.name || values.intervalMinutes == null || values.scoreValue == null) { - MessagePlugin.warning('请填写完整信息'); - return; + MessagePlugin.warning('请填写完整信息') + return } // 根据单位转换间隔时间 - const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes; + const intervalMinutes = + values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes // 确保 studentNames 是数组类型 - const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []; + const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] const ruleData = { enabled: true, @@ -112,8 +115,8 @@ export const AutoScoreManager: React.FC = () => { intervalMinutes, studentNames, scoreValue: values.scoreValue, - reason: values.reason || `自动化加分 - ${values.name}`, - }; + reason: values.reason || `自动化加分 - ${values.name}` + } // 权限检查:仅管理员可创建/更新自动化 try { @@ -153,7 +156,9 @@ export const AutoScoreManager: React.FC = () => { setEditingRuleId(null) fetchRules() // 刷新自动化列表 } else { - MessagePlugin.error(res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')) + MessagePlugin.error( + res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败') + ) } } catch (error) { console.error('Failed to submit auto score rule:', error) @@ -238,11 +243,11 @@ export const AutoScoreManager: React.FC = () => { } const columns: PrimaryTableCol[] = [ - { - colKey: 'drag', - title: '排序', - cell: () => , - width: 60 + { + colKey: 'drag', + title: '排序', + cell: () => , + width: 60 }, { colKey: 'enabled', @@ -288,12 +293,7 @@ export const AutoScoreManager: React.FC = () => { } const studentList = row.studentNames.join(',\n') return ( - + {row.studentNames.length} 名学生 ) @@ -320,17 +320,10 @@ export const AutoScoreManager: React.FC = () => { width: 150, cell: ({ row }) => ( - - handleDelete(row.id)} - > + handleDelete(row.id)}> @@ -339,7 +332,7 @@ export const AutoScoreManager: React.FC = () => { ) } ] - const onDragSort = (params: any) => setRules(params.newData); + const onDragSort = (params: any) => setRules(params.newData) // 定义触发规则选项 const triggerOptions = [ @@ -350,12 +343,30 @@ export const AutoScoreManager: React.FC = () => { { label: '参与活动', value: 'event_participated' }, { label: '签到', value: 'check_in' }, { label: '其他自定义事件', value: 'custom_event' } - ]; - + ] + const initialTriggers = [ - { id: 1, triggerEvent: triggerOptions[1], description: '当学生登录时触发自动化', haveValue: true, value: 1 }, - { id: 2, triggerEvent: triggerOptions[2], description: '当学生完成作业时触发自动化', haveValue: true, value: '12' }, - { id: 3, triggerEvent: triggerOptions[3], description: '当学生考试通过时触发自动化', haveValue: false, value: null } + { + id: 1, + triggerEvent: triggerOptions[1], + description: '当学生登录时触发自动化', + haveValue: true, + value: 1 + }, + { + id: 2, + triggerEvent: triggerOptions[2], + description: '当学生完成作业时触发自动化', + haveValue: true, + value: '12' + }, + { + id: 3, + triggerEvent: triggerOptions[3], + description: '当学生考试通过时触发自动化', + haveValue: false, + value: null + } ] const [triggerList, setTriggerList] = useState(initialTriggers) @@ -363,7 +374,9 @@ export const AutoScoreManager: React.FC = () => { const handleTriggerChange = (id: number, value: string) => { setTriggerList((prev) => prev.map((t) => - t.id === id ? { ...t, triggerEvent: triggerOptions.find((o) => o.value === value) || t.triggerEvent } : t + t.id === id + ? { ...t, triggerEvent: triggerOptions.find((o) => o.value === value) || t.triggerEvent } + : t ) ) } @@ -378,7 +391,10 @@ export const AutoScoreManager: React.FC = () => { const handleAddTrigger = () => { const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 - setTriggerList((prev) => [...prev, { id: nextId, triggerEvent: triggerOptions[0], description: '', haveValue: false, value: '' }]) + setTriggerList((prev) => [ + ...prev, + { id: nextId, triggerEvent: triggerOptions[0], description: '', haveValue: false, value: '' } + ]) } const triggerItems = triggerList @@ -403,7 +419,9 @@ export const AutoScoreManager: React.FC = () => { placeholder="请输入Value" style={{ width: '150px' }} value={String(triggerTest.value)} - onChange={(v) => handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))} + onChange={(v) => + handleValueChange(triggerTest.id, String((v as any).target?.value ?? v)) + } /> ) : null} @@ -414,11 +432,7 @@ export const AutoScoreManager: React.FC = () => {

自动化加分管理

-
+
{ @@ -458,43 +472,31 @@ export const AutoScoreManager: React.FC = () => { - - ({ label: student.name, value: student.name }))} + /> - +
- -
- + { pageSize, total: rules.length, onChange: (pageInfo) => setCurrentPage(pageInfo.current), - onPageSizeChange: (size) => setPageSize(size), + onPageSizeChange: (size) => setPageSize(size) }} style={{ color: 'var(--ss-text-main)' }} /> - - + + {triggerItems} - - + println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") -{/*
+ {/*

使用说明

  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • @@ -544,4 +551,4 @@ export const AutoScoreManager: React.FC = () => {
*/}
) -} \ No newline at end of file +} diff --git a/src/main/services/AutoScoreService.ts b/src/main/services/AutoScoreService.ts index b7fa94d..a135a43 100644 --- a/src/main/services/AutoScoreService.ts +++ b/src/main/services/AutoScoreService.ts @@ -108,7 +108,7 @@ export class AutoScoreService extends Service { 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 @@ -155,7 +155,7 @@ export class AutoScoreService extends Service { // 计算下次执行时间 const now = new Date() const intervalMs = rule.intervalMinutes * 60 * 1000 - + // 如果规则之前执行过,计算从上次执行到现在需要等待的时间 let delayMs = intervalMs if (rule.lastExecuted) { @@ -188,7 +188,7 @@ export class AutoScoreService extends Service { private async executeRule(rule: AutoScoreRule) { try { console.log(`Executing auto score rule: ${rule.name}`) - + const studentRepo = this.mainCtx.students const eventRepo = this.mainCtx.events @@ -201,7 +201,7 @@ export class AutoScoreService extends Service { // 否则只对指定的学生加分 const allStudents = await studentRepo.findAll() for (const name of rule.studentNames) { - const student = allStudents.find(s => s.name === name) + const student = allStudents.find((s) => s.name === name) if (student) { studentsToScore.push(student) } @@ -220,7 +220,7 @@ export class AutoScoreService extends Service { // 更新规则的最后执行时间 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) @@ -236,25 +236,25 @@ export class AutoScoreService extends Service { } async addRule(rule: Omit): Promise { - const newId = this.rules.length > 0 ? Math.max(...this.rules.map(r => r.id)) + 1 : 1 + 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 & { id: number }): Promise { - const index = this.rules.findIndex(r => r.id === rule.id) + const index = this.rules.findIndex((r) => r.id === rule.id) if (index === -1) return false // 停止旧的定时器 @@ -280,7 +280,7 @@ export class AutoScoreService extends Service { } async deleteRule(ruleId: number): Promise { - const index = this.rules.findIndex(r => r.id === ruleId) + const index = this.rules.findIndex((r) => r.id === ruleId) if (index === -1) return false // 停止定时器 @@ -298,7 +298,7 @@ export class AutoScoreService extends Service { } async toggleRule(ruleId: number, enabled: boolean): Promise { - const rule = this.rules.find(r => r.id === ruleId) + const rule = this.rules.find((r) => r.id === ruleId) if (!rule) return false rule.enabled = enabled @@ -324,7 +324,7 @@ export class AutoScoreService extends Service { } isEnabled(): boolean { - return this.rules.some(rule => rule.enabled) + return this.rules.some((rule) => rule.enabled) } async restart() { @@ -342,4 +342,4 @@ export class AutoScoreService extends Service { async dispose(): Promise { this.stopRules() } -} \ No newline at end of file +} diff --git a/src/main/services/DataService.ts b/src/main/services/DataService.ts index 47442d4..c0b1f0e 100644 --- a/src/main/services/DataService.ts +++ b/src/main/services/DataService.ts @@ -53,7 +53,7 @@ export class DataService extends Service { const tags = await this.tagRepo.findAll() return { success: true, - data: tags.map(t => ({ id: t.id, name: t.name })) + data: tags.map((t) => ({ id: t.id, name: t.name })) } }) @@ -64,7 +64,7 @@ export class DataService extends Service { const tags = await this.tagRepo.findByStudent(studentId) return { success: true, - data: tags.map(t => ({ id: t.id, name: t.name })) + data: tags.map((t) => ({ id: t.id, name: t.name })) } }) @@ -90,12 +90,15 @@ export class DataService extends Service { } }) - this.mainCtx.handle('tags:updateStudentTags', async (event, studentId: number, tagIds: number[]) => { - if (!this.mainCtx.permissions.requirePermission(event, 'admin')) - return { success: false, message: 'Permission denied' } + this.mainCtx.handle( + 'tags:updateStudentTags', + async (event, studentId: number, tagIds: number[]) => { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } - await this.tagRepo.updateStudentTags(studentId, tagIds) - return { success: true } - }) + await this.tagRepo.updateStudentTags(studentId, tagIds) + return { success: true } + } + ) } } diff --git a/src/main/services/FileSystemService.ts b/src/main/services/FileSystemService.ts index 9de74b1..276623f 100644 --- a/src/main/services/FileSystemService.ts +++ b/src/main/services/FileSystemService.ts @@ -59,7 +59,10 @@ export class FileSystemService extends Service { } } - async readJsonFile(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise { + async readJsonFile( + relativePath: string, + folder: 'automatic' | 'script' = 'automatic' + ): Promise { await this.initPromise const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const filePath = join(baseDir, relativePath) @@ -90,7 +93,10 @@ export class FileSystemService extends Service { await fs.writeFile(filePath, content, 'utf-8') return true } catch (error) { - this.ctx.logger.warn('FileSystemService: Failed to write JSON file', { path: filePath, error }) + this.ctx.logger.warn('FileSystemService: Failed to write JSON file', { + path: filePath, + error + }) return false } } @@ -127,12 +133,18 @@ export class FileSystemService extends Service { await fs.writeFile(filePath, content, 'utf-8') return true } catch (error) { - this.ctx.logger.warn('FileSystemService: Failed to write text file', { path: filePath, error }) + this.ctx.logger.warn('FileSystemService: Failed to write text file', { + path: filePath, + error + }) return false } } - async deleteFile(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise { + async deleteFile( + relativePath: string, + folder: 'automatic' | 'script' = 'automatic' + ): Promise { await this.initPromise const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const filePath = join(baseDir, relativePath) @@ -180,7 +192,10 @@ export class FileSystemService extends Service { } } - async fileExists(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise { + async fileExists( + relativePath: string, + folder: 'automatic' | 'script' = 'automatic' + ): Promise { await this.initPromise const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const filePath = join(baseDir, relativePath) @@ -193,7 +208,7 @@ export class FileSystemService extends Service { } } -private get mainCtx() { + private get mainCtx() { return this.ctx as MainContext } @@ -203,35 +218,50 @@ private get mainCtx() { return { success: true, data: this.getConfigStructure() } }) - this.mainCtx.handle('fs:readJson', async (_event, relativePath: string, folder: 'automatic' | 'script') => { - await this.initPromise - const data = await this.readJsonFile(relativePath, folder) - return { success: true, data } - }) + this.mainCtx.handle( + 'fs:readJson', + async (_event, relativePath: string, folder: 'automatic' | 'script') => { + await this.initPromise + const data = await this.readJsonFile(relativePath, folder) + return { success: true, data } + } + ) - this.mainCtx.handle('fs:writeJson', async (_event, relativePath: string, data: any, folder: 'automatic' | 'script') => { - await this.initPromise - const success = await this.writeJsonFile(relativePath, data, folder) - return { success } - }) + this.mainCtx.handle( + 'fs:writeJson', + async (_event, relativePath: string, data: any, folder: 'automatic' | 'script') => { + await this.initPromise + const success = await this.writeJsonFile(relativePath, data, folder) + return { success } + } + ) - this.mainCtx.handle('fs:readText', async (_event, relativePath: string, folder: 'automatic' | 'script') => { - await this.initPromise - const content = await this.readTextFile(relativePath, folder) - return { success: true, data: content } - }) + this.mainCtx.handle( + 'fs:readText', + async (_event, relativePath: string, folder: 'automatic' | 'script') => { + await this.initPromise + const content = await this.readTextFile(relativePath, folder) + return { success: true, data: content } + } + ) - this.mainCtx.handle('fs:writeText', async (_event, content: string, relativePath: string, folder: 'automatic' | 'script') => { - await this.initPromise - const success = await this.writeTextFile(content, relativePath, folder) - return { success } - }) + this.mainCtx.handle( + 'fs:writeText', + async (_event, content: string, relativePath: string, folder: 'automatic' | 'script') => { + await this.initPromise + const success = await this.writeTextFile(content, relativePath, folder) + return { success } + } + ) - this.mainCtx.handle('fs:deleteFile', async (_event, relativePath: string, folder: 'automatic' | 'script') => { - await this.initPromise - const success = await this.deleteFile(relativePath, folder) - return { success } - }) + this.mainCtx.handle( + 'fs:deleteFile', + async (_event, relativePath: string, folder: 'automatic' | 'script') => { + await this.initPromise + const success = await this.deleteFile(relativePath, folder) + return { success } + } + ) this.mainCtx.handle('fs:listFiles', async (_event, folder: 'automatic' | 'script') => { await this.initPromise @@ -239,10 +269,13 @@ private get mainCtx() { return { success: true, data: files } }) - this.mainCtx.handle('fs:fileExists', async (_event, relativePath: string, folder: 'automatic' | 'script') => { - await this.initPromise - const exists = await this.fileExists(relativePath, folder) - return { success: true, data: exists } - }) + this.mainCtx.handle( + 'fs:fileExists', + async (_event, relativePath: string, folder: 'automatic' | 'script') => { + await this.initPromise + const exists = await this.fileExists(relativePath, folder) + return { success: true, data: exists } + } + ) } } diff --git a/src/main/services/HttpServerService.ts b/src/main/services/HttpServerService.ts index 8ad3c5d..74e2552 100644 --- a/src/main/services/HttpServerService.ts +++ b/src/main/services/HttpServerService.ts @@ -47,12 +47,12 @@ export class HttpServerService extends Service { try { await this.start(config) - return { - success: true, - data: { + return { + success: true, + data: { url: `http://${this.config.host}:${this.config.port}`, config: this.config - } + } } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' @@ -109,14 +109,16 @@ export class HttpServerService extends Service { if (await this.isPortAvailable(this.config.port)) { break } - + // 端口被占用,尝试下一个端口 this.config.port++ attempts++ } if (attempts === maxAttempts) { - throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${originalPort}`) + throw new Error( + `Could not find available port after ${maxAttempts} attempts starting from ${originalPort}` + ) } if (attempts > 0) { @@ -131,7 +133,7 @@ export class HttpServerService extends Service { // 配置中间件 this.app.use(express.json()) this.app.use(express.urlencoded({ extended: true })) - + // 简单的CORS处理 this.app.use((_: Request, res: Response, next: NextFunction) => { res.header('Access-Control-Allow-Origin', this.config.corsOrigin || '*') @@ -352,4 +354,4 @@ export class HttpServerService extends Service { }) }) } -} \ No newline at end of file +} diff --git a/src/main/services/ThemeService.ts b/src/main/services/ThemeService.ts index fbcd67f..43e11f2 100644 --- a/src/main/services/ThemeService.ts +++ b/src/main/services/ThemeService.ts @@ -55,7 +55,7 @@ export class ThemeService extends Service { const savedThemeId = await this.mainCtx.settings.getValue('current_theme_id' as any) if (savedThemeId && typeof savedThemeId === 'string') { const themes = this.getThemeList() - const exists = themes.some(t => t.id === savedThemeId) + const exists = themes.some((t) => t.id === savedThemeId) if (exists) { this.currentThemeId = savedThemeId } diff --git a/src/preload/index.ts b/src/preload/index.ts index da9b812..920e48c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -112,7 +112,7 @@ const api = { ipcRenderer.invoke('log:write', payload), registerUrlProtocol: () => ipcRenderer.invoke('app:register-url-protocol'), - + // HTTP Server httpServerStart: (config?: { port?: number; host?: string; corsOrigin?: string }) => ipcRenderer.invoke('http:server:start', config), @@ -135,7 +135,7 @@ const api = { ipcRenderer.invoke('fs:listFiles', folder ?? 'automatic'), fsFileExists: (relativePath: string, folder?: 'automatic' | 'script') => ipcRenderer.invoke('fs:fileExists', relativePath, folder ?? 'automatic'), - + // Generic invoke wrapper for backward compatibility with callers using `api.invoke` invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args) } diff --git a/src/preload/types.ts b/src/preload/types.ts index 52f7b84..1204fef 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -175,9 +175,13 @@ export interface electronApi { }) => Promise> registerUrlProtocol: () => Promise> - + // HTTP Server - httpServerStart: (config?: { port?: number; host?: string; corsOrigin?: string }) => Promise< + httpServerStart: (config?: { + port?: number + host?: string + corsOrigin?: string + }) => Promise< ipcResponse<{ url: string; config: { port: number; host: string; corsOrigin?: string } }> > httpServerStop: () => Promise> @@ -192,13 +196,30 @@ export interface electronApi { // File System fsGetConfigStructure: () => Promise> fsReadJson: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - fsWriteJson: (relativePath: string, data: any, folder?: 'automatic' | 'sscript') => Promise> - fsReadText: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - fsWriteText: (content: string, relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - fsDeleteFile: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> + fsWriteJson: ( + relativePath: string, + data: any, + folder?: 'automatic' | 'sscript' + ) => Promise> + fsReadText: ( + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> + fsWriteText: ( + content: string, + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> + fsDeleteFile: ( + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> fsListFiles: (folder?: 'automatic' | 'sscript') => Promise> - fsFileExists: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - + fsFileExists: ( + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> + // Generic invoke wrapper (minimal compatibility API) invoke?: (channel: string, ...args: any[]) => Promise } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 04ebe4d..b207581 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -139,67 +139,71 @@ function MainContent(): React.JSX.Element { /> - -
-

- 开发中画面,不代表最终品质 -

-

- SecScore Dev ({getPlatform()}-{getArchitecture()}) -

+ bottom: '2px', + left: '20px', + opacity: 0.6, + zIndex: 9999 + }} + > +

+ 开发中画面,不代表最终品质 +

+

+ SecScore Dev ({getPlatform()}-{getArchitecture()}) +

) } function getArchitecture(): string { - // 尝试从 userAgent 中获取架构信息 - const userAgent = navigator.userAgent.toLowerCase(); - + const userAgent = navigator.userAgent.toLowerCase() + if (userAgent.includes('arm64') || userAgent.includes('aarch64')) { - return 'ARM64'; + return 'ARM64' } else if (userAgent.includes('x64') || userAgent.includes('amd64')) { - return 'x64'; + return 'x64' } else if (userAgent.includes('i386') || userAgent.includes('i686')) { - return 'x86'; + return 'x86' } - + // 默认返回未知架构 - return userAgent; + return userAgent } function getPlatform(): string { - // 尝试从 userAgent 中获取平台信息 - const userAgent = navigator.userAgent.toLowerCase(); - + const userAgent = navigator.userAgent.toLowerCase() + if (userAgent.includes('windows')) { - return 'Windows'; + return 'Windows' } else if (userAgent.includes('mac')) { - return 'Mac'; + return 'Mac' } else if (userAgent.includes('linux')) { - return 'Linux'; + return 'Linux' } - return 'Unknown'; + return 'Unknown' } function App(): React.JSX.Element { return ( diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 22cbe50..2623030 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -52,7 +52,7 @@ export const AutoScoreManager: React.FC = () => { const fetchRules = async () => { if (!(window as any).api) return - + setLoading(true) try { // 权限检查:确保当前为 admin @@ -93,20 +93,23 @@ export const AutoScoreManager: React.FC = () => { }, []) const handleSubmit = async () => { - if (!(window as any).api) return; + if (!(window as any).api) return - const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & { timeUnit: string }; + const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & { + timeUnit: string + } if (!values.name || values.intervalMinutes == null || values.scoreValue == null) { - MessagePlugin.warning('请填写完整信息'); - return; + MessagePlugin.warning('请填写完整信息') + return } // 根据单位转换间隔时间 - const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes; + const intervalMinutes = + values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes // 确保 studentNames 是数组类型 - const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []; + const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value })) @@ -118,7 +121,7 @@ export const AutoScoreManager: React.FC = () => { scoreValue: values.scoreValue, reason: values.reason || `自动化加分 - ${values.name}`, triggers: triggersPayload - }; + } // 权限检查:仅管理员可创建/更新自动化 try { @@ -168,7 +171,9 @@ export const AutoScoreManager: React.FC = () => { setTriggerList([]) fetchRules() // 刷新自动化列表 } else { - MessagePlugin.error(res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')) + MessagePlugin.error( + res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败') + ) } } catch (error) { console.error('Failed to submit auto score rule:', error) @@ -268,11 +273,11 @@ export const AutoScoreManager: React.FC = () => { } const columns: PrimaryTableCol[] = [ - { - colKey: 'drag', - title: '排序', - cell: () => , - width: 60 + { + colKey: 'drag', + title: '排序', + cell: () => , + width: 60 }, { colKey: 'enabled', @@ -318,12 +323,7 @@ export const AutoScoreManager: React.FC = () => { } const studentList = row.studentNames.join(',\n') return ( - + {row.studentNames.length} 名学生 ) @@ -350,17 +350,10 @@ export const AutoScoreManager: React.FC = () => { width: 150, cell: ({ row }) => ( - - handleDelete(row.id)} - > + handleDelete(row.id)}> @@ -369,31 +362,57 @@ export const AutoScoreManager: React.FC = () => { ) } ] - const onDragSort = (params: any) => setRules(params.newData); - + const onDragSort = (params: any) => setRules(params.newData) + type TriggerDef = { id: number label: string description: string eventName: string - valueType?: "DatePicker" | "Input" + valueType?: 'DatePicker' | 'Input' } const allTriggers: TriggerDef[] = [ - { id: 1, label: '根据间隔时间触发', description: '当学生注册时触发自动化', eventName: 'interval_time_passed', valueType: 'DatePicker' }, - { id: 2, label: '按照学生标签触发', description: '当学生完成作业时触发自动化', eventName: 'student_tag_matched', valueType: 'Input' }, - { id: 3, label: '随机时间触发', description: '当随机时间到达时触发自动化', eventName: 'random_time_reached' } + { + id: 1, + label: '根据间隔时间触发', + description: '当学生注册时触发自动化', + eventName: 'interval_time_passed', + valueType: 'DatePicker' + }, + { + id: 2, + label: '按照学生标签触发', + description: '当学生完成作业时触发自动化', + eventName: 'student_tag_matched', + valueType: 'Input' + }, + { + id: 3, + label: '随机时间触发', + description: '当随机时间到达时触发自动化', + eventName: 'random_time_reached' + } ] const triggerOptions = allTriggers.map((t) => ({ label: t.label, value: t.eventName })) // triggerList items only need id, eventName, haveValue and value - type TriggerItem = { id: number; eventName: string; value?: string; valueType?: "DatePicker" | "Input" } + type TriggerItem = { + id: number + eventName: string + value?: string + valueType?: 'DatePicker' | 'Input' + } const [triggerList, setTriggerList] = useState([]) const handleTriggerChange = (id: number, value: string) => { const found = allTriggers.find((a) => a.eventName === value) - setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : t))) + setTriggerList((prev) => + prev.map((t) => + t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : t + ) + ) } const handleValueChange = (id: number, val: string) => { @@ -407,7 +426,15 @@ export const AutoScoreManager: React.FC = () => { const handleAddTrigger = () => { const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 const defaultTrigger = allTriggers[0] - setTriggerList((prev) => [...prev, { id: nextId, eventName: defaultTrigger.eventName, valueType: defaultTrigger.valueType, value: '' }]) + setTriggerList((prev) => [ + ...prev, + { + id: nextId, + eventName: defaultTrigger.eventName, + valueType: defaultTrigger.valueType, + value: '' + } + ]) } const triggerItems = triggerList @@ -440,24 +467,21 @@ export const AutoScoreManager: React.FC = () => { placeholder="请输入Value" style={{ width: '150px' }} value={String(triggerTest.value ?? '')} - onChange={(v) => handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))} + onChange={(v) => + handleValueChange(triggerTest.id, String((v as any).target?.value ?? v)) + } /> ) ) : null} )) - return (

自动化加分管理

-
+
{ @@ -497,43 +521,31 @@ export const AutoScoreManager: React.FC = () => { - - ({ label: student.name, value: student.name }))} + /> - +
- -
- +
{ pageSize, total: rules.length, onChange: (pageInfo) => setCurrentPage(pageInfo.current), - onPageSizeChange: (size) => setPageSize(size), + onPageSizeChange: (size) => setPageSize(size) }} style={{ color: 'var(--ss-text-main)' }} /> - - + + {triggerItems} - - + println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") -{/*
+ {/*

使用说明

  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • @@ -583,4 +600,4 @@ export const AutoScoreManager: React.FC = () => {
*/}
) -} \ No newline at end of file +} diff --git a/src/renderer/src/components/ScoreManager.tsx b/src/renderer/src/components/ScoreManager.tsx index 227b741..48f1b5b 100644 --- a/src/renderer/src/components/ScoreManager.tsx +++ b/src/renderer/src/components/ScoreManager.tsx @@ -138,9 +138,11 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { return } const values = form.getFieldsValue(true) as any - + // 支持多选学生 - const studentNames = Array.isArray(values.student_name) ? values.student_name : [values.student_name] + const studentNames = Array.isArray(values.student_name) + ? values.student_name + : [values.student_name] if (!studentNames || studentNames.length === 0 || !values.reason_content) { MessagePlugin.warning('请填写完整信息') return diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 4916cd8..a95eff9 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -62,7 +62,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission const [settleDialogVisible, setSettleDialogVisible] = useState(false) const [urlRegisterLoading, setUrlRegisterLoading] = useState(false) - + // HTTP Server状态 const [httpServerStatus, setHttpServerStatus] = useState<{ isRunning: boolean @@ -111,7 +111,8 @@ 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 } + if (change?.key === 'auto_score_enabled') + return { ...prev, auto_score_enabled: change.value } return prev }) }) @@ -259,7 +260,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission if (!(window as any).api) return setSettleDialogVisible(true) } - + // HTTP Server函数 const refreshHttpServerStatus = async () => { if (!(window as any).api) return @@ -275,7 +276,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission console.error('Failed to refresh HTTP server status:', error) } } - + const handleHttpServerStart = async () => { if (!(window as any).api) return setHttpLoading(true) @@ -291,7 +292,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission if (httpCorsOrigin.trim()) { config.corsOrigin = httpCorsOrigin.trim() } - + const res = await (window as any).api.httpServerStart(config) if (res.success) { MessagePlugin.success(`HTTP服务器已启动: ${res.data.url}`) @@ -306,7 +307,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission setHttpLoading(false) } } - + const handleHttpServerStop = async () => { if (!(window as any).api) return setHttpLoading(true) @@ -632,14 +633,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission > 通过HTTP API提供学生分数查询服务,可在局域网内访问。 - +
- + {httpServerStatus?.isRunning ? '运行中' : '已停止'} {httpServerStatus?.isRunning && ( @@ -649,7 +647,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission )}
- + = ({ permission disabled={!canAdmin || httpLoading} /> - + = ({ permission disabled={!canAdmin || httpLoading} /> - + = ({ permission style={{ width: '200px' }} disabled={!canAdmin || httpLoading} /> -
+
* 表示允许所有来源,建议在生产环境中设置具体域名
- + - + -
+
• GET /health - 健康检查
• GET /api/students/scores - 获取所有学生分数
• GET /api/students/:id/score - 获取单个学生分数
@@ -724,7 +726,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission - +
diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index b527776..e5da4dd 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -68,7 +68,9 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
- }> 主页 + }> + {' '} + 主页 } disabled={permission !== 'admin'}> 学生管理 @@ -76,13 +78,13 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps): }> 积分管理 - }> + }> 自动加分 - }> + }> 排行榜 - }> + }> 结算历史 } disabled={permission !== 'admin'}> diff --git a/src/renderer/src/components/StudentManager.tsx b/src/renderer/src/components/StudentManager.tsx index cd360f3..8fb87fb 100644 --- a/src/renderer/src/components/StudentManager.tsx +++ b/src/renderer/src/components/StudentManager.tsx @@ -382,7 +382,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { {tags.length === 0 ? ( 无标签 ) : ( - tags.slice(0, 3).map(tag => ( + tags.slice(0, 3).map((tag) => ( {tag} @@ -449,7 +449,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { pageSize, total: data.length, onChange: (pageInfo) => setCurrentPage(pageInfo.current), - onPageSizeChange: (size) => setPageSize(size), + onPageSizeChange: (size) => setPageSize(size) }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} /> diff --git a/src/renderer/src/components/TagEditorDialog.tsx b/src/renderer/src/components/TagEditorDialog.tsx index 83becbf..4a46f44 100644 --- a/src/renderer/src/components/TagEditorDialog.tsx +++ b/src/renderer/src/components/TagEditorDialog.tsx @@ -26,8 +26,8 @@ export const TagEditorDialog: React.FC = ({ const [allTags, setAllTags] = useState([]) const [selectedTagIds, setSelectedTagIds] = useState>(new Set(initialTagIds)) const { currentTheme } = useTheme() // 获取当前主题 - - const themeMode = currentTheme?.mode || 'light'; // 默认为 light + + const themeMode = currentTheme?.mode || 'light' // 默认为 light useEffect(() => { if (visible) { @@ -59,10 +59,10 @@ export const TagEditorDialog: React.FC = ({ return } - if (allTags.some(t => t.name === trimmed)) { - const existingTag = allTags.find(t => t.name === trimmed) + if (allTags.some((t) => t.name === trimmed)) { + const existingTag = allTags.find((t) => t.name === trimmed) if (existingTag) { - setSelectedTagIds(prev => new Set([...prev, existingTag.id])) + setSelectedTagIds((prev) => new Set([...prev, existingTag.id])) } setInputValue('') return @@ -71,8 +71,8 @@ export const TagEditorDialog: React.FC = ({ try { const res = await (window as any).api.tagsCreate(trimmed) if (res.success && res.data) { - setAllTags(prev => [...prev, res.data]) - setSelectedTagIds(prev => new Set([...prev, res.data.id])) + setAllTags((prev) => [...prev, res.data]) + setSelectedTagIds((prev) => new Set([...prev, res.data.id])) setInputValue('') } else { MessagePlugin.error(res.message || '添加标签失败') @@ -84,7 +84,7 @@ export const TagEditorDialog: React.FC = ({ } const handleToggleTag = (tagId: number) => { - setSelectedTagIds(prev => { + setSelectedTagIds((prev) => { const newSet = new Set(prev) if (newSet.has(tagId)) { newSet.delete(tagId) @@ -99,8 +99,8 @@ export const TagEditorDialog: React.FC = ({ try { const res = await (window as any).api.tagsDelete(tagId) if (res.success) { - setAllTags(prev => prev.filter(t => t.id !== tagId)) - setSelectedTagIds(prev => { + setAllTags((prev) => prev.filter((t) => t.id !== tagId)) + setSelectedTagIds((prev) => { const newSet = new Set(prev) newSet.delete(tagId) return newSet @@ -115,7 +115,7 @@ export const TagEditorDialog: React.FC = ({ } } -/* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent }) => { + /* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent }) => { if (context.e.key === 'Enter') { context.e.preventDefault() handleAddTag() @@ -127,8 +127,8 @@ export const TagEditorDialog: React.FC = ({ onClose() } - const selectedTags = allTags.filter(t => selectedTagIds.has(t.id)) - const availableTags = allTags.filter(t => !selectedTagIds.has(t.id)) + const selectedTags = allTags.filter((t) => selectedTagIds.has(t.id)) + const availableTags = allTags.filter((t) => !selectedTagIds.has(t.id)) return ( = ({ value={inputValue} onChange={setInputValue} onEnter={handleAddTag} -/* maxLength={50} */ + /* maxLength={50} */ style={{ flex: 1 }} /> -
@@ -166,7 +162,14 @@ export const TagEditorDialog: React.FC = ({
已选标签(点击取消)
-
+
{selectedTags.length === 0 ? ( 未选择标签 ) : ( @@ -193,7 +196,14 @@ export const TagEditorDialog: React.FC = ({
可选标签(点击选择)
-
+
{availableTags.length === 0 ? ( 无可用标签 ) : ( @@ -218,4 +228,4 @@ export const TagEditorDialog: React.FC = ({
) -} \ No newline at end of file +}