feat(web,desktop,server): 灵动岛收起后可再次打开 · 下线作业截止时间 · Web 端圆角设计

灵动岛"收起后部分情况无法再次打开"(用户反馈):
- 收起不再等于消失:只要有未处理通知(当前展示的或队列里的),超时后保持胶囊常驻,
  只有"知道了 / 标为已读"、队列清空或进入上课时段才彻底隐藏。
- 收起态点击兜底: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 已重新打包并部署到本机安装目录。
This commit is contained in:
LoveChengke committed 2026-09-12 21:01:31 +08:00
1 parent 0690546307
commit b69f708aa2
23 files changed
+534 -222

No files matched your search

+2
View File
@@ -155,6 +155,8 @@ model Homework {
title String
content String
attachmentUrl String?
/// 遗留字段:作业"截止时间"功能已下线(UI/接口均不再读写)。
/// 保留列以免对已安装的库做破坏性迁移;新库该列恒为 null。
dueAt DateTime?
createdBy String
createdAt DateTime @default(now())
+1 -6
View File
@@ -186,37 +186,33 @@ async function main(): Promise<void> {
}
// ---------------------------------------------------------------- 作业
// 注:作业"截止时间"功能已下线,种子数据不再写入 dueAt(库字段保留以兼容已装库)
const now = Date.now();
const day = 86_400_000;
const homeworkSeeds = [
{
title: '《劝学》全文背诵',
content: '背诵《劝学》并录制音频上传到班级群,注意断句。',
dueInDays: 3,
courseIndex: 0,
},
{
title: '数学必修一 3.2 习题',
content: '完成课本 P78 习题 3.2 全部题目,第 12 题选做。',
dueInDays: 1,
courseIndex: 1,
},
{
title: '英语周记一篇',
content: '以"My School Life"为题写一篇 150 词左右的英语周记。',
dueInDays: 5,
courseIndex: 2,
},
{
title: '物理实验报告',
content: '整理"探究匀变速直线运动"实验数据,撰写实验报告。',
dueInDays: -2,
courseIndex: 3,
},
{
title: '化学方程式默写',
content: '默写第一至第三章全部化学方程式,家长签字。',
dueInDays: -1,
courseIndex: 4,
},
];
@@ -234,7 +230,6 @@ async function main(): Promise<void> {
content: seed.content,
attachmentUrl:
seed.courseIndex % 2 === 0 ? 'https://example.com/classhelper/homework-sample.pdf' : null,
dueAt: new Date(now + seed.dueInDays * day),
createdBy: targetClass.teacherId,
},
});
+6 -1
View File
@@ -198,11 +198,16 @@ async function main() {
courseId,
title: `联调验证作业 ${new Date().toISOString()}`,
content: '请完成验证脚本创建的作业。',
dueAt: new Date(Date.now() + 86_400_000).toISOString(),
},
});
const homeworkId = createdHomework.payload?.data?.id;
record('教师发布作业', createdHomework.status === 201, `status=${createdHomework.status}`);
// 截止时间功能已下线:接口不再返回该字段(历史数据保留在库里但不读写)
record(
'作业接口不再返回截止时间(dueAt 已下线)',
createdHomework.payload?.data?.dueAt === undefined,
`dueAt=${String(createdHomework.payload?.data?.dueAt)}`,
);
try {
const { payload, elapsed } = await homeworkWait;
-2
View File
@@ -190,7 +190,6 @@ export interface HomeworkLike {
title: string;
content: string;
attachmentUrl: string | null;
dueAt: Date | null;
createdBy: string;
createdAt: Date;
course?: CourseBriefLike | null;
@@ -228,7 +227,6 @@ export function toHomeworkDto(
title: item.title,
content: item.content,
attachmentUrl: item.attachmentUrl,
dueAt: toIsoOrNull(item.dueAt),
createdBy: item.createdBy,
createdAt: toIso(item.createdAt),
course: item.course ? toCourseBrief(item.course) : null,
@@ -25,7 +25,6 @@ export async function getDashboardSummary(user: TokenPayload): Promise<Dashboard
// 子表按 classId 过滤;Class 表本身按主键 id 过滤
const where = classScopeWhere(scope);
const classWhere = classScopeIdFilter(scope);
const now = new Date();
// 班级账号(班级设备)以全班学生为范围;普通学生即自己
const personalIds = await resolvePersonalIds(user);
const personalWhere = personalIdWhere(personalIds);
@@ -42,7 +41,6 @@ export async function getDashboardSummary(user: TokenPayload): Promise<Dashboard
pendingHomeworkCount,
recentNotifications,
recentHomeworks,
upcoming,
] = await Promise.all([
prisma.class.count({ where: classWhere }),
prisma.user.count({ where: { ...where, role: 'STUDENT' } }),
@@ -56,7 +54,7 @@ export async function getDashboardSummary(user: TokenPayload): Promise<Dashboard
? prisma.homework.count({
where: { ...where, statuses: { none: { ...personalWhere, completed: true } } },
})
: prisma.homework.count({ where: { ...where, dueAt: { gte: now } } }),
: prisma.homework.count({ where }),
prisma.notification.findMany({
where,
include: { creator: creatorSelect, reads: { where: personalWhere } },
@@ -73,16 +71,6 @@ export async function getDashboardSummary(user: TokenPayload): Promise<Dashboard
orderBy: { createdAt: 'desc' },
take: 5,
}),
prisma.homework.findMany({
where: { ...where, dueAt: { gte: now } },
include: {
course: courseSelect,
creator: creatorSelect,
statuses: student ? { where: personalWhere } : true,
},
orderBy: { dueAt: 'asc' },
take: 5,
}),
]);
return {
@@ -101,8 +89,5 @@ export async function getDashboardSummary(user: TokenPayload): Promise<Dashboard
recentHomeworks: recentHomeworks.map((item): HomeworkDto =>
toHomeworkDto(item, { userIds: student ? personalIds : [], withStatus: !student }),
),
upcomingDeadlines: upcoming.map((item): HomeworkDto =>
toHomeworkDto(item, { userIds: student ? personalIds : [], withStatus: !student }),
),
};
}
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { booleanFlagSchema, optionalDateTimeSchema } from '../../lib/schemas.js';
import { booleanFlagSchema } from '../../lib/schemas.js';
export const listHomeworksQuerySchema = z.object({
classId: z.string().min(1).optional(),
@@ -15,7 +15,6 @@ export const createHomeworkSchema = z.object({
title: z.string().trim().min(1, '请输入作业标题').max(120),
content: z.string().min(1, '请输入作业内容').max(5000),
attachmentUrl: z.string().trim().max(500).nullish(),
dueAt: optionalDateTimeSchema,
});
export const updateHomeworkSchema = z.object({
@@ -23,7 +22,6 @@ export const updateHomeworkSchema = z.object({
title: z.string().trim().min(1).max(120).optional(),
content: z.string().min(1).max(5000).optional(),
attachmentUrl: z.string().trim().max(500).nullish(),
dueAt: optionalDateTimeSchema,
});
export const updateHomeworkStatusSchema = z.object({
@@ -10,7 +10,6 @@ import { prisma } from '../../lib/db.js';
import { ApiError } from '../../lib/http.js';
import type { TokenPayload } from '../../lib/jwt.js';
import { toHomeworkDto, toHomeworkStatusDto } from '../../lib/mappers.js';
import { parseOptionalDate } from '../../lib/schemas.js';
import { personalIdWhere, resolvePersonalIds } from '../../lib/session.js';
import { emitToClass } from '../../realtime/bus.js';
import type {
@@ -101,7 +100,6 @@ export async function createHomework(user: TokenPayload, input: CreateHomeworkIn
title: input.title,
content: input.content,
attachmentUrl: input.attachmentUrl ?? null,
dueAt: parseOptionalDate(input.dueAt),
createdBy: user.sub,
},
include: { course: courseSelect, creator: creatorSelect, statuses: true },
@@ -130,7 +128,6 @@ export async function updateHomework(
...(input.content ? { content: input.content } : {}),
...(input.courseId !== undefined ? { courseId: input.courseId ?? null } : {}),
...(input.attachmentUrl !== undefined ? { attachmentUrl: input.attachmentUrl ?? null } : {}),
...(input.dueAt !== undefined ? { dueAt: parseOptionalDate(input.dueAt) } : {}),
},
include: { course: courseSelect, creator: creatorSelect, statuses: true },
});