mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +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),
|
('迟到','纪律',-1,1,CURRENT_TIMESTAMP),
|
||||||
('未交作业','作业情况',-2,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> {
|
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 "students"`)
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "reasons"`)
|
await queryRunner.query(`DROP TABLE IF EXISTS "reasons"`)
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "settings"`)
|
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 { 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
|
const triggerItems = triggerList
|
||||||
.filter((t) => t.eventName !== null)
|
.filter((t) => t.eventName !== null)
|
||||||
.map((item) => (
|
.map((item, idx) => (
|
||||||
<TriggerItemComponent
|
<TriggerItemComponent
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
onDelete={handleDeleteTrigger}
|
onDelete={handleDeleteTrigger}
|
||||||
onChange={handleTriggerChange}
|
onChange={handleTriggerChange}
|
||||||
onValueChange={handleTriggerValueChange}
|
onValueChange={handleTriggerValueChange}
|
||||||
|
isFirst={idx === 0}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -498,25 +499,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</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
|
<Card
|
||||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||||
@@ -555,6 +537,27 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</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)' }}>
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
<Code
|
<Code
|
||||||
code={(() => {
|
code={(() => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const ActionItem: React.FC<ActionItemProps> = ({
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={item.eventName}
|
value={item.eventName}
|
||||||
style={{ width: '200px' }}
|
style={{ width: '200px', marginRight: 12 }}
|
||||||
options={allActions.options}
|
options={allActions.options}
|
||||||
placeholder="请选择触发行动"
|
placeholder="请选择触发行动"
|
||||||
onChange={(value) => onChange(item.id, value as string)}
|
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 { Button, Select } from 'tdesign-react'
|
||||||
import { Delete1Icon } from 'tdesign-icons-react'
|
import { Delete1Icon } from 'tdesign-icons-react'
|
||||||
import { triggerRegistry, allTriggers } from './registry'
|
import { triggerRegistry, allTriggers } from './registry'
|
||||||
@@ -9,14 +9,31 @@ interface TriggerItemProps {
|
|||||||
onDelete: (id: number) => void
|
onDelete: (id: number) => void
|
||||||
onChange: (id: number, eventName: string) => void
|
onChange: (id: number, eventName: string) => void
|
||||||
onValueChange: (id: number, value: 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 definition = triggerRegistry.get(item.eventName)
|
||||||
const Component = definition?.component
|
const Component = definition?.component
|
||||||
|
const [andFilled, setAndFilled] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 5 }}>
|
<div style={{ display: 'flex', gap: 5 }}>
|
||||||
|
{!isFirst && (
|
||||||
|
<Button
|
||||||
|
theme="primary"
|
||||||
|
variant={andFilled ? 'base' : 'outline'}
|
||||||
|
onClick={() => setAndFilled((v) => !v)}
|
||||||
|
>
|
||||||
|
并
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
theme="default"
|
theme="default"
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -25,7 +42,7 @@ const TriggerItem: React.FC<TriggerItemProps> = ({ item, onDelete, onChange, onV
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={item.eventName}
|
value={item.eventName}
|
||||||
style={{ width: '200px' }}
|
style={{ width: '200px', marginRight: 12 }}
|
||||||
options={allTriggers.options}
|
options={allTriggers.options}
|
||||||
placeholder="请选择触发规则"
|
placeholder="请选择触发规则"
|
||||||
onChange={(value) => onChange(item.id, value as string)}
|
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'
|
import type { TriggerComponentProps } from '../types'
|
||||||
|
|
||||||
export const eventName = 'interval_time_passed'
|
export const eventName = 'interval_time_passed'
|
||||||
@@ -19,21 +20,59 @@ export const triggerLogic = {
|
|||||||
|
|
||||||
const IntervalTimeTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
|
const IntervalTimeTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
|
||||||
const numValue = value ? parseInt(value, 10) : undefined
|
const numValue = value ? parseInt(value, 10) : undefined
|
||||||
|
const [unit, setUnit] = useState<'minutes' | 'days'>('minutes')
|
||||||
|
|
||||||
const handleChange = (v: any) => {
|
const handleChange = (v: any) => {
|
||||||
const numV = typeof v === 'number' ? v : v ? Number(v) : undefined
|
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 (
|
return (
|
||||||
<InputNumber
|
<Space>
|
||||||
placeholder="请输入时间间隔(分钟)"
|
<Form.FormItem
|
||||||
style={{ width: '150px' }}
|
name="intervalMinutes"
|
||||||
value={numValue}
|
rules={[
|
||||||
onChange={handleChange}
|
{ required: true, message: '请输入时间' },
|
||||||
min={1}
|
{ min: 1, message: unit === 'minutes' ? '间隔时间至少为1分钟' : '间隔时间至少为1天' }
|
||||||
theme="column"
|
]}
|
||||||
/>
|
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 { InputNumber, Row, Col } from 'tdesign-react'
|
||||||
import type { TriggerComponentProps } from '../types'
|
import type { TriggerComponentProps } from '../types'
|
||||||
|
|
||||||
export const eventName = 'random_time'
|
export const eventName = 'random_time_reached'
|
||||||
export const label = '随机时间触发'
|
export const label = '随机时间触发'
|
||||||
export const description = '在指定时间范围内随机触发自动化'
|
export const description = '在指定时间范围内随机触发自动化'
|
||||||
export const triggerLogic = {
|
export const triggerLogic = {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Select } from 'tdesign-react'
|
import { Select } from 'tdesign-react'
|
||||||
import type { TriggerComponentProps } from '../types'
|
import type { TriggerComponentProps } from '../types'
|
||||||
|
export const eventName = 'student_tag_matched'
|
||||||
export const eventName = 'student_tag_added'
|
export const label = '当学生匹配标签时触发'
|
||||||
export const label = '当学生添加标签时触发'
|
export const description = '当学生匹配特定标签时触发自动化'
|
||||||
export const description = '当学生添加特定标签时触发自动化'
|
|
||||||
export const triggerLogic = {
|
export const triggerLogic = {
|
||||||
eventName,
|
eventName,
|
||||||
label,
|
label,
|
||||||
@@ -23,7 +22,7 @@ const StudentTagTrigger: React.FC<TriggerComponentProps> = ({ value, onChange })
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTags = async () => {
|
const fetchTags = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await (window as any).api.invoke('tag:getAll', {})
|
const res = await (window as any).api.tagsGetAll()
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
setTags(res.data.map((tag: any) => ({ label: tag.name, value: tag.name })))
|
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 {
|
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