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

@@ -186,12 +186,15 @@ function syncTargets(snap = false): void {
springs.radius.snap(radius);
morph.value = { width: size.width, height: size.height, radius };
progress.value = mode.value === 'expanded' ? 1 : 0;
refreshInteractive();
return;
}
springs.width.setTarget(size.width);
springs.height.setTarget(size.height);
springs.radius.setTarget(radius);
startFrameLoop();
// 目标几何变了:立刻按新尺寸重算一次命中(形变过程中每帧也会重算)
refreshInteractive();
}
function startFrameLoop(): void {
@@ -218,6 +221,8 @@ function startFrameLoop(): void {
span < 1 ? (mode.value === 'expanded' ? 1 : 0) : Math.abs(morph.value.height - pill) / span;
if (moving || !springs.width.settled || !springs.height.settled || !springs.radius.settled) {
// 形变过程中岛的可点区域在变:每帧按缓存指针位置重算命中(值不变时不发 IPC)
refreshInteractive();
rafHandle = requestAnimationFrame(frame);
return;
}
@@ -228,6 +233,7 @@ function startFrameLoop(): void {
springs.radius.snap(targetRadius(size));
morph.value = { width: size.width, height: size.height, radius: springs.radius.value };
progress.value = mode.value === 'expanded' ? 1 : 0;
refreshInteractive();
rafHandle = null;
};
rafHandle = requestAnimationFrame(frame);
@@ -293,7 +299,11 @@ const islandClass = computed(() => ({
* 指针在岛体上(或可交互控件上)时才让窗口接收鼠标,否则穿透,保证不吞桌面点击。
*/
let lastInteractive: boolean | null = null;
/** 最近一次已知的指针位置:岛收起/变大后用它重算命中,避免"鼠标没动就点不到" */
let lastPointer: { x: number; y: number } | null = null;
function updateInteractive(clientX: number, clientY: number): void {
lastPointer = { x: clientX, y: clientY };
const node = document.querySelector('.island-card') as HTMLElement | null;
let inside = false;
if (node) {
@@ -310,6 +320,29 @@ function updateInteractive(clientX: number, clientY: number): void {
bridge?.setInteractive?.(inside);
}
/**
* 状态 / 外观变化后主动重算一次命中测试。
*
* 为什么必须主动算:窗口默认 `setIgnoreMouseEvents(true, {forward:true})`,
* 只有在收到 mousemove 时才会打开命中。岛"收起 / 展开 / 换形态"时几何变了,
* 但指针可能一动没动(没有新的 mousemove)—— 此时窗口会一直保持"穿透",
* 表现就是**收起后怎么点都打不开**(用户反馈)。所以每次状态变化都用缓存的指针位置重算一次。
*/
function refreshInteractive(): void {
const fullyHidden = state.value?.mode === 'hidden' && !appearance.value.idleSliver;
if (fullyHidden) {
if (lastInteractive !== false) {
lastInteractive = false;
bridge?.setInteractive?.(false);
}
return;
}
if (!lastPointer) return;
const pointer = lastPointer;
lastInteractive = null; // 强制按新几何再算一次
updateInteractive(pointer.x, pointer.y);
}
function applyAppearance(next: IslandAppearance | null | undefined): void {
if (!next) return;
appearance.value = next;