From 86430c5f4a5871c70fe439558167f6e13d78eb2f Mon Sep 17 00:00:00 2001 From: LoveChengke Date: Sat, 12 Sep 2026 22:36:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(island):=20=E4=BF=AE=E5=A5=BD"=E9=9D=9E?= =?UTF-8?q?=E7=B4=A7=E6=80=A5=E5=8F=AB=E4=BA=BA=20/=20=E7=B4=A7=E6=80=A5?= =?UTF-8?q?=E5=8F=AB=E4=BA=BA=E6=94=B6=E8=B5=B7=20/=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=A1=B5=E9=A2=84=E8=A7=88"=E4=B8=89=E5=A4=84=E7=82=B9?= =?UTF-8?q?=E4=B8=8D=E5=BC=80=EF=BC=8C=E5=B9=B6=E8=AE=A9=E8=83=B6=E5=9B=8A?= =?UTF-8?q?=E6=B1=87=E6=80=BB=E9=80=9A=E7=9F=A5=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈的三个场景: 1) 非紧急叫人(HIGH + kind=call)—— 上课时段原先只进队列、屏幕上看不到东西, 自然"无法点开"。现在:上课时段仍以**胶囊**保持可见(不自动展开、不打断课堂), 点一下即可展开详情;下课自动展开。新增 isOpenable():紧急消息与"任何叫人"都允许点开。 2) 紧急叫人点击缩回后点不开 —— 归因于固定包围盒窗口的命中丢失(上一轮已改为光标兜底轮询), 本轮补上"展开 → 收起 → 再次展开"的叫人类专项回归。 3) 设置页"预览效果"示例岛无法正常展开 —— 预览通知现在走 preview 通道:直接展开、 失焦不收起(用户拖滑块时主窗口一直是焦点)、30 秒后自行消失、点"知道了"不留残影。 顺带修掉两个真问题: - pushNotification 在"胶囊态"收到新消息时会把上一条静默顶掉(只有展开态才回队); 现在只要换了当前展示的消息,上一条一律回队列 —— 否则类型汇总与"还有 N 条"都会漏。 - syncWindow 的上课时段守卫只认"紧急",导致普通叫人被 hideImmediately 藏掉;改用 isOpenable。 胶囊类型汇总(用户要求):多条未处理消息时收起态显示 「新消息:叫人/作业/通知(共 3 条)」+「点击查看」,并把三种类型做成小标签; 单条时仍显示类型名(新作业 / 老师叫你 / 新消息)。 性能:岛体矩形上报加限流(~10 次/秒,尾随补发最终形态),修掉"每帧 IPC + 每帧读光标" 导致的渲染变慢(实测会让开合采样从 57 帧掉到 4 帧)。 验证:verify:desktop 71/71 + 离线复跑 65/65;typecheck/lint 全绿。 --- .../desktop-client/src/island/IslandApp.vue | 98 +++++++++- packages/desktop-client/src/main/island.ts | 107 +++++++++-- packages/desktop-client/src/main/smoke.ts | 177 ++++++++++++++++-- .../src/renderer/views/SettingsView.vue | 4 +- .../desktop-client/src/types/desktop.d.ts | 2 + 5 files changed, 353 insertions(+), 35 deletions(-) diff --git a/packages/desktop-client/src/island/IslandApp.vue b/packages/desktop-client/src/island/IslandApp.vue index 4898f8f..2848318 100644 --- a/packages/desktop-client/src/island/IslandApp.vue +++ b/packages/desktop-client/src/island/IslandApp.vue @@ -58,6 +58,43 @@ const pillTitle = computed(() => { return isUrgent.value ? '紧急通知' : '新消息'; }); +/** + * 未处理消息的**类型汇总**(当前展示的 + 队列里的): + * 用户要求收起后的胶囊直接说明这批消息里都有什么,例如 + * 「新消息:叫人/作业/通知(共 3 条)」+「点击查看」。 + */ +const TYPE_LABELS: Record = { + call: '叫人', + homework: '作业', + notification: '通知', +}; +/** 汇总展示顺序:叫人 > 作业 > 通知(与用户示例一致) */ +const TYPE_ORDER: IslandNotificationKind[] = ['call', 'homework', 'notification']; + +const pendingNotifications = computed(() => { + const list: IslandNotification[] = []; + if (state.value.active) list.push(state.value.active); + for (const item of state.value.queued) { + if (!list.some((existing) => existing.id === item.id)) list.push(item); + } + return list; +}); + +/** 这批未处理消息覆盖了哪些类型(按"叫人/作业/通知"顺序) */ +const pendingTypes = computed(() => { + const kinds = new Set(pendingNotifications.value.map((item) => item.kind ?? 'notification')); + return TYPE_ORDER.filter((item) => kinds.has(item)).map((item) => TYPE_LABELS[item]); +}); + +const pendingCount = computed(() => pendingNotifications.value.length); + +/** 多条消息时把类型摊开:`新消息:叫人/作业/通知(共 3 条)` */ +const pillSummaryTitle = computed(() => { + if (pendingCount.value <= 1) return pillTitle.value; + const types = pendingTypes.value.join('/'); + return types ? `新消息:${types}(共 ${pendingCount.value} 条)` : `新消息(共 ${pendingCount.value} 条)`; +}); + /** 展开态徽标:叫人 / 新作业 / 优先级文案 */ const badgeText = computed(() => { if (isCall.value) return '叫人'; @@ -301,8 +338,11 @@ const islandClass = computed(() => ({ let lastInteractive: boolean | null = null; /** 最近一次已知的指针位置:岛收起/变大后用它重算命中,避免"鼠标没动就点不到" */ let lastPointer: { x: number; y: number } | null = null; -/** 最近一次上报给主进程的岛体矩形(去重,避免形变期间刷屏 IPC) */ +/** 最近一次上报给主进程的岛体矩形(去重 + 限流,避免形变期间刷屏 IPC 拖慢渲染) */ let lastHitRectKey: string | null = null; +let lastHitRectAt = 0; +const HIT_RECT_MIN_INTERVAL_MS = 100; +let hitRectTrailingTimer: number | null = null; /** 把岛体矩形上报给主进程:主进程按光标位置兜底校正命中(不依赖 mousemove 转发) */ function reportHitRect(rect: DOMRect | null): void { @@ -312,7 +352,30 @@ function reportHitRect(rect: DOMRect | null): void { : 'none'; if (key === lastHitRectKey) return; lastHitRectKey = key; - bridge?.setHitRect?.(payload); + const now = performance.now(); + const elapsed = now - lastHitRectAt; + if (elapsed >= HIT_RECT_MIN_INTERVAL_MS) { + lastHitRectAt = now; + bridge?.setHitRect?.(payload); + return; + } + // 限流:形变过程中每帧都发会挤占主进程(实测拖慢到采样都变少), + // 这里保证"最后一次形状"一定会送达(尾随发送)。 + if (hitRectTrailingTimer !== null) window.clearTimeout(hitRectTrailingTimer); + hitRectTrailingTimer = window.setTimeout(() => { + hitRectTrailingTimer = null; + lastHitRectAt = performance.now(); + const node = document.querySelector('.island-card') as HTMLElement | null; + const fullyHidden = state.value?.mode === 'hidden' && !appearance.value.idleSliver; + const latest = node && !fullyHidden ? node.getBoundingClientRect() : null; + const latestPayload = latest + ? { x: latest.left, y: latest.top, width: latest.width, height: latest.height } + : null; + lastHitRectKey = latestPayload + ? `${Math.round(latestPayload.x)},${Math.round(latestPayload.y)},${Math.round(latestPayload.width)},${Math.round(latestPayload.height)}` + : 'none'; + bridge?.setHitRect?.(latestPayload); + }, HIT_RECT_MIN_INTERVAL_MS); } function updateInteractive(clientX: number, clientY: number): void { @@ -449,11 +512,15 @@ onMounted(async () => { - - {{ pillTitle }} - + {{ pillSummaryTitle }} + + + - {{ notification?.teacherName || '老师' }} · 点击查看 + + +