灵动岛"收起后部分情况无法再次打开"(用户反馈): - 收起不再等于消失:只要有未处理通知(当前展示的或队列里的),超时后保持胶囊常驻, 只有"知道了 / 标为已读"、队列清空或进入上课时段才彻底隐藏。 - 收起态点击兜底:active 为空但队列还有未看通知时,点击胶囊会把它取出并展开; 上课时段仍严格不显示(该兜底放在上课守卫之后,不影响"上课不打扰")。 - 命中测试自愈:渲染进程记住最近指针位置,状态/外观变化与形变每一帧都按新几何重算命中; 岛可见时主进程也会重新下发可交互,避免"窗口还处于穿透 → 点了没反应"。 - 新增 3 条回归断言:收起后胶囊常驻、收起态点击可再次展开(命中恢复)、空闲细缝也可点开。 作业"截止时间"功能下线(用户要求): - 接口/类型:HomeworkDto 与创建请求去掉 dueAt,homeworks schema/service 不再读写; 仪表盘去掉"即将截止的作业"卡片与待截止统计口径(改为"未完成/待完成")。 - Web:作业发布表单移除截止时间选择、列表与详情移除截止时间列; 学生端作业列表/详情移除截止时间与"已逾期"状态(只保留完成状态)。 - 灵动岛:作业卡不再显示"截止时间:…"副标题,改为只展示作业要求。 - DB 中的 Homework.dueAt 列**保留**(已在 schema 注明为遗留字段),避免对已安装库做破坏性迁移; 种子数据不再写入该列。 Web 管理端圆角设计(用户要求): - 新增设计令牌 --ch-radius-xl/lg/md/sm/pill 与两级柔和投影,并让 Element Plus 的 --el-border-radius-* 对齐同一套令牌;卡片、弹窗、抽屉、按钮、输入、标签、分页、 侧栏菜单项、表格表头统一改造,卡片去硬边框改投影 + 悬停微浮起,顶栏/侧栏做圆角收口。 - 未改组件源码,全部为 CSS 变量 + 类选择器覆盖;手机端媒体查询保持生效(截图已更新)。 验证:verify:desktop 62/62(含 3 条收起态回归)与离线复跑 56/56、打包便携版 62/62(packaged=true)、 verify:e2e 136/136(新增"作业接口不再返回 dueAt")、verify:web 20/20、typecheck/lint/format 全绿; 服务端与 Web 已重新打包并部署到本机安装目录。
261 lines
8.1 KiB
Plaintext
261 lines
8.1 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?
|
|
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)
|
|
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])
|
|
}
|