mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
过下pnpm lint
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ export class StudentEntity {
|
||||
|
||||
@Column({ type: 'text', default: '[]' })
|
||||
tags!: string
|
||||
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
score!: number
|
||||
|
||||
|
||||
@@ -24,4 +24,4 @@ export class StudentTagEntity {
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
created_at!: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ export class TagEntity {
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updated_at!: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService')
|
||||
|
||||
+11
-7
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -76,4 +76,4 @@ export class TagRepository {
|
||||
await this.studentTagRepo.save(relation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AutoScoreRule>[] = [
|
||||
{
|
||||
colKey: 'drag',
|
||||
title: '排序',
|
||||
cell: () => <MoveIcon />,
|
||||
width: 60
|
||||
{
|
||||
colKey: 'drag',
|
||||
title: '排序',
|
||||
cell: () => <MoveIcon />,
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
colKey: 'enabled',
|
||||
@@ -288,12 +293,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
}
|
||||
const studentList = row.studentNames.join(',\n')
|
||||
return (
|
||||
<TooltipLite
|
||||
content={studentList}
|
||||
showArrow
|
||||
placement="mouse"
|
||||
theme="default"
|
||||
>
|
||||
<TooltipLite content={studentList} showArrow placement="mouse" theme="default">
|
||||
{row.studentNames.length} 名学生
|
||||
</TooltipLite>
|
||||
)
|
||||
@@ -320,17 +320,10 @@ export const AutoScoreManager: React.FC = () => {
|
||||
width: 150,
|
||||
cell: ({ row }) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outline"
|
||||
onClick={() => handleEdit(row)}
|
||||
>
|
||||
<Button size="small" variant="outline" onClick={() => handleEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
content="确定要删除这条自动化吗?"
|
||||
onConfirm={() => handleDelete(row.id)}
|
||||
>
|
||||
<Popconfirm content="确定要删除这条自动化吗?" onConfirm={() => handleDelete(row.id)}>
|
||||
<Button size="small" variant="outline" theme="danger">
|
||||
删除
|
||||
</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 = [
|
||||
@@ -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}
|
||||
</div>
|
||||
@@ -414,11 +432,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}>自动化加分管理</h2>
|
||||
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Form
|
||||
form={form}
|
||||
labelWidth={100}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
<Form form={form} labelWidth={100} onReset={handleResetForm}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
||||
<Form.FormItem
|
||||
label="自动化名称"
|
||||
@@ -431,13 +445,13 @@ export const AutoScoreManager: React.FC = () => {
|
||||
<Form.FormItem>
|
||||
<Space>
|
||||
<Form.FormItem
|
||||
label="间隔时间"
|
||||
name="intervalMinutes"
|
||||
rules={[
|
||||
{ required: true, message: '请输入间隔时间' },
|
||||
{ min: 1, message: '间隔时间至少为1分钟' }
|
||||
]}
|
||||
style={{ marginBottom: 0 }}
|
||||
label="间隔时间"
|
||||
name="intervalMinutes"
|
||||
rules={[
|
||||
{ required: true, message: '请输入间隔时间' },
|
||||
{ min: 1, message: '间隔时间至少为1分钟' }
|
||||
]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<InputNumber min={1} placeholder="例如:1(每隔1分钟/天执行一次)" />
|
||||
</Form.FormItem>
|
||||
@@ -458,43 +472,31 @@ export const AutoScoreManager: React.FC = () => {
|
||||
<InputNumber placeholder="例如:1(每次加1分)" />
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="适用学生"
|
||||
name="studentNames"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
multiple
|
||||
placeholder="请选择或搜索学生(留空表示所有学生)"
|
||||
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
||||
/>
|
||||
<Form.FormItem label="适用学生" name="studentNames">
|
||||
<Select
|
||||
filterable
|
||||
multiple
|
||||
placeholder="请选择或搜索学生(留空表示所有学生)"
|
||||
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
||||
/>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="加分理由"
|
||||
name="reason"
|
||||
>
|
||||
<Form.FormItem label="加分理由" name="reason">
|
||||
<Input placeholder="例如:每日签到奖励" />
|
||||
</Form.FormItem>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
||||
<Button
|
||||
theme="primary"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<Button theme="primary" onClick={handleSubmit}>
|
||||
{editingRuleId !== null ? '更新自动化' : '添加自动化'}
|
||||
</Button>
|
||||
<Button
|
||||
type="reset"
|
||||
variant="outline"
|
||||
>
|
||||
<Button type="reset" variant="outline">
|
||||
{editingRuleId !== null ? '取消编辑' : '重置表单'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card style={{ marginBottom: '24px',backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Table
|
||||
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
||||
columns={columns}
|
||||
@@ -508,32 +510,37 @@ export const AutoScoreManager: React.FC = () => {
|
||||
pageSize,
|
||||
total: rules.length,
|
||||
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
||||
onPageSizeChange: (size) => setPageSize(size),
|
||||
onPageSizeChange: (size) => setPageSize(size)
|
||||
}}
|
||||
style={{ color: 'var(--ss-text-main)' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }} title="当以下事件触发时" headerBordered>
|
||||
<Space style={{ display: "grid" }}>
|
||||
<Card
|
||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||
title="当以下事件触发时"
|
||||
headerBordered
|
||||
>
|
||||
<Space style={{ display: 'grid' }}>
|
||||
{triggerItems}
|
||||
<Button
|
||||
theme="default"
|
||||
variant="text"
|
||||
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
||||
icon={<AddIcon strokeWidth={3}/>}
|
||||
onClick={handleAddTrigger}>
|
||||
<Button
|
||||
theme="default"
|
||||
variant="text"
|
||||
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
||||
icon={<AddIcon strokeWidth={3} />}
|
||||
onClick={handleAddTrigger}
|
||||
>
|
||||
添加触发器
|
||||
</Button>
|
||||
</Space>
|
||||
</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>
|
||||
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
|
||||
</SyntaxHighlighter>
|
||||
</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>
|
||||
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
|
||||
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
|
||||
@@ -544,4 +551,4 @@ export const AutoScoreManager: React.FC = () => {
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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 = {
|
||||
...rule,
|
||||
id: newId,
|
||||
lastExecuted: undefined
|
||||
}
|
||||
|
||||
|
||||
this.rules.push(newRule)
|
||||
await this.saveRulesToSettings()
|
||||
|
||||
|
||||
if (rule.enabled) {
|
||||
this.startRuleTimer(newRule)
|
||||
}
|
||||
|
||||
|
||||
return newId
|
||||
}
|
||||
|
||||
async updateRule(rule: Partial<AutoScoreRule> & { id: number }): Promise<boolean> {
|
||||
const index = this.rules.findIndex(r => r.id === rule.id)
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
this.stopRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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<boolean> {
|
||||
async deleteFile(
|
||||
relativePath: string,
|
||||
folder: 'automatic' | 'script' = 'automatic'
|
||||
): Promise<boolean> {
|
||||
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<boolean> {
|
||||
async fileExists(
|
||||
relativePath: string,
|
||||
folder: 'automatic' | 'script' = 'automatic'
|
||||
): Promise<boolean> {
|
||||
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 }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+29
-8
@@ -175,9 +175,13 @@ export interface electronApi {
|
||||
}) => Promise<ipcResponse<void>>
|
||||
|
||||
registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>>
|
||||
|
||||
|
||||
// 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<ipcResponse<void>>
|
||||
@@ -192,13 +196,30 @@ export interface electronApi {
|
||||
// File System
|
||||
fsGetConfigStructure: () => Promise<ipcResponse<ConfigFolderStructure>>
|
||||
fsReadJson: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<any>>
|
||||
fsWriteJson: (relativePath: string, data: any, 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>>
|
||||
fsWriteJson: (
|
||||
relativePath: string,
|
||||
data: any,
|
||||
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[]>>
|
||||
fsFileExists: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<boolean>>
|
||||
|
||||
fsFileExists: (
|
||||
relativePath: string,
|
||||
folder?: 'automatic' | 'sscript'
|
||||
) => Promise<ipcResponse<boolean>>
|
||||
|
||||
// Generic invoke wrapper (minimal compatibility API)
|
||||
invoke?: (channel: string, ...args: any[]) => Promise<any>
|
||||
}
|
||||
|
||||
+43
-39
@@ -139,67 +139,71 @@ function MainContent(): React.JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
display: 'flex',
|
||||
bottom: '2px',
|
||||
left: '20px',
|
||||
opacity: 0.6,
|
||||
zIndex: 9999
|
||||
}}>
|
||||
<p style={{
|
||||
color: '#de2611',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '13px',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
开发中画面,不代表最终品质
|
||||
</p>
|
||||
<p style={{
|
||||
color: '#44474b',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '13px',
|
||||
paddingLeft: '5px',
|
||||
}}>
|
||||
SecScore Dev ({getPlatform()}-{getArchitecture()})
|
||||
</p>
|
||||
bottom: '2px',
|
||||
left: '20px',
|
||||
opacity: 0.6,
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
color: '#de2611',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '13px',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
开发中画面,不代表最终品质
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
color: '#44474b',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '13px',
|
||||
paddingLeft: '5px'
|
||||
}}
|
||||
>
|
||||
SecScore Dev ({getPlatform()}-{getArchitecture()})
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
|
||||
@@ -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<AutoScoreRule>[] = [
|
||||
{
|
||||
colKey: 'drag',
|
||||
title: '排序',
|
||||
cell: () => <MoveIcon />,
|
||||
width: 60
|
||||
{
|
||||
colKey: 'drag',
|
||||
title: '排序',
|
||||
cell: () => <MoveIcon />,
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
colKey: 'enabled',
|
||||
@@ -318,12 +323,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
}
|
||||
const studentList = row.studentNames.join(',\n')
|
||||
return (
|
||||
<TooltipLite
|
||||
content={studentList}
|
||||
showArrow
|
||||
placement="mouse"
|
||||
theme="default"
|
||||
>
|
||||
<TooltipLite content={studentList} showArrow placement="mouse" theme="default">
|
||||
{row.studentNames.length} 名学生
|
||||
</TooltipLite>
|
||||
)
|
||||
@@ -350,17 +350,10 @@ export const AutoScoreManager: React.FC = () => {
|
||||
width: 150,
|
||||
cell: ({ row }) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outline"
|
||||
onClick={() => handleEdit(row)}
|
||||
>
|
||||
<Button size="small" variant="outline" onClick={() => handleEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
content="确定要删除这条自动化吗?"
|
||||
onConfirm={() => handleDelete(row.id)}
|
||||
>
|
||||
<Popconfirm content="确定要删除这条自动化吗?" onConfirm={() => handleDelete(row.id)}>
|
||||
<Button size="small" variant="outline" theme="danger">
|
||||
删除
|
||||
</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 = {
|
||||
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<TriggerItem[]>([])
|
||||
|
||||
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}
|
||||
</div>
|
||||
))
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}>自动化加分管理</h2>
|
||||
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Form
|
||||
form={form}
|
||||
labelWidth={100}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
<Form form={form} labelWidth={100} onReset={handleResetForm}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
||||
<Form.FormItem
|
||||
label="自动化名称"
|
||||
@@ -470,13 +494,13 @@ export const AutoScoreManager: React.FC = () => {
|
||||
<Form.FormItem>
|
||||
<Space>
|
||||
<Form.FormItem
|
||||
label="间隔时间"
|
||||
name="intervalMinutes"
|
||||
rules={[
|
||||
{ required: true, message: '请输入间隔时间' },
|
||||
{ min: 1, message: '间隔时间至少为1分钟/天' }
|
||||
]}
|
||||
style={{ marginBottom: 0 }}
|
||||
label="间隔时间"
|
||||
name="intervalMinutes"
|
||||
rules={[
|
||||
{ required: true, message: '请输入间隔时间' },
|
||||
{ min: 1, message: '间隔时间至少为1分钟/天' }
|
||||
]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<InputNumber min={1} placeholder="例如:1(每隔1分钟/天执行一次)" />
|
||||
</Form.FormItem>
|
||||
@@ -497,43 +521,31 @@ export const AutoScoreManager: React.FC = () => {
|
||||
<InputNumber placeholder="例如:1(每次加1分)" />
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="适用学生"
|
||||
name="studentNames"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
multiple
|
||||
placeholder="请选择或搜索学生(留空表示所有学生)"
|
||||
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
||||
/>
|
||||
<Form.FormItem label="适用学生" name="studentNames">
|
||||
<Select
|
||||
filterable
|
||||
multiple
|
||||
placeholder="请选择或搜索学生(留空表示所有学生)"
|
||||
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
||||
/>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem
|
||||
label="加分理由"
|
||||
name="reason"
|
||||
>
|
||||
<Form.FormItem label="加分理由" name="reason">
|
||||
<Input placeholder="例如:每日签到奖励" />
|
||||
</Form.FormItem>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
||||
<Button
|
||||
theme="primary"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<Button theme="primary" onClick={handleSubmit}>
|
||||
{editingRuleId !== null ? '更新自动化' : '添加自动化'}
|
||||
</Button>
|
||||
<Button
|
||||
type="reset"
|
||||
variant="outline"
|
||||
>
|
||||
<Button type="reset" variant="outline">
|
||||
{editingRuleId !== null ? '取消编辑' : '重置表单'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card style={{ marginBottom: '24px',backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<Table
|
||||
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
||||
columns={columns}
|
||||
@@ -547,32 +559,37 @@ export const AutoScoreManager: React.FC = () => {
|
||||
pageSize,
|
||||
total: rules.length,
|
||||
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
||||
onPageSizeChange: (size) => setPageSize(size),
|
||||
onPageSizeChange: (size) => setPageSize(size)
|
||||
}}
|
||||
style={{ color: 'var(--ss-text-main)' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }} title="当以下事件触发时" headerBordered>
|
||||
<Space style={{ display: "grid" }}>
|
||||
<Card
|
||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||
title="当以下事件触发时"
|
||||
headerBordered
|
||||
>
|
||||
<Space style={{ display: 'grid' }}>
|
||||
{triggerItems}
|
||||
<Button
|
||||
theme="default"
|
||||
variant="text"
|
||||
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
||||
icon={<AddIcon strokeWidth={3}/>}
|
||||
onClick={handleAddTrigger}>
|
||||
<Button
|
||||
theme="default"
|
||||
variant="text"
|
||||
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
||||
icon={<AddIcon strokeWidth={3} />}
|
||||
onClick={handleAddTrigger}
|
||||
>
|
||||
添加触发器
|
||||
</Button>
|
||||
</Space>
|
||||
</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>
|
||||
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
|
||||
</SyntaxHighlighter>
|
||||
</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>
|
||||
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
|
||||
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
|
||||
@@ -583,4 +600,4 @@ export const AutoScoreManager: React.FC = () => {
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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提供学生分数查询服务,可在局域网内访问。
|
||||
</div>
|
||||
|
||||
|
||||
<Form labelWidth={120}>
|
||||
<Form.FormItem label="服务器状态">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Tag
|
||||
theme={httpServerStatus?.isRunning ? 'success' : 'default'}
|
||||
variant="light"
|
||||
>
|
||||
<Tag theme={httpServerStatus?.isRunning ? 'success' : 'default'} variant="light">
|
||||
{httpServerStatus?.isRunning ? '运行中' : '已停止'}
|
||||
</Tag>
|
||||
{httpServerStatus?.isRunning && (
|
||||
@@ -649,7 +647,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
)}
|
||||
</div>
|
||||
</Form.FormItem>
|
||||
|
||||
|
||||
<Form.FormItem label="端口设置">
|
||||
<Input
|
||||
value={httpPort}
|
||||
@@ -659,7 +657,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
disabled={!canAdmin || httpLoading}
|
||||
/>
|
||||
</Form.FormItem>
|
||||
|
||||
|
||||
<Form.FormItem label="主机地址">
|
||||
<Input
|
||||
value={httpHost}
|
||||
@@ -669,7 +667,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
disabled={!canAdmin || httpLoading}
|
||||
/>
|
||||
</Form.FormItem>
|
||||
|
||||
|
||||
<Form.FormItem label="CORS来源">
|
||||
<Input
|
||||
value={httpCorsOrigin}
|
||||
@@ -678,11 +676,13 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
style={{ width: '200px' }}
|
||||
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>
|
||||
</Form.FormItem>
|
||||
|
||||
|
||||
<Form.FormItem label="操作">
|
||||
<Space>
|
||||
<Button
|
||||
@@ -711,9 +711,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.FormItem>
|
||||
|
||||
|
||||
<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 /api/students/scores - 获取所有学生分数</div>
|
||||
<div>• GET /api/students/:id/score - 获取单个学生分数</div>
|
||||
@@ -724,7 +726,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
</Form>
|
||||
</Card>
|
||||
</Tabs.TabPanel>
|
||||
|
||||
|
||||
<Tabs.TabPanel value="url" label="URL 链接">
|
||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
|
||||
|
||||
@@ -68,7 +68,9 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||
<Menu value={activeMenu} onChange={onMenuChange} style={{ width: '100%', border: 'none' }}>
|
||||
<Menu.MenuItem value="home" icon={<HomeIcon />}> 主页
|
||||
<Menu.MenuItem value="home" icon={<HomeIcon />}>
|
||||
{' '}
|
||||
主页
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="students" icon={<UserIcon />} disabled={permission !== 'admin'}>
|
||||
学生管理
|
||||
@@ -76,13 +78,13 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
||||
<Menu.MenuItem value="score" icon={<HistoryIcon />}>
|
||||
积分管理
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="auto-score" icon={<ReplayIcon />}>
|
||||
<Menu.MenuItem value="auto-score" icon={<ReplayIcon />}>
|
||||
自动加分
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}>
|
||||
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}>
|
||||
排行榜
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="settlements" icon={<HistoryIcon />}>
|
||||
<Menu.MenuItem value="settlements" icon={<HistoryIcon />}>
|
||||
结算历史
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="reasons" icon={<RootListIcon />} disabled={permission !== 'admin'}>
|
||||
|
||||
@@ -382,7 +382,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
{tags.length === 0 ? (
|
||||
<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}
|
||||
</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)' }}
|
||||
/>
|
||||
|
||||
@@ -26,8 +26,8 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
||||
const [allTags, setAllTags] = useState<Tag[]>([])
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<number>>(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<TagEditorDialogProps> = ({
|
||||
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<TagEditorDialogProps> = ({
|
||||
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<TagEditorDialogProps> = ({
|
||||
}
|
||||
|
||||
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<TagEditorDialogProps> = ({
|
||||
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<TagEditorDialogProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
/* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => {
|
||||
/* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => {
|
||||
if (context.e.key === 'Enter') {
|
||||
context.e.preventDefault()
|
||||
handleAddTag()
|
||||
@@ -127,8 +127,8 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
||||
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 (
|
||||
<Dialog
|
||||
@@ -149,14 +149,10 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onEnter={handleAddTag}
|
||||
/* maxLength={50} */
|
||||
/* maxLength={50} */
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
theme="primary"
|
||||
onClick={handleAddTag}
|
||||
disabled={!inputValue.trim()}
|
||||
>
|
||||
<Button theme="primary" onClick={handleAddTag} disabled={!inputValue.trim()}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
@@ -166,7 +162,14 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
||||
<div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}>
|
||||
已选标签(点击取消)
|
||||
</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 ? (
|
||||
<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>
|
||||
<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 ? (
|
||||
<span style={{ color: 'var(--ss-text-secondary)' }}>无可用标签</span>
|
||||
) : (
|
||||
@@ -218,4 +228,4 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user