feat:自动加分更新逻辑

This commit is contained in:
NanGua_QWQ
2026-02-21 18:18:38 +08:00
parent 094bd639b1
commit dc4873ebcb
10 changed files with 153 additions and 96 deletions
@@ -1,53 +0,0 @@
import type { MigrationInterface, QueryRunner } from 'typeorm'
export class AddTagsSchema2026020600000 implements MigrationInterface {
name = 'AddTagsSchema2026020600000'
async up(queryRunner: QueryRunner): Promise<void> {
if (!(await queryRunner.hasTable('tags'))) {
await queryRunner.query(`
CREATE TABLE "tags" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" text NOT NULL UNIQUE,
"created_at" text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"updated_at" text NOT NULL DEFAULT (CURRENT_TIMESTAMP)
)
`)
}
if (!(await queryRunner.hasTable('student_tags'))) {
await queryRunner.query(`
CREATE TABLE "student_tags" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"student_id" integer NOT NULL,
"tag_id" integer NOT NULL,
"created_at" text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
FOREIGN KEY ("student_id") REFERENCES "students"("id") ON DELETE CASCADE,
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON DELETE CASCADE,
UNIQUE("student_id", "tag_id")
)
`)
}
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_student_id" ON "student_tags" ("student_id")`
)
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_tag_id" ON "student_tags" ("tag_id")`
)
await queryRunner.query(`
INSERT OR IGNORE INTO "tags" ("name") VALUES
('优秀生'),
('班干部'),
('勤奋'),
('活跃'),
('进步快')
`)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "student_tags"`)
await queryRunner.query(`DROP TABLE IF EXISTS "tags"`)
}
}
@@ -81,6 +81,47 @@ export class InitSchema2026011800000 implements MigrationInterface {
('迟到','纪律',-1,1,CURRENT_TIMESTAMP),
('未交作业','作业情况',-2,1,CURRENT_TIMESTAMP)
`)
if (!(await queryRunner.hasTable('tags'))) {
await queryRunner.query(`
CREATE TABLE "tags" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" text NOT NULL UNIQUE,
"created_at" text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"updated_at" text NOT NULL DEFAULT (CURRENT_TIMESTAMP)
)
`)
}
if (!(await queryRunner.hasTable('student_tags'))) {
await queryRunner.query(`
CREATE TABLE "student_tags" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"student_id" integer NOT NULL,
"tag_id" integer NOT NULL,
"created_at" text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
FOREIGN KEY ("student_id") REFERENCES "students"("id") ON DELETE CASCADE,
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON DELETE CASCADE,
UNIQUE("student_id", "tag_id")
)
`)
}
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_student_id" ON "student_tags" ("student_id")`
)
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_tag_id" ON "student_tags" ("tag_id")`
)
await queryRunner.query(`
INSERT OR IGNORE INTO "tags" ("name") VALUES
('优秀生'),
('班干部'),
('勤奋'),
('活跃'),
('进步快')
`)
}
async down(queryRunner: QueryRunner): Promise<void> {
@@ -89,5 +130,7 @@ export class InitSchema2026011800000 implements MigrationInterface {
await queryRunner.query(`DROP TABLE IF EXISTS "students"`)
await queryRunner.query(`DROP TABLE IF EXISTS "reasons"`)
await queryRunner.query(`DROP TABLE IF EXISTS "settings"`)
await queryRunner.query(`DROP TABLE IF EXISTS "student_tags"`)
await queryRunner.query(`DROP TABLE IF EXISTS "tags"`)
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
import { InitSchema2026011800000 } from './InitSchema2026011800000'
import { AddTagsSchema2026020600000 } from './AddTagsSchema2026020600000'
export const migrations = [InitSchema2026011800000, AddTagsSchema2026020600000]
export const migrations = [InitSchema2026011800000]
@@ -440,13 +440,14 @@ export const AutoScoreManager: React.FC = () => {
const triggerItems = triggerList
.filter((t) => t.eventName !== null)
.map((item) => (
.map((item, idx) => (
<TriggerItemComponent
key={item.id}
item={item}
onDelete={handleDeleteTrigger}
onChange={handleTriggerChange}
onValueChange={handleTriggerValueChange}
isFirst={idx === 0}
/>
))
@@ -498,25 +499,6 @@ export const AutoScoreManager: React.FC = () => {
</div>
</Form>
</Card>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Table
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
columns={columns}
rowKey="id"
resizable
loading={loading}
dragSort="row-handler"
onDragSort={onDragSort}
pagination={{
current: currentPage,
pageSize,
total: rules.length,
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
onPageSizeChange: (size) => setPageSize(size)
}}
style={{ color: 'var(--ss-text-main)' }}
/>
</Card>
<Card
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
@@ -555,6 +537,27 @@ export const AutoScoreManager: React.FC = () => {
</Button>
</Space>
</Card>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Table
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
columns={columns}
rowKey="id"
resizable
loading={loading}
dragSort="row-handler"
onDragSort={onDragSort}
pagination={{
current: currentPage,
pageSize,
total: rules.length,
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
onPageSizeChange: (size) => setPageSize(size)
}}
style={{ color: 'var(--ss-text-main)' }}
/>
</Card>
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
<Code
code={(() => {
@@ -31,7 +31,7 @@ const ActionItem: React.FC<ActionItemProps> = ({
/>
<Select
value={item.eventName}
style={{ width: '200px' }}
style={{ width: '200px', marginRight: 12 }}
options={allActions.options}
placeholder="请选择触发行动"
onChange={(value) => onChange(item.id, value as string)}
@@ -1,4 +1,4 @@
import React from 'react'
import { useState } from 'react'
import { Button, Select } from 'tdesign-react'
import { Delete1Icon } from 'tdesign-icons-react'
import { triggerRegistry, allTriggers } from './registry'
@@ -9,14 +9,31 @@ interface TriggerItemProps {
onDelete: (id: number) => void
onChange: (id: number, eventName: string) => void
onValueChange: (id: number, value: string) => void
isFirst?: boolean
}
const TriggerItem: React.FC<TriggerItemProps> = ({ item, onDelete, onChange, onValueChange }) => {
const TriggerItem: React.FC<TriggerItemProps> = ({
item,
onDelete,
onChange,
onValueChange,
isFirst = false
}) => {
const definition = triggerRegistry.get(item.eventName)
const Component = definition?.component
const [andFilled, setAndFilled] = useState(false)
return (
<div style={{ display: 'flex', gap: 5 }}>
{!isFirst && (
<Button
theme="primary"
variant={andFilled ? 'base' : 'outline'}
onClick={() => setAndFilled((v) => !v)}
>
</Button>
)}
<Button
theme="default"
variant="text"
@@ -25,7 +42,7 @@ const TriggerItem: React.FC<TriggerItemProps> = ({ item, onDelete, onChange, onV
/>
<Select
value={item.eventName}
style={{ width: '200px' }}
style={{ width: '200px', marginRight: 12 }}
options={allTriggers.options}
placeholder="请选择触发规则"
onChange={(value) => onChange(item.id, value as string)}
@@ -1,4 +1,5 @@
import { InputNumber } from 'tdesign-react'
import { useState } from 'react'
import { InputNumber, Space, Radio, Form } from 'tdesign-react'
import type { TriggerComponentProps } from '../types'
export const eventName = 'interval_time_passed'
@@ -19,21 +20,59 @@ export const triggerLogic = {
const IntervalTimeTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
const numValue = value ? parseInt(value, 10) : undefined
const [unit, setUnit] = useState<'minutes' | 'days'>('minutes')
const handleChange = (v: any) => {
const numV = typeof v === 'number' ? v : v ? Number(v) : undefined
onChange(numV !== undefined && numV !== null && !isNaN(numV) ? String(numV) : '')
if (numV === undefined || numV === null || isNaN(numV)) {
onChange('')
return
}
const minutes = unit === 'minutes' ? numV : numV * 1440
onChange(String(Math.round(minutes)))
}
const displayValue =
numValue === undefined || isNaN(numValue)
? undefined
: unit === 'minutes'
? numValue
: Math.max(1, Math.round(numValue / 1440))
return (
<InputNumber
placeholder="请输入时间间隔(分钟)"
style={{ width: '150px' }}
value={numValue}
onChange={handleChange}
min={1}
theme="column"
/>
<Space>
<Form.FormItem
name="intervalMinutes"
rules={[
{ required: true, message: '请输入时间' },
{ min: 1, message: unit === 'minutes' ? '间隔时间至少为1分钟' : '间隔时间至少为1天' }
]}
style={{ marginBottom: 0 }}
>
<InputNumber
placeholder={unit === 'minutes' ? '请输入时间间隔(分钟)' : '请输入时间间隔(天)'}
style={{ width: '100px' }}
value={displayValue}
onChange={handleChange}
min={1}
theme="column"
/>
</Form.FormItem>
<Form.FormItem
name="timeUnit"
initialData="minutes"
style={{ marginBottom: 0, marginLeft: -12 }}
>
<Radio.Group
variant="default-filled"
value={unit}
onChange={(v) => setUnit(String(v) as 'minutes' | 'days')}
>
<Radio.Button value="days"></Radio.Button>
<Radio.Button value="minutes"></Radio.Button>
</Radio.Group>
</Form.FormItem>
</Space>
)
}
@@ -1,7 +1,7 @@
import { InputNumber, Row, Col } from 'tdesign-react'
import type { TriggerComponentProps } from '../types'
export const eventName = 'random_time'
export const eventName = 'random_time_reached'
export const label = '随机时间触发'
export const description = '在指定时间范围内随机触发自动化'
export const triggerLogic = {
@@ -1,10 +1,9 @@
import { useState, useEffect } from 'react'
import { Select } from 'tdesign-react'
import type { TriggerComponentProps } from '../types'
export const eventName = 'student_tag_added'
export const label = '当学生添加标签时触发'
export const description = '当学生添加特定标签时触发自动化'
export const eventName = 'student_tag_matched'
export const label = '当学生匹配标签时触发'
export const description = '当学生匹配特定标签时触发自动化'
export const triggerLogic = {
eventName,
label,
@@ -23,7 +22,7 @@ const StudentTagTrigger: React.FC<TriggerComponentProps> = ({ value, onChange })
useEffect(() => {
const fetchTags = async () => {
try {
const res = await (window as any).api.invoke('tag:getAll', {})
const res = await (window as any).api.tagsGetAll()
if (res.success && res.data) {
setTags(res.data.map((tag: any) => ({ label: tag.name, value: tag.name })))
}
+11 -1
View File
@@ -13,5 +13,15 @@ export const allTriggerLogics: TriggerLogic[] = [
]
export function getTriggerLogic(eventName: string): TriggerLogic | undefined {
return allTriggerLogics.find((t) => t.eventName === eventName)
const direct = allTriggerLogics.find((t) => t.eventName === eventName)
if (direct) return direct
const aliasMap: Record<string, string> = {
random_time: 'random_time_reached',
student_tag_added: 'student_tag_matched'
}
const mapped = aliasMap[eventName]
if (mapped) {
return allTriggerLogics.find((t) => t.eventName === mapped)
}
return undefined
}