mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat:自动加分更新逻辑
This commit is contained in:
@@ -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,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 })))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user