基本功能完成,下一步开始美化UI

This commit is contained in:
2025-12-19 16:02:38 +08:00
parent 465d4d7b4a
commit 6ac216d184
46 changed files with 2576 additions and 618 deletions

View File

@@ -8,6 +8,7 @@ export interface ExamTask {
startAt: string;
endAt: string;
createdAt: string;
selectionConfig?: string; // JSON string
}
export interface ExamTaskUser {
@@ -178,7 +179,7 @@ export class ExamTaskModel {
}
static async findById(id: string): Promise<ExamTask | null> {
const sql = `SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt FROM exam_tasks WHERE id = ?`;
const sql = `SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt, selection_config as selectionConfig FROM exam_tasks WHERE id = ?`;
const row = await get(sql, [id]);
return row || null;
}
@@ -189,6 +190,7 @@ export class ExamTaskModel {
startAt: string;
endAt: string;
userIds: string[];
selectionConfig?: string;
}): Promise<ExamTask> {
if (!data.name.trim()) throw new Error('任务名称不能为空');
if (!data.userIds.length) throw new Error('至少选择一位用户');
@@ -198,8 +200,8 @@ export class ExamTaskModel {
const id = uuidv4();
const sqlTask = `
INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at)
VALUES (?, ?, ?, ?, ?)
INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config)
VALUES (?, ?, ?, ?, ?, ?)
`;
const sqlTaskUser = `
@@ -207,7 +209,7 @@ export class ExamTaskModel {
VALUES (?, ?, ?)
`;
await run(sqlTask, [id, data.name.trim(), data.subjectId, data.startAt, data.endAt]);
await run(sqlTask, [id, data.name.trim(), data.subjectId, data.startAt, data.endAt, data.selectionConfig || null]);
for (const userId of data.userIds) {
await run(sqlTaskUser, [uuidv4(), id, userId]);
@@ -222,6 +224,7 @@ export class ExamTaskModel {
startAt: string;
endAt: string;
userIds: string[];
selectionConfig?: string;
}): Promise<ExamTask> {
const existing = await this.findById(id);
if (!existing) throw new Error('任务不存在');
@@ -232,11 +235,12 @@ export class ExamTaskModel {
const subject = await import('./examSubject').then(({ ExamSubjectModel }) => ExamSubjectModel.findById(data.subjectId));
if (!subject) throw new Error('科目不存在');
await run(`UPDATE exam_tasks SET name = ?, subject_id = ?, start_at = ?, end_at = ? WHERE id = ?`, [
await run(`UPDATE exam_tasks SET name = ?, subject_id = ?, start_at = ?, end_at = ?, selection_config = ? WHERE id = ?`, [
data.name.trim(),
data.subjectId,
data.startAt,
data.endAt,
data.selectionConfig || null,
id
]);

View File

@@ -5,4 +5,5 @@ export { QuizModel, type QuizRecord, type QuizAnswer, type SubmitQuizData } from
export { SystemConfigModel, type SystemConfig, type QuizConfig, type AdminUser } from './systemConfig';
export { QuestionCategoryModel, type QuestionCategory } from './questionCategory';
export { ExamSubjectModel, type ExamSubject } from './examSubject';
export { ExamTaskModel, type ExamTask, type TaskWithSubject, type TaskReport } from './examTask';
export { ExamTaskModel, type ExamTask, type TaskWithSubject, type TaskReport } from './examTask';
export { UserGroupModel, type UserGroup, type CreateUserGroupData } from './userGroup';

View File

@@ -86,8 +86,6 @@ export class QuestionCategoryModel {
}
static async update(id: string, name: string): Promise<QuestionCategory> {
if (id === 'default') throw new Error('默认类别不允许修改');
const existing = await this.findById(id);
if (!existing) throw new Error('类别不存在');

197
api/models/userGroup.ts Normal file
View File

@@ -0,0 +1,197 @@
import { v4 as uuidv4 } from 'uuid';
import { query, run, get } from '../database';
import { User } from './user';
export interface UserGroup {
id: string;
name: string;
description?: string;
isSystem: boolean;
createdAt: string;
memberCount?: number;
}
export interface CreateUserGroupData {
name: string;
description?: string;
}
export class UserGroupModel {
static async create(data: CreateUserGroupData): Promise<UserGroup> {
const id = uuidv4();
const sql = `
INSERT INTO user_groups (id, name, description, is_system)
VALUES (?, ?, ?, 0)
`;
try {
await run(sql, [id, data.name, data.description || '']);
return this.findById(id) as Promise<UserGroup>;
} catch (error: any) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new Error('用户组名称已存在');
}
throw error;
}
}
static async update(id: string, data: Partial<CreateUserGroupData>): Promise<UserGroup> {
const group = await this.findById(id);
if (!group) throw new Error('用户组不存在');
if (group.isSystem) throw new Error('系统内置用户组无法修改');
const fields: string[] = [];
const values: any[] = [];
if (data.name !== undefined) {
fields.push('name = ?');
values.push(data.name);
}
if (data.description !== undefined) {
fields.push('description = ?');
values.push(data.description);
}
if (fields.length === 0) {
return group;
}
values.push(id);
const sql = `UPDATE user_groups SET ${fields.join(', ')} WHERE id = ?`;
try {
await run(sql, values);
return this.findById(id) as Promise<UserGroup>;
} catch (error: any) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new Error('用户组名称已存在');
}
throw error;
}
}
static async delete(id: string): Promise<void> {
const group = await this.findById(id);
if (!group) throw new Error('用户组不存在');
if (group.isSystem) throw new Error('系统内置用户组无法删除');
await run(`DELETE FROM user_groups WHERE id = ?`, [id]);
}
static async findById(id: string): Promise<UserGroup | null> {
const sql = `
SELECT id, name, description, is_system as isSystem, created_at as createdAt
FROM user_groups WHERE id = ?
`;
const group = await get(sql, [id]);
return group || null;
}
static async findAll(): Promise<UserGroup[]> {
const sql = `
SELECT
g.id, g.name, g.description, g.is_system as isSystem, g.created_at as createdAt,
(SELECT COUNT(*) FROM user_group_members m WHERE m.group_id = g.id) as memberCount
FROM user_groups g
ORDER BY g.is_system DESC, g.created_at DESC
`;
return await query(sql);
}
static async addMember(groupId: string, userId: string): Promise<void> {
const sql = `INSERT OR IGNORE INTO user_group_members (group_id, user_id) VALUES (?, ?)`;
await run(sql, [groupId, userId]);
}
static async removeMember(groupId: string, userId: string): Promise<void> {
const group = await this.findById(groupId);
if (group?.isSystem) {
// Check if user is being deleted? No, this method is for removing member.
// Requirement: "User cannot actively exit this group".
// Implementation: Cannot remove member from system group via this API.
// Only user deletion removes them (cascade).
throw new Error('无法从系统内置组中移除成员');
}
const sql = `DELETE FROM user_group_members WHERE group_id = ? AND user_id = ?`;
await run(sql, [groupId, userId]);
}
static async getMembers(groupId: string): Promise<User[]> {
const sql = `
SELECT u.id, u.name, u.phone, u.created_at as createdAt
FROM users u
JOIN user_group_members m ON u.id = m.user_id
WHERE m.group_id = ?
ORDER BY m.created_at DESC
`;
return await query(sql, [groupId]);
}
static async getUserGroups(userId: string): Promise<UserGroup[]> {
const sql = `
SELECT g.id, g.name, g.description, g.is_system as isSystem, g.created_at as createdAt
FROM user_groups g
JOIN user_group_members m ON g.id = m.group_id
WHERE m.user_id = ?
ORDER BY g.is_system DESC, g.created_at DESC
`;
return await query(sql, [userId]);
}
static async getSystemGroup(): Promise<UserGroup | null> {
const sql = `
SELECT id, name, description, is_system as isSystem, created_at as createdAt
FROM user_groups WHERE is_system = 1
`;
return await get(sql);
}
static async updateMembers(groupId: string, userIds: string[]): Promise<void> {
const group = await this.findById(groupId);
if (!group) throw new Error('用户组不存在');
if (group.isSystem) throw new Error('无法修改系统内置组成员');
// Transaction-like behavior needed but SQLite wrapper doesn't expose it easily.
// We'll do delete then insert.
await run(`DELETE FROM user_group_members WHERE group_id = ?`, [groupId]);
if (userIds.length > 0) {
// Batch insert
// SQLite limit is usually high enough, but safer to loop or construct big query
// For simplicity in this helper:
for (const userId of userIds) {
await run(`INSERT INTO user_group_members (group_id, user_id) VALUES (?, ?)`, [groupId, userId]);
}
}
}
static async updateUserGroups(userId: string, groupIds: string[]): Promise<void> {
// 1. Get current system group(s) the user belongs to
const currentGroups = await this.getUserGroups(userId);
const systemGroupIds = currentGroups.filter(g => g.isSystem).map(g => g.id);
// 2. Ensure system groups are in the new list (force keep them)
const newGroupSet = new Set(groupIds);
for (const sysId of systemGroupIds) {
newGroupSet.add(sysId);
}
// Also ensure the default "All Users" group is there if not already
// (In case the user was created before groups existed and somehow not migrated, though migration handles it)
// Safe to just ensure "All Users" is present.
const allUsersGroup = await this.getSystemGroup();
if (allUsersGroup) {
newGroupSet.add(allUsersGroup.id);
}
const finalGroupIds = Array.from(newGroupSet);
// 3. Update
await run(`DELETE FROM user_group_members WHERE user_id = ?`, [userId]);
for (const gid of finalGroupIds) {
await run(`INSERT INTO user_group_members (group_id, user_id) VALUES (?, ?)`, [gid, userId]);
}
}
}