feat(imports): 成绩/名单表格导入 + ClassIsland 时间配置导入 + 安装版增量迁移
服务端:
- 新增 imports 模块: 模板下载(csv/xlsx)、表格解析预览、字段映射导入(append/upsert)、
错误行号与结果统计; ClassIsland 时间配置容错解析 + replace/merge 导入 + 失败回滚(400 不写库)
- TimeLayout 模型 + 迁移; 修复 contentBase64 空值被 422 拦截, 改由 service 返回 IMPORT_EMPTY_FILE
- merge 语义改为按开始时间槽位覆盖, 同开始时间的旧节次不再被静默丢弃
- db-bootstrap 增加 _ch_migrations 账本: 覆盖安装升级也能自动补跑新增迁移(旧库实测通过)
Web 管理端:
- TableImportDialog(成绩/名单): 模板下载、预览、字段映射、重复处理模式、结果统计与错误行
- TimeLayoutImportDialog: 粘贴/选 .json、解析预览、覆盖/合并、已导入列表与删除
- 成绩录入/学生管理/课表管理接入入口(按权限矩阵显示)
验证: verify:e2e 107/107(新增导入 22 项) verify:web 18/18(新增时间配置导入弹窗真实点击)
verify:desktop 40/40
文档: README 新增导入章节、增量迁移说明、API 表与验收对照更新
This commit is contained in:
1 parent
0dcab6f369
commit
8b85079d81
23 files changed
+2735
-108
No files matched your search
@@ -21,6 +21,8 @@ export const API_PATHS = {
|
||||
grades: '/grades',
|
||||
students: '/students',
|
||||
teachers: '/teachers',
|
||||
/** 导入:模板下载 / 表格导入 / ClassIsland 课表时间配置 */
|
||||
imports: '/imports',
|
||||
dashboard: '/dashboard/summary',
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -458,6 +458,77 @@ export interface ServerToClientEvents {
|
||||
connected: (payload: { userId: string; role: UserRole; rooms: string[] }) => void;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ 导入(模板 / 表格 / 课表时间配置) */
|
||||
|
||||
/** 表格导入预览:列名、前若干行、建议字段映射与校验问题 */
|
||||
export interface TableImportPreview {
|
||||
kind: 'grades' | 'students';
|
||||
columns: string[];
|
||||
rows: string[][];
|
||||
totalRows: number;
|
||||
suggestedMapping: Record<string, string>;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
templateCsv: string;
|
||||
}
|
||||
|
||||
/** 导入时某一行的具体错误(row 为 Excel 视角的行号,含表头) */
|
||||
export interface TableImportRowError {
|
||||
row: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** 导入结果统计 */
|
||||
export interface TableImportResult {
|
||||
kind: 'grades' | 'students';
|
||||
total: number;
|
||||
inserted: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
errors: TableImportRowError[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/** ClassIsland 时间配置条目 */
|
||||
export interface TimeLayoutEntry {
|
||||
index: number;
|
||||
name: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
type: 'class' | 'break' | 'divider';
|
||||
skipped: boolean;
|
||||
}
|
||||
|
||||
export interface TimeLayoutDto {
|
||||
id: string;
|
||||
classId: string;
|
||||
name: string;
|
||||
source: string;
|
||||
items: TimeLayoutEntry[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** ClassIsland JSON 解析结果(预览用:只解析不落库) */
|
||||
export interface TimeLayoutParsePreview {
|
||||
items: TimeLayoutEntry[];
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
shape: string;
|
||||
}
|
||||
|
||||
/** 课表时间配置导入结果 */
|
||||
export interface TimeLayoutImportResult {
|
||||
layout: TimeLayoutDto;
|
||||
mode: 'replace' | 'merge';
|
||||
warnings: string[];
|
||||
/** merge 时:被此次导入覆盖的旧节次数 */
|
||||
merged: number;
|
||||
/** merge 时:新增节次数;replace 时为总节次数 */
|
||||
replaced: number;
|
||||
}
|
||||
|
||||
/** 客户端 -> 服务端事件名 */
|
||||
export interface ClientToServerEvents {
|
||||
'class:join': (classId: string, ack?: (result: { ok: boolean; message?: string }) => void) => void;
|
||||
|
||||
Reference in new issue
Block a user