feat(web): D4 会话未读徽标 — 打开即已读 + 消费 session_read
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- SessionMeta.unreadCount(可选,兼容旧后端)+ api/sessions.markSessionRead
- lib/sessionRead.ts:会话历史落地后再推游标(best-effort,失败静默,下次 GET /sessions 收敛)+ 帧归属判定(ws.ts 因此留在 500 行内)
- store.clearUnread 无变化时返回同一 state,避免多余重渲染;Nav 徽标沿用 Miuix token(mu-radius-pill/mu-err/mu-rise-in)
- 逻辑测试 35 → 54 项,含变异校验(改成 clearUnread(activeSessionId) 即 FAIL)
This commit is contained in:
AskaEth committed 2026-10-03 16:47:56 +08:00
1 parent e8eccd2bb2
commit a828edacc5
9 files changed
+235 -11

No files matched your search

+14 -1
View File
@@ -102,6 +102,7 @@ function NavContent({ onNavigate }: { onNavigate: () => void }): React.ReactNode
active={s.id === activeSessionId}
icon={s.type === "main" ? "chat" : "folder"}
label={s.title ?? (s.type === "main" ? "主会话" : "次级会话")}
badge={s.unreadCount ?? 0}
onClick={() => selectSession(s.id)}
/>
))}
@@ -190,12 +191,15 @@ function NavItem({
onClick,
active = false,
hint,
badge = 0,
}: {
icon: IconName;
label: string;
onClick: () => void;
active?: boolean;
hint?: string;
/** Unread count (协同 B3/D4); 0 or absent renders no badge. */
badge?: number;
}): React.ReactNode {
return (
<button
@@ -208,7 +212,16 @@ function NavItem({
>
<Icon name={icon} size={18} />
<span>{label}</span>
{hint ? <span className="ml-auto text-[10px] text-[var(--mu-text-lo)]">{hint}</span> : null}
{badge > 0 ? (
<span
className="mu-rise-in ml-auto inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[var(--mu-radius-pill)] bg-[var(--mu-err)] px-1.5 text-[10px] font-medium leading-none text-white"
aria-label={`${badge} 条未读`}
>
{badge > 99 ? "99+" : badge}
</span>
) : hint ? (
<span className="ml-auto text-[10px] text-[var(--mu-text-lo)]">{hint}</span>
) : null}
</button>
);
}