一、灵动岛
- ①「紧急通知后来了新作业,收起再点开不了新作业」根因有二,都已修:
(a) active 仍是紧急那条,点击只会反复打开它 —— 现在点击胶囊优先打开**最新**那条未处理消息(队列里更晚的);
(b) 上课时段 syncWindow 会把普通通知立刻隐藏(表现为点了没反应)—— 新增 explicitShowId,
"用户主动点开"的那条守卫放行(岛完全隐藏时仍严格不打扰)。
冒烟新增「紧急通知后来了新作业:收起再点击打开的是新作业(联合通知回归)」。
- ② 设置实时预览/保存:补「设置页真实链路」断言(window.desktop.islandSetAppearance → IPC → 主进程 →
渲染进程 CSS 变量 + 窗口尺寸),并断言"岛显示中改尺寸立即生效"(实测均通过)。
- ③ 未读红点移到「通知」图标右上角(此前在文字后面),并加 DOM 几何断言(dx>0 且 dy<0,
没有未读时自动造一条未读通知再断言、断言后删除)。
- 冒烟稳定性:新增 island.setBlurCollapseEnabled()(默认开),测试期间关掉"失焦收起",
避免系统抢焦点导致展开态被收回的随机失败;专门验证失焦的用例仍临时打开。
二、作业看板(学生端)
- ⑥ 只看当天:新增「只看今天」开关(默认开),按 createdAt 的本地日期过滤,昨天的过期作业不再混入。
- ⑧ 完整显示:卡片同时渲染标题 + 作业要求全文(列表模式也加「作业要求」列)。
- ⑨ 铺满不滚动:看板外层高度固定 + overflow hidden,按内容/可用高度算缩放系数用 transform: scale 适配
(工具栏显示当前缩放比例),页面本身不出现滚动条。
- ⑦ 全屏看板标题栏用 #header 插槽:标题左对齐、当天日期时间绝对居中(受「显示时间」开关控制,每分钟刷新)。
三、课表与周数
- ⑤ 学期周数可调:新增 Class.termWeeks(默认 20,迁移 + 维护脚本),班主任在班级管理里改;
GET /api/dashboard/term?classId= 返回该班 maxWeek,课表周次下拉与新建课表的默认 weekEnd 都按它来
(不再固定 30 周)。
- ⑩ 课表管理只保留「导入 ClassIsland 课程表」入口(时间表由课程表 JSON 的 TimeLayouts 一并带入),
「导入时间配置」按钮下线;ui-smoke 第 7.7 段改为断言新弹窗(解析 4 行 + 单双周标签 + 旧入口已下线)。
四、以班级为单位(⑬)与 Web 端
- 个人学生账号登录停用:POST /api/auth/login 对 role=STUDENT 返回 403(改用班级码 + 班级密码);
学生行仍作为个人数据记录键由班级会话代全班读写;e2e 全面切换到班级会话并新增该断言。
- ④ 作业科目过少:新增共享 SUBJECT_CATALOG(16 个常见科目)+ 种子课程扩到全科目;
Web 作业发布弹窗可选「其他科目(新建)」,提交前自动建课(同名复用)。
- ⑪ 通知支持多班批量发布(多选 + 逐班发布 + 汇总结果 + 二次确认覆盖所有选中班);
⑫ 单班只显示已读情况、跨班才显示已读率;⑭ 手机端课表可横向滚动看全 7 天;
⑮ 手机端已连接不再显示孤立绿点(仅未连接时显示文字标签);⑯ 手机端「叫人/发布通知」按钮同宽左对齐;
⑬ Web 学生管理去掉「用户名」列(新建自动生成学号式用户名,导入仍支持显式指定)。
验证:verify:e2e 149/149、verify:web 20/20、verify:desktop 75/75(复跑 68/68,打包便携版 75/75 packaged=true)、
typecheck / lint / format 全绿;服务端与 Web 已重新打包并部署到本机安装目录。
265 lines
8.4 KiB
Plaintext
265 lines
8.4 KiB
Plaintext
// 班级小助手 - 数据模型
|
|
//
|
|
// 设计说明:
|
|
// 1) 本 schema 使用 sqlite provider(本地 MVP / 离线开发)。
|
|
// 字段全部使用 String / Int / Float / Boolean / DateTime 等跨库通用类型,
|
|
// 不使用 Prisma enum 与 @db.* 原生类型,因此可平移到 MySQL。
|
|
// MySQL 版本见同目录 schema.mysql.prisma,切换步骤见 docs/mysql.md。
|
|
// 2) 角色与优先级用 String 存储('ADMIN' | 'TEACHER' | 'STUDENT'、
|
|
// 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT'),在 API 层用 zod 校验,
|
|
// 这样 SQLite / MySQL 行为完全一致。
|
|
// 3) 所有主键使用 cuid 字符串,便于多端离线生成与合并。
|
|
|
|
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "sqlite"
|
|
}
|
|
|
|
/// 用户:教师 / 管理员 / 学生统一存储
|
|
model User {
|
|
id String @id @default(cuid())
|
|
username String @unique
|
|
passwordHash String
|
|
name String
|
|
role String @default("STUDENT")
|
|
classId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// 学生的主班级(一个学生属于一个主班级)
|
|
class Class? @relation("ClassStudents", fields: [classId], references: [id], onDelete: SetNull)
|
|
// 教师创建(负责)的班级
|
|
ownedClasses Class[] @relation("ClassOwner")
|
|
// 教师被分配协作的班级
|
|
classAssignments ClassTeacher[]
|
|
courses Course[]
|
|
homeworkStatuses HomeworkStatus[]
|
|
notificationReads NotificationRead[]
|
|
grades Grade[]
|
|
enrollments Enrollment[]
|
|
createdHomeworks Homework[] @relation("HomeworkCreator")
|
|
createdNotices Notification[] @relation("NotificationCreator")
|
|
|
|
@@index([classId])
|
|
@@index([role])
|
|
}
|
|
|
|
/// 班级
|
|
model Class {
|
|
id String @id @default(cuid())
|
|
name String
|
|
grade String
|
|
teacherId String
|
|
/// 班级码:学生端「班级账号」登录用(唯一,字母数字,4~16 位,存大写)
|
|
code String @unique
|
|
/// 班级密码(bcrypt 哈希);null = 尚未设置,此时禁止班级账号登录
|
|
passwordHash String?
|
|
/// 本学期教学周数(班主任可调,默认 20):课表周次选择与 weekEnd 默认值都用它
|
|
termWeeks Int @default(20)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
teacher User @relation("ClassOwner", fields: [teacherId], references: [id], onDelete: Cascade)
|
|
students User[] @relation("ClassStudents")
|
|
teachers ClassTeacher[]
|
|
courses Course[]
|
|
schedules Schedule[]
|
|
homeworks Homework[]
|
|
notifications Notification[]
|
|
grades Grade[]
|
|
enrollments Enrollment[]
|
|
timeLayouts TimeLayout[]
|
|
|
|
@@index([teacherId])
|
|
}
|
|
|
|
/// 教师-班级 分配关系(一个教师可管理多个班级)
|
|
model ClassTeacher {
|
|
id String @id @default(cuid())
|
|
classId String
|
|
teacherId String
|
|
createdAt DateTime @default(now())
|
|
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
teacher User @relation(fields: [teacherId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([classId, teacherId])
|
|
@@index([teacherId])
|
|
}
|
|
|
|
/// 学生-班级 选课/归属记录(与 User.classId 保持同步,支持未来多班)
|
|
model Enrollment {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
classId String
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, classId])
|
|
@@index([classId])
|
|
}
|
|
|
|
/// 课程
|
|
model Course {
|
|
id String @id @default(cuid())
|
|
name String
|
|
teacherId String
|
|
classId String
|
|
createdAt DateTime @default(now())
|
|
|
|
teacher User @relation(fields: [teacherId], references: [id], onDelete: Cascade)
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
schedules Schedule[]
|
|
homeworks Homework[]
|
|
grades Grade[]
|
|
|
|
@@unique([classId, name])
|
|
@@index([classId])
|
|
}
|
|
|
|
/// 课表条目
|
|
model Schedule {
|
|
id String @id @default(cuid())
|
|
classId String
|
|
courseId String
|
|
/// 1=周一 ... 7=周日
|
|
dayOfWeek Int
|
|
/// HH:mm
|
|
startTime String
|
|
/// HH:mm
|
|
endTime String
|
|
location String?
|
|
/// 学期起始周次(含)
|
|
weekStart Int @default(1)
|
|
/// 学期结束周次(含)
|
|
weekEnd Int @default(20)
|
|
/// 单双周:ALL=每周(默认)/ ODD=单周 / EVEN=双周(ClassIsland WeekCountDiv + WeekCountDivTotal 的映射)
|
|
weekParity String @default("ALL")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([classId, dayOfWeek])
|
|
@@index([courseId])
|
|
}
|
|
|
|
/// 作业
|
|
model Homework {
|
|
id String @id @default(cuid())
|
|
classId String
|
|
courseId String?
|
|
title String
|
|
content String
|
|
attachmentUrl String?
|
|
/// 遗留字段:作业"截止时间"功能已下线(UI/接口均不再读写)。
|
|
/// 保留列以免对已安装的库做破坏性迁移;新库该列恒为 null。
|
|
dueAt DateTime?
|
|
createdBy String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
course Course? @relation(fields: [courseId], references: [id], onDelete: SetNull)
|
|
creator User @relation("HomeworkCreator", fields: [createdBy], references: [id], onDelete: Cascade)
|
|
statuses HomeworkStatus[]
|
|
|
|
@@index([classId, createdAt])
|
|
@@index([courseId])
|
|
@@index([dueAt])
|
|
}
|
|
|
|
/// 学生作业完成状态
|
|
model HomeworkStatus {
|
|
id String @id @default(cuid())
|
|
homeworkId String
|
|
userId String
|
|
completed Boolean @default(false)
|
|
updatedAt DateTime @updatedAt
|
|
|
|
homework Homework @relation(fields: [homeworkId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([homeworkId, userId])
|
|
@@index([userId])
|
|
}
|
|
|
|
/// 通知
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
classId String
|
|
title String
|
|
content String
|
|
priority String @default("NORMAL")
|
|
createdBy String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
creator User @relation("NotificationCreator", fields: [createdBy], references: [id], onDelete: Cascade)
|
|
reads NotificationRead[]
|
|
|
|
@@index([classId, createdAt])
|
|
}
|
|
|
|
/// 通知已读记录
|
|
model NotificationRead {
|
|
id String @id @default(cuid())
|
|
notificationId String
|
|
userId String
|
|
readAt DateTime @default(now())
|
|
|
|
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([notificationId, userId])
|
|
@@index([userId])
|
|
}
|
|
|
|
|
|
/// 课表时间配置(ClassIsland 同款 JSON 导入后的节次时间表)
|
|
model TimeLayout {
|
|
id String @id @default(cuid())
|
|
classId String
|
|
name String
|
|
/// 来源:classisland | manual
|
|
source String @default("classisland")
|
|
/// 节次数组(JSON 字符串):[{ index, name, startTime, endTime, type, skipped }]
|
|
items String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([classId])
|
|
}
|
|
|
|
/// 成绩
|
|
model Grade {
|
|
id String @id @default(cuid())
|
|
classId String
|
|
courseId String?
|
|
userId String
|
|
examName String
|
|
score Float
|
|
totalScore Float @default(100)
|
|
publishedAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
course Course? @relation(fields: [courseId], references: [id], onDelete: SetNull)
|
|
student User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId, publishedAt])
|
|
@@index([classId, examName])
|
|
@@index([courseId])
|
|
}
|