过下pnpm lint

This commit is contained in:
NanGua-QWQ
2026-02-12 22:29:33 +08:00
parent c64abc070e
commit c11c4e958f
24 changed files with 471 additions and 356 deletions
+9 -1
View File
@@ -29,7 +29,15 @@ export class DbManager extends Service {
this.dataSource = new DataSource({ this.dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',
database: dbPath, database: dbPath,
entities: [StudentEntity, ReasonEntity, ScoreEventEntity, SettlementEntity, SettingEntity, TagEntity, StudentTagEntity], entities: [
StudentEntity,
ReasonEntity,
ScoreEventEntity,
SettlementEntity,
SettingEntity,
TagEntity,
StudentTagEntity
],
migrations, migrations,
synchronize: false, synchronize: false,
logging: false logging: false
+11 -7
View File
@@ -237,7 +237,10 @@ app.whenReady().then(async () => {
(p) => new PermissionService(p.get(MainContext)) (p) => new PermissionService(p.get(MainContext))
) )
services.addSingleton(AuthService, (p) => new AuthService(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( services.addSingleton(
StudentRepositoryToken, StudentRepositoryToken,
@@ -266,10 +269,7 @@ app.whenReady().then(async () => {
TrayServiceToken, TrayServiceToken,
(p) => new TrayService(p.get(MainContext), config.window) (p) => new TrayService(p.get(MainContext), config.window)
) )
services.addSingleton( services.addSingleton(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext)))
AutoScoreServiceToken,
(p) => new AutoScoreService(p.get(MainContext))
)
services.addSingleton( services.addSingleton(
HttpServerServiceToken, HttpServerServiceToken,
(p) => new HttpServerService(p.get(MainContext)) (p) => new HttpServerService(p.get(MainContext))
@@ -294,7 +294,9 @@ app.whenReady().then(async () => {
services.get(ReasonRepositoryToken) services.get(ReasonRepositoryToken)
services.get(EventRepositoryToken) services.get(EventRepositoryToken)
services.get(SettlementRepositoryToken) 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() await theme.init()
if (!process.env.HEADLESS) { if (!process.env.HEADLESS) {
services.get(WindowManagerToken) services.get(WindowManagerToken)
@@ -321,7 +323,9 @@ app.whenReady().then(async () => {
services.get(ReasonRepositoryToken) services.get(ReasonRepositoryToken)
services.get(EventRepositoryToken) services.get(EventRepositoryToken)
services.get(SettlementRepositoryToken) 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() await theme.init()
if (!process.env.HEADLESS) { if (!process.env.HEADLESS) {
services.get(WindowManagerToken) services.get(WindowManagerToken)
+1 -1
View File
@@ -80,7 +80,7 @@ export class StudentRepository extends Service {
order: { score: 'DESC', name: 'ASC' } order: { score: 'DESC', name: 'ASC' }
}) })
return entities.map(entity => { return entities.map((entity) => {
let tags: string[] = [] let tags: string[] = []
try { try {
tags = Array.isArray(entity.tags) ? entity.tags : JSON.parse(entity.tags || '[]') tags = Array.isArray(entity.tags) ? entity.tags : JSON.parse(entity.tags || '[]')
+1 -1
View File
@@ -43,7 +43,7 @@ export class TagRepository {
.orderBy('st.created_at', 'ASC') .orderBy('st.created_at', 'ASC')
.getMany() .getMany()
return relations.map(r => r.tag).filter(Boolean) return relations.map((r) => r.tag).filter(Boolean)
} }
async addTagToStudent(studentId: number, tagId: number): Promise<void> { async addTagToStudent(studentId: number, tagId: number): Promise<void> {
+68 -61
View File
@@ -91,20 +91,23 @@ export const AutoScoreManager: React.FC = () => {
}, []) }, [])
const handleSubmit = async () => { 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) { if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
MessagePlugin.warning('请填写完整信息'); MessagePlugin.warning('请填写完整信息')
return; return
} }
// 根据单位转换间隔时间 // 根据单位转换间隔时间
const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes; const intervalMinutes =
values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes
// 确保 studentNames 是数组类型 // 确保 studentNames 是数组类型
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []; const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
const ruleData = { const ruleData = {
enabled: true, enabled: true,
@@ -112,8 +115,8 @@ export const AutoScoreManager: React.FC = () => {
intervalMinutes, intervalMinutes,
studentNames, studentNames,
scoreValue: values.scoreValue, scoreValue: values.scoreValue,
reason: values.reason || `自动化加分 - ${values.name}`, reason: values.reason || `自动化加分 - ${values.name}`
}; }
// 权限检查:仅管理员可创建/更新自动化 // 权限检查:仅管理员可创建/更新自动化
try { try {
@@ -153,7 +156,9 @@ export const AutoScoreManager: React.FC = () => {
setEditingRuleId(null) setEditingRuleId(null)
fetchRules() // 刷新自动化列表 fetchRules() // 刷新自动化列表
} else { } else {
MessagePlugin.error(res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')) MessagePlugin.error(
res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
)
} }
} catch (error) { } catch (error) {
console.error('Failed to submit auto score rule:', error) console.error('Failed to submit auto score rule:', error)
@@ -288,12 +293,7 @@ export const AutoScoreManager: React.FC = () => {
} }
const studentList = row.studentNames.join(',\n') const studentList = row.studentNames.join(',\n')
return ( return (
<TooltipLite <TooltipLite content={studentList} showArrow placement="mouse" theme="default">
content={studentList}
showArrow
placement="mouse"
theme="default"
>
{row.studentNames.length} {row.studentNames.length}
</TooltipLite> </TooltipLite>
) )
@@ -320,17 +320,10 @@ export const AutoScoreManager: React.FC = () => {
width: 150, width: 150,
cell: ({ row }) => ( cell: ({ row }) => (
<Space> <Space>
<Button <Button size="small" variant="outline" onClick={() => handleEdit(row)}>
size="small"
variant="outline"
onClick={() => handleEdit(row)}
>
</Button> </Button>
<Popconfirm <Popconfirm content="确定要删除这条自动化吗?" onConfirm={() => handleDelete(row.id)}>
content="确定要删除这条自动化吗?"
onConfirm={() => handleDelete(row.id)}
>
<Button size="small" variant="outline" theme="danger"> <Button size="small" variant="outline" theme="danger">
</Button> </Button>
@@ -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 = [ const triggerOptions = [
@@ -350,12 +343,30 @@ export const AutoScoreManager: React.FC = () => {
{ label: '参与活动', value: 'event_participated' }, { label: '参与活动', value: 'event_participated' },
{ label: '签到', value: 'check_in' }, { label: '签到', value: 'check_in' },
{ label: '其他自定义事件', value: 'custom_event' } { label: '其他自定义事件', value: 'custom_event' }
]; ]
const initialTriggers = [ const initialTriggers = [
{ id: 1, triggerEvent: triggerOptions[1], description: '当学生登录时触发自动化', haveValue: true, value: 1 }, {
{ id: 2, triggerEvent: triggerOptions[2], description: '当学生完成作业时触发自动化', haveValue: true, value: '12' }, id: 1,
{ id: 3, triggerEvent: triggerOptions[3], description: '当学生考试通过时触发自动化', haveValue: false, value: null } 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) const [triggerList, setTriggerList] = useState(initialTriggers)
@@ -363,7 +374,9 @@ export const AutoScoreManager: React.FC = () => {
const handleTriggerChange = (id: number, value: string) => { const handleTriggerChange = (id: number, value: string) => {
setTriggerList((prev) => setTriggerList((prev) =>
prev.map((t) => 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 handleAddTrigger = () => {
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 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 const triggerItems = triggerList
@@ -403,7 +419,9 @@ export const AutoScoreManager: React.FC = () => {
placeholder="请输入Value" placeholder="请输入Value"
style={{ width: '150px' }} style={{ width: '150px' }}
value={String(triggerTest.value)} 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} ) : null}
</div> </div>
@@ -414,11 +432,7 @@ export const AutoScoreManager: React.FC = () => {
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}></h2> <h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}></h2>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}> <Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Form <Form form={form} labelWidth={100} onReset={handleResetForm}>
form={form}
labelWidth={100}
onReset={handleResetForm}
>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<Form.FormItem <Form.FormItem
label="自动化名称" label="自动化名称"
@@ -458,10 +472,7 @@ export const AutoScoreManager: React.FC = () => {
<InputNumber placeholder="例如:1(每次加1分)" /> <InputNumber placeholder="例如:1(每次加1分)" />
</Form.FormItem> </Form.FormItem>
<Form.FormItem <Form.FormItem label="适用学生" name="studentNames">
label="适用学生"
name="studentNames"
>
<Select <Select
filterable filterable
multiple multiple
@@ -470,31 +481,22 @@ export const AutoScoreManager: React.FC = () => {
/> />
</Form.FormItem> </Form.FormItem>
<Form.FormItem <Form.FormItem label="加分理由" name="reason">
label="加分理由"
name="reason"
>
<Input placeholder="例如:每日签到奖励" /> <Input placeholder="例如:每日签到奖励" />
</Form.FormItem> </Form.FormItem>
</div> </div>
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}> <div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
<Button <Button theme="primary" onClick={handleSubmit}>
theme="primary"
onClick={handleSubmit}
>
{editingRuleId !== null ? '更新自动化' : '添加自动化'} {editingRuleId !== null ? '更新自动化' : '添加自动化'}
</Button> </Button>
<Button <Button type="reset" variant="outline">
type="reset"
variant="outline"
>
{editingRuleId !== null ? '取消编辑' : '重置表单'} {editingRuleId !== null ? '取消编辑' : '重置表单'}
</Button> </Button>
</div> </div>
</Form> </Form>
</Card> </Card>
<Card style={{ marginBottom: '24px',backgroundColor: 'var(--ss-card-bg)' }}> <Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Table <Table
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)} data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
columns={columns} columns={columns}
@@ -508,32 +510,37 @@ export const AutoScoreManager: React.FC = () => {
pageSize, pageSize,
total: rules.length, total: rules.length,
onChange: (pageInfo) => setCurrentPage(pageInfo.current), onChange: (pageInfo) => setCurrentPage(pageInfo.current),
onPageSizeChange: (size) => setPageSize(size), onPageSizeChange: (size) => setPageSize(size)
}} }}
style={{ color: 'var(--ss-text-main)' }} style={{ color: 'var(--ss-text-main)' }}
/> />
</Card> </Card>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }} title="当以下事件触发时" headerBordered> <Card
<Space style={{ display: "grid" }}> style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
title="当以下事件触发时"
headerBordered
>
<Space style={{ display: 'grid' }}>
{triggerItems} {triggerItems}
<Button <Button
theme="default" theme="default"
variant="text" variant="text"
style={{ fontWeight: 'bolder', fontSize: 15 }} style={{ fontWeight: 'bolder', fontSize: 15 }}
icon={<AddIcon strokeWidth={3}/>} icon={<AddIcon strokeWidth={3} />}
onClick={handleAddTrigger}> onClick={handleAddTrigger}
>
</Button> </Button>
</Space> </Space>
</Card> </Card>
<Card style={{ marginBottom: '24px' , backgroundColor: 'var(--ss-card-bg)' }}> <Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<SyntaxHighlighter language="javascript" style={prism} showLineNumbers> <SyntaxHighlighter language="javascript" style={prism} showLineNumbers>
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
</SyntaxHighlighter> </SyntaxHighlighter>
</Card> </Card>
{/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}> {/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}>
<h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3> <h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3>
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}> <ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li> <li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
+6 -6
View File
@@ -201,7 +201,7 @@ export class AutoScoreService extends Service {
// 否则只对指定的学生加分 // 否则只对指定的学生加分
const allStudents = await studentRepo.findAll() const allStudents = await studentRepo.findAll()
for (const name of rule.studentNames) { for (const name of rule.studentNames) {
const student = allStudents.find(s => s.name === name) const student = allStudents.find((s) => s.name === name)
if (student) { if (student) {
studentsToScore.push(student) studentsToScore.push(student)
} }
@@ -236,7 +236,7 @@ export class AutoScoreService extends Service {
} }
async addRule(rule: Omit<AutoScoreRule, 'id' | 'lastExecuted'>): Promise<number> { 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 newId = this.rules.length > 0 ? Math.max(...this.rules.map((r) => r.id)) + 1 : 1
const newRule: AutoScoreRule = { const newRule: AutoScoreRule = {
...rule, ...rule,
id: newId, id: newId,
@@ -254,7 +254,7 @@ export class AutoScoreService extends Service {
} }
async updateRule(rule: Partial<AutoScoreRule> & { id: number }): Promise<boolean> { async updateRule(rule: Partial<AutoScoreRule> & { id: number }): Promise<boolean> {
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 if (index === -1) return false
// 停止旧的定时器 // 停止旧的定时器
@@ -280,7 +280,7 @@ export class AutoScoreService extends Service {
} }
async deleteRule(ruleId: number): Promise<boolean> { async deleteRule(ruleId: number): Promise<boolean> {
const index = this.rules.findIndex(r => r.id === ruleId) const index = this.rules.findIndex((r) => r.id === ruleId)
if (index === -1) return false if (index === -1) return false
// 停止定时器 // 停止定时器
@@ -298,7 +298,7 @@ export class AutoScoreService extends Service {
} }
async toggleRule(ruleId: number, enabled: boolean): Promise<boolean> { async toggleRule(ruleId: number, enabled: boolean): Promise<boolean> {
const rule = this.rules.find(r => r.id === ruleId) const rule = this.rules.find((r) => r.id === ruleId)
if (!rule) return false if (!rule) return false
rule.enabled = enabled rule.enabled = enabled
@@ -324,7 +324,7 @@ export class AutoScoreService extends Service {
} }
isEnabled(): boolean { isEnabled(): boolean {
return this.rules.some(rule => rule.enabled) return this.rules.some((rule) => rule.enabled)
} }
async restart() { async restart() {
+7 -4
View File
@@ -53,7 +53,7 @@ export class DataService extends Service {
const tags = await this.tagRepo.findAll() const tags = await this.tagRepo.findAll()
return { return {
success: true, 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) const tags = await this.tagRepo.findByStudent(studentId)
return { return {
success: true, 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[]) => { this.mainCtx.handle(
'tags:updateStudentTags',
async (event, studentId: number, tagIds: number[]) => {
if (!this.mainCtx.permissions.requirePermission(event, 'admin')) if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
return { success: false, message: 'Permission denied' } return { success: false, message: 'Permission denied' }
await this.tagRepo.updateStudentTags(studentId, tagIds) await this.tagRepo.updateStudentTags(studentId, tagIds)
return { success: true } return { success: true }
}) }
)
} }
} }
+51 -18
View File
@@ -59,7 +59,10 @@ export class FileSystemService extends Service {
} }
} }
async readJsonFile<T = any>(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise<T | null> { async readJsonFile<T = any>(
relativePath: string,
folder: 'automatic' | 'script' = 'automatic'
): Promise<T | null> {
await this.initPromise await this.initPromise
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
const filePath = join(baseDir, relativePath) const filePath = join(baseDir, relativePath)
@@ -90,7 +93,10 @@ export class FileSystemService extends Service {
await fs.writeFile(filePath, content, 'utf-8') await fs.writeFile(filePath, content, 'utf-8')
return true return true
} catch (error) { } 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 return false
} }
} }
@@ -127,12 +133,18 @@ export class FileSystemService extends Service {
await fs.writeFile(filePath, content, 'utf-8') await fs.writeFile(filePath, content, 'utf-8')
return true return true
} catch (error) { } 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 return false
} }
} }
async deleteFile(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise<boolean> { async deleteFile(
relativePath: string,
folder: 'automatic' | 'script' = 'automatic'
): Promise<boolean> {
await this.initPromise await this.initPromise
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
const filePath = join(baseDir, relativePath) const filePath = join(baseDir, relativePath)
@@ -180,7 +192,10 @@ export class FileSystemService extends Service {
} }
} }
async fileExists(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise<boolean> { async fileExists(
relativePath: string,
folder: 'automatic' | 'script' = 'automatic'
): Promise<boolean> {
await this.initPromise await this.initPromise
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
const filePath = join(baseDir, relativePath) 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 return this.ctx as MainContext
} }
@@ -203,35 +218,50 @@ private get mainCtx() {
return { success: true, data: this.getConfigStructure() } return { success: true, data: this.getConfigStructure() }
}) })
this.mainCtx.handle('fs:readJson', async (_event, relativePath: string, folder: 'automatic' | 'script') => { this.mainCtx.handle(
'fs:readJson',
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
await this.initPromise await this.initPromise
const data = await this.readJsonFile(relativePath, folder) const data = await this.readJsonFile(relativePath, folder)
return { success: true, data } return { success: true, data }
}) }
)
this.mainCtx.handle('fs:writeJson', async (_event, relativePath: string, data: any, folder: 'automatic' | 'script') => { this.mainCtx.handle(
'fs:writeJson',
async (_event, relativePath: string, data: any, folder: 'automatic' | 'script') => {
await this.initPromise await this.initPromise
const success = await this.writeJsonFile(relativePath, data, folder) const success = await this.writeJsonFile(relativePath, data, folder)
return { success } return { success }
}) }
)
this.mainCtx.handle('fs:readText', async (_event, relativePath: string, folder: 'automatic' | 'script') => { this.mainCtx.handle(
'fs:readText',
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
await this.initPromise await this.initPromise
const content = await this.readTextFile(relativePath, folder) const content = await this.readTextFile(relativePath, folder)
return { success: true, data: content } return { success: true, data: content }
}) }
)
this.mainCtx.handle('fs:writeText', async (_event, content: string, relativePath: string, folder: 'automatic' | 'script') => { this.mainCtx.handle(
'fs:writeText',
async (_event, content: string, relativePath: string, folder: 'automatic' | 'script') => {
await this.initPromise await this.initPromise
const success = await this.writeTextFile(content, relativePath, folder) const success = await this.writeTextFile(content, relativePath, folder)
return { success } return { success }
}) }
)
this.mainCtx.handle('fs:deleteFile', async (_event, relativePath: string, folder: 'automatic' | 'script') => { this.mainCtx.handle(
'fs:deleteFile',
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
await this.initPromise await this.initPromise
const success = await this.deleteFile(relativePath, folder) const success = await this.deleteFile(relativePath, folder)
return { success } return { success }
}) }
)
this.mainCtx.handle('fs:listFiles', async (_event, folder: 'automatic' | 'script') => { this.mainCtx.handle('fs:listFiles', async (_event, folder: 'automatic' | 'script') => {
await this.initPromise await this.initPromise
@@ -239,10 +269,13 @@ private get mainCtx() {
return { success: true, data: files } return { success: true, data: files }
}) })
this.mainCtx.handle('fs:fileExists', async (_event, relativePath: string, folder: 'automatic' | 'script') => { this.mainCtx.handle(
'fs:fileExists',
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
await this.initPromise await this.initPromise
const exists = await this.fileExists(relativePath, folder) const exists = await this.fileExists(relativePath, folder)
return { success: true, data: exists } return { success: true, data: exists }
}) }
)
} }
} }
+3 -1
View File
@@ -116,7 +116,9 @@ export class HttpServerService extends Service {
} }
if (attempts === maxAttempts) { 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) { if (attempts > 0) {
+1 -1
View File
@@ -55,7 +55,7 @@ export class ThemeService extends Service {
const savedThemeId = await this.mainCtx.settings.getValue('current_theme_id' as any) const savedThemeId = await this.mainCtx.settings.getValue('current_theme_id' as any)
if (savedThemeId && typeof savedThemeId === 'string') { if (savedThemeId && typeof savedThemeId === 'string') {
const themes = this.getThemeList() const themes = this.getThemeList()
const exists = themes.some(t => t.id === savedThemeId) const exists = themes.some((t) => t.id === savedThemeId)
if (exists) { if (exists) {
this.currentThemeId = savedThemeId this.currentThemeId = savedThemeId
} }
+27 -6
View File
@@ -177,7 +177,11 @@ export interface electronApi {
registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>> registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>>
// HTTP Server // 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 } }> ipcResponse<{ url: string; config: { port: number; host: string; corsOrigin?: string } }>
> >
httpServerStop: () => Promise<ipcResponse<void>> httpServerStop: () => Promise<ipcResponse<void>>
@@ -192,12 +196,29 @@ export interface electronApi {
// File System // File System
fsGetConfigStructure: () => Promise<ipcResponse<ConfigFolderStructure>> fsGetConfigStructure: () => Promise<ipcResponse<ConfigFolderStructure>>
fsReadJson: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<any>> fsReadJson: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<any>>
fsWriteJson: (relativePath: string, data: any, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<void>> fsWriteJson: (
fsReadText: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<string | null>> relativePath: string,
fsWriteText: (content: string, relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<void>> data: any,
fsDeleteFile: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<void>> folder?: 'automatic' | 'sscript'
) => Promise<ipcResponse<void>>
fsReadText: (
relativePath: string,
folder?: 'automatic' | 'sscript'
) => Promise<ipcResponse<string | null>>
fsWriteText: (
content: string,
relativePath: string,
folder?: 'automatic' | 'sscript'
) => Promise<ipcResponse<void>>
fsDeleteFile: (
relativePath: string,
folder?: 'automatic' | 'sscript'
) => Promise<ipcResponse<void>>
fsListFiles: (folder?: 'automatic' | 'sscript') => Promise<ipcResponse<ConfigFileInfo[]>> fsListFiles: (folder?: 'automatic' | 'sscript') => Promise<ipcResponse<ConfigFileInfo[]>>
fsFileExists: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<boolean>> fsFileExists: (
relativePath: string,
folder?: 'automatic' | 'sscript'
) => Promise<ipcResponse<boolean>>
// Generic invoke wrapper (minimal compatibility API) // Generic invoke wrapper (minimal compatibility API)
invoke?: (channel: string, ...args: any[]) => Promise<any> invoke?: (channel: string, ...args: any[]) => Promise<any>
+24 -20
View File
@@ -140,28 +140,34 @@ function MainContent(): React.JSX.Element {
</div> </div>
</Dialog> </Dialog>
<div style={{ <div
style={{
position: 'fixed', position: 'fixed',
display: 'flex', display: 'flex',
bottom: '2px', bottom: '2px',
left: '20px', left: '20px',
opacity: 0.6, opacity: 0.6,
zIndex: 9999 zIndex: 9999
}}> }}
<p style={{ >
<p
style={{
color: '#de2611', color: '#de2611',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: '13px', fontSize: '13px',
pointerEvents: 'none', pointerEvents: 'none'
}}> }}
>
, ,
</p> </p>
<p style={{ <p
style={{
color: '#44474b', color: '#44474b',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: '13px', fontSize: '13px',
paddingLeft: '5px', paddingLeft: '5px'
}}> }}
>
SecScore Dev ({getPlatform()}-{getArchitecture()}) SecScore Dev ({getPlatform()}-{getArchitecture()})
</p> </p>
</div> </div>
@@ -170,36 +176,34 @@ function MainContent(): React.JSX.Element {
} }
function getArchitecture(): string { function getArchitecture(): string {
// 尝试从 userAgent 中获取架构信息 // 尝试从 userAgent 中获取架构信息
const userAgent = navigator.userAgent.toLowerCase(); const userAgent = navigator.userAgent.toLowerCase()
if (userAgent.includes('arm64') || userAgent.includes('aarch64')) { if (userAgent.includes('arm64') || userAgent.includes('aarch64')) {
return 'ARM64'; return 'ARM64'
} else if (userAgent.includes('x64') || userAgent.includes('amd64')) { } else if (userAgent.includes('x64') || userAgent.includes('amd64')) {
return 'x64'; return 'x64'
} else if (userAgent.includes('i386') || userAgent.includes('i686')) { } else if (userAgent.includes('i386') || userAgent.includes('i686')) {
return 'x86'; return 'x86'
} }
// 默认返回未知架构 // 默认返回未知架构
return userAgent; return userAgent
} }
function getPlatform(): string { function getPlatform(): string {
// 尝试从 userAgent 中获取平台信息 // 尝试从 userAgent 中获取平台信息
const userAgent = navigator.userAgent.toLowerCase(); const userAgent = navigator.userAgent.toLowerCase()
if (userAgent.includes('windows')) { if (userAgent.includes('windows')) {
return 'Windows'; return 'Windows'
} else if (userAgent.includes('mac')) { } else if (userAgent.includes('mac')) {
return 'Mac'; return 'Mac'
} else if (userAgent.includes('linux')) { } else if (userAgent.includes('linux')) {
return 'Linux'; return 'Linux'
} }
return 'Unknown'; return 'Unknown'
} }
function App(): React.JSX.Element { function App(): React.JSX.Element {
return ( return (
@@ -93,20 +93,23 @@ export const AutoScoreManager: React.FC = () => {
}, []) }, [])
const handleSubmit = async () => { 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) { if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
MessagePlugin.warning('请填写完整信息'); MessagePlugin.warning('请填写完整信息')
return; return
} }
// 根据单位转换间隔时间 // 根据单位转换间隔时间
const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes; const intervalMinutes =
values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes
// 确保 studentNames 是数组类型 // 确保 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 })) const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value }))
@@ -118,7 +121,7 @@ export const AutoScoreManager: React.FC = () => {
scoreValue: values.scoreValue, scoreValue: values.scoreValue,
reason: values.reason || `自动化加分 - ${values.name}`, reason: values.reason || `自动化加分 - ${values.name}`,
triggers: triggersPayload triggers: triggersPayload
}; }
// 权限检查:仅管理员可创建/更新自动化 // 权限检查:仅管理员可创建/更新自动化
try { try {
@@ -168,7 +171,9 @@ export const AutoScoreManager: React.FC = () => {
setTriggerList([]) setTriggerList([])
fetchRules() // 刷新自动化列表 fetchRules() // 刷新自动化列表
} else { } else {
MessagePlugin.error(res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')) MessagePlugin.error(
res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
)
} }
} catch (error) { } catch (error) {
console.error('Failed to submit auto score rule:', error) console.error('Failed to submit auto score rule:', error)
@@ -318,12 +323,7 @@ export const AutoScoreManager: React.FC = () => {
} }
const studentList = row.studentNames.join(',\n') const studentList = row.studentNames.join(',\n')
return ( return (
<TooltipLite <TooltipLite content={studentList} showArrow placement="mouse" theme="default">
content={studentList}
showArrow
placement="mouse"
theme="default"
>
{row.studentNames.length} {row.studentNames.length}
</TooltipLite> </TooltipLite>
) )
@@ -350,17 +350,10 @@ export const AutoScoreManager: React.FC = () => {
width: 150, width: 150,
cell: ({ row }) => ( cell: ({ row }) => (
<Space> <Space>
<Button <Button size="small" variant="outline" onClick={() => handleEdit(row)}>
size="small"
variant="outline"
onClick={() => handleEdit(row)}
>
</Button> </Button>
<Popconfirm <Popconfirm content="确定要删除这条自动化吗?" onConfirm={() => handleDelete(row.id)}>
content="确定要删除这条自动化吗?"
onConfirm={() => handleDelete(row.id)}
>
<Button size="small" variant="outline" theme="danger"> <Button size="small" variant="outline" theme="danger">
</Button> </Button>
@@ -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 = { type TriggerDef = {
id: number id: number
label: string label: string
description: string description: string
eventName: string eventName: string
valueType?: "DatePicker" | "Input" valueType?: 'DatePicker' | 'Input'
} }
const allTriggers: TriggerDef[] = [ const allTriggers: TriggerDef[] = [
{ id: 1, label: '根据间隔时间触发', description: '当学生注册时触发自动化', eventName: 'interval_time_passed', valueType: 'DatePicker' }, {
{ id: 2, label: '按照学生标签触发', description: '当学生完成作业时触发自动化', eventName: 'student_tag_matched', valueType: 'Input' }, id: 1,
{ id: 3, label: '随机时间触发', description: '当随机时间到达时触发自动化', eventName: 'random_time_reached' } 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 })) const triggerOptions = allTriggers.map((t) => ({ label: t.label, value: t.eventName }))
// triggerList items only need id, eventName, haveValue and value // 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<TriggerItem[]>([]) const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
const handleTriggerChange = (id: number, value: string) => { const handleTriggerChange = (id: number, value: string) => {
const found = allTriggers.find((a) => a.eventName === value) 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) => { const handleValueChange = (id: number, val: string) => {
@@ -407,7 +426,15 @@ export const AutoScoreManager: React.FC = () => {
const handleAddTrigger = () => { const handleAddTrigger = () => {
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
const defaultTrigger = allTriggers[0] 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 const triggerItems = triggerList
@@ -440,24 +467,21 @@ export const AutoScoreManager: React.FC = () => {
placeholder="请输入Value" placeholder="请输入Value"
style={{ width: '150px' }} style={{ width: '150px' }}
value={String(triggerTest.value ?? '')} 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} ) : null}
</div> </div>
)) ))
return ( return (
<div style={{ padding: '24px' }}> <div style={{ padding: '24px' }}>
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}></h2> <h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}></h2>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}> <Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Form <Form form={form} labelWidth={100} onReset={handleResetForm}>
form={form}
labelWidth={100}
onReset={handleResetForm}
>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<Form.FormItem <Form.FormItem
label="自动化名称" label="自动化名称"
@@ -497,10 +521,7 @@ export const AutoScoreManager: React.FC = () => {
<InputNumber placeholder="例如:1(每次加1分)" /> <InputNumber placeholder="例如:1(每次加1分)" />
</Form.FormItem> </Form.FormItem>
<Form.FormItem <Form.FormItem label="适用学生" name="studentNames">
label="适用学生"
name="studentNames"
>
<Select <Select
filterable filterable
multiple multiple
@@ -509,31 +530,22 @@ export const AutoScoreManager: React.FC = () => {
/> />
</Form.FormItem> </Form.FormItem>
<Form.FormItem <Form.FormItem label="加分理由" name="reason">
label="加分理由"
name="reason"
>
<Input placeholder="例如:每日签到奖励" /> <Input placeholder="例如:每日签到奖励" />
</Form.FormItem> </Form.FormItem>
</div> </div>
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}> <div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
<Button <Button theme="primary" onClick={handleSubmit}>
theme="primary"
onClick={handleSubmit}
>
{editingRuleId !== null ? '更新自动化' : '添加自动化'} {editingRuleId !== null ? '更新自动化' : '添加自动化'}
</Button> </Button>
<Button <Button type="reset" variant="outline">
type="reset"
variant="outline"
>
{editingRuleId !== null ? '取消编辑' : '重置表单'} {editingRuleId !== null ? '取消编辑' : '重置表单'}
</Button> </Button>
</div> </div>
</Form> </Form>
</Card> </Card>
<Card style={{ marginBottom: '24px',backgroundColor: 'var(--ss-card-bg)' }}> <Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Table <Table
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)} data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
columns={columns} columns={columns}
@@ -547,32 +559,37 @@ export const AutoScoreManager: React.FC = () => {
pageSize, pageSize,
total: rules.length, total: rules.length,
onChange: (pageInfo) => setCurrentPage(pageInfo.current), onChange: (pageInfo) => setCurrentPage(pageInfo.current),
onPageSizeChange: (size) => setPageSize(size), onPageSizeChange: (size) => setPageSize(size)
}} }}
style={{ color: 'var(--ss-text-main)' }} style={{ color: 'var(--ss-text-main)' }}
/> />
</Card> </Card>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }} title="当以下事件触发时" headerBordered> <Card
<Space style={{ display: "grid" }}> style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
title="当以下事件触发时"
headerBordered
>
<Space style={{ display: 'grid' }}>
{triggerItems} {triggerItems}
<Button <Button
theme="default" theme="default"
variant="text" variant="text"
style={{ fontWeight: 'bolder', fontSize: 15 }} style={{ fontWeight: 'bolder', fontSize: 15 }}
icon={<AddIcon strokeWidth={3}/>} icon={<AddIcon strokeWidth={3} />}
onClick={handleAddTrigger}> onClick={handleAddTrigger}
>
</Button> </Button>
</Space> </Space>
</Card> </Card>
<Card style={{ marginBottom: '24px' , backgroundColor: 'var(--ss-card-bg)' }}> <Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<SyntaxHighlighter language="javascript" style={prism} showLineNumbers> <SyntaxHighlighter language="javascript" style={prism} showLineNumbers>
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
</SyntaxHighlighter> </SyntaxHighlighter>
</Card> </Card>
{/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}> {/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}>
<h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3> <h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3>
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}> <ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li> <li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
+3 -1
View File
@@ -140,7 +140,9 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const values = form.getFieldsValue(true) as any 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) { if (!studentNames || studentNames.length === 0 || !values.reason_content) {
MessagePlugin.warning('请填写完整信息') MessagePlugin.warning('请填写完整信息')
return return
+9 -7
View File
@@ -111,7 +111,8 @@ 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 } if (change?.key === 'auto_score_enabled')
return { ...prev, auto_score_enabled: change.value }
return prev return prev
}) })
}) })
@@ -636,10 +637,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
<Form labelWidth={120}> <Form labelWidth={120}>
<Form.FormItem label="服务器状态"> <Form.FormItem label="服务器状态">
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Tag <Tag theme={httpServerStatus?.isRunning ? 'success' : 'default'} variant="light">
theme={httpServerStatus?.isRunning ? 'success' : 'default'}
variant="light"
>
{httpServerStatus?.isRunning ? '运行中' : '已停止'} {httpServerStatus?.isRunning ? '运行中' : '已停止'}
</Tag> </Tag>
{httpServerStatus?.isRunning && ( {httpServerStatus?.isRunning && (
@@ -678,7 +676,9 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
style={{ width: '200px' }} style={{ width: '200px' }}
disabled={!canAdmin || httpLoading} disabled={!canAdmin || httpLoading}
/> />
<div style={{ marginTop: '4px', fontSize: '12px', color: 'var(--ss-text-secondary)' }}> <div
style={{ marginTop: '4px', fontSize: '12px', color: 'var(--ss-text-secondary)' }}
>
* *
</div> </div>
</Form.FormItem> </Form.FormItem>
@@ -713,7 +713,9 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
</Form.FormItem> </Form.FormItem>
<Form.FormItem label="API端点"> <Form.FormItem label="API端点">
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '12px' }}> <div
style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '12px' }}
>
<div> GET /health - </div> <div> GET /health - </div>
<div> GET /api/students/scores - </div> <div> GET /api/students/scores - </div>
<div> GET /api/students/:id/score - </div> <div> GET /api/students/:id/score - </div>
+3 -1
View File
@@ -68,7 +68,9 @@ 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'}>
@@ -382,7 +382,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
{tags.length === 0 ? ( {tags.length === 0 ? (
<span style={{ color: 'var(--ss-text-secondary)' }}></span> <span style={{ color: 'var(--ss-text-secondary)' }}></span>
) : ( ) : (
tags.slice(0, 3).map(tag => ( tags.slice(0, 3).map((tag) => (
<Tag key={tag} theme="primary" size="small"> <Tag key={tag} theme="primary" size="small">
{tag} {tag}
</Tag> </Tag>
@@ -449,7 +449,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
pageSize, pageSize,
total: data.length, total: data.length,
onChange: (pageInfo) => setCurrentPage(pageInfo.current), onChange: (pageInfo) => setCurrentPage(pageInfo.current),
onPageSizeChange: (size) => setPageSize(size), onPageSizeChange: (size) => setPageSize(size)
}} }}
style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}
/> />
+30 -20
View File
@@ -27,7 +27,7 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
const [selectedTagIds, setSelectedTagIds] = useState<Set<number>>(new Set(initialTagIds)) const [selectedTagIds, setSelectedTagIds] = useState<Set<number>>(new Set(initialTagIds))
const { currentTheme } = useTheme() // 获取当前主题 const { currentTheme } = useTheme() // 获取当前主题
const themeMode = currentTheme?.mode || 'light'; // 默认为 light const themeMode = currentTheme?.mode || 'light' // 默认为 light
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
@@ -59,10 +59,10 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
return return
} }
if (allTags.some(t => t.name === trimmed)) { if (allTags.some((t) => t.name === trimmed)) {
const existingTag = allTags.find(t => t.name === trimmed) const existingTag = allTags.find((t) => t.name === trimmed)
if (existingTag) { if (existingTag) {
setSelectedTagIds(prev => new Set([...prev, existingTag.id])) setSelectedTagIds((prev) => new Set([...prev, existingTag.id]))
} }
setInputValue('') setInputValue('')
return return
@@ -71,8 +71,8 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
try { try {
const res = await (window as any).api.tagsCreate(trimmed) const res = await (window as any).api.tagsCreate(trimmed)
if (res.success && res.data) { if (res.success && res.data) {
setAllTags(prev => [...prev, res.data]) setAllTags((prev) => [...prev, res.data])
setSelectedTagIds(prev => new Set([...prev, res.data.id])) setSelectedTagIds((prev) => new Set([...prev, res.data.id]))
setInputValue('') setInputValue('')
} else { } else {
MessagePlugin.error(res.message || '添加标签失败') MessagePlugin.error(res.message || '添加标签失败')
@@ -84,7 +84,7 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
} }
const handleToggleTag = (tagId: number) => { const handleToggleTag = (tagId: number) => {
setSelectedTagIds(prev => { setSelectedTagIds((prev) => {
const newSet = new Set(prev) const newSet = new Set(prev)
if (newSet.has(tagId)) { if (newSet.has(tagId)) {
newSet.delete(tagId) newSet.delete(tagId)
@@ -99,8 +99,8 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
try { try {
const res = await (window as any).api.tagsDelete(tagId) const res = await (window as any).api.tagsDelete(tagId)
if (res.success) { if (res.success) {
setAllTags(prev => prev.filter(t => t.id !== tagId)) setAllTags((prev) => prev.filter((t) => t.id !== tagId))
setSelectedTagIds(prev => { setSelectedTagIds((prev) => {
const newSet = new Set(prev) const newSet = new Set(prev)
newSet.delete(tagId) newSet.delete(tagId)
return newSet return newSet
@@ -115,7 +115,7 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
} }
} }
/* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => { /* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => {
if (context.e.key === 'Enter') { if (context.e.key === 'Enter') {
context.e.preventDefault() context.e.preventDefault()
handleAddTag() handleAddTag()
@@ -127,8 +127,8 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
onClose() onClose()
} }
const selectedTags = 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)) const availableTags = allTags.filter((t) => !selectedTagIds.has(t.id))
return ( return (
<Dialog <Dialog
@@ -149,14 +149,10 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
value={inputValue} value={inputValue}
onChange={setInputValue} onChange={setInputValue}
onEnter={handleAddTag} onEnter={handleAddTag}
/* maxLength={50} */ /* maxLength={50} */
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<Button <Button theme="primary" onClick={handleAddTag} disabled={!inputValue.trim()}>
theme="primary"
onClick={handleAddTag}
disabled={!inputValue.trim()}
>
</Button> </Button>
</div> </div>
@@ -166,7 +162,14 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
<div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}> <div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}>
</div> </div>
<div style={{ minHeight: '50px', padding: '12px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '4px' }}> <div
style={{
minHeight: '50px',
padding: '12px',
backgroundColor: 'var(--ss-card-bg)',
borderRadius: '4px'
}}
>
{selectedTags.length === 0 ? ( {selectedTags.length === 0 ? (
<span style={{ color: 'var(--ss-text-secondary)' }}></span> <span style={{ color: 'var(--ss-text-secondary)' }}></span>
) : ( ) : (
@@ -193,7 +196,14 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
<div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}> <div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}>
</div> </div>
<div style={{ minHeight: '50px', padding: '12px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '4px' }}> <div
style={{
minHeight: '50px',
padding: '12px',
backgroundColor: 'var(--ss-card-bg)',
borderRadius: '4px'
}}
>
{availableTags.length === 0 ? ( {availableTags.length === 0 ? (
<span style={{ color: 'var(--ss-text-secondary)' }}></span> <span style={{ color: 'var(--ss-text-secondary)' }}></span>
) : ( ) : (