feat(web): IA-3 navigation, legacy redirects and top-bar inbox (FE-1)

Sidebar becomes 5 groups (overview / resources / subscriptions / organize /
system). Pending items move to a top-bar inbox with badge and grouped
dropdown. Engine panel, schedules and identity pages redirect to the task
page, its plan tab and media overview views; the download plan becomes a
second tab of the download panel. nav.test and smoke cover the new routes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:10:42 +08:00
1 parent 4bb2583f6c
commit 738d4ce752
18 files changed
+647 -1104

No files matched your search

+5 -31
View File
@@ -3,12 +3,12 @@ import { ChevronLeft, ChevronRight, Clapperboard, LoaderCircle, Menu } from "luc
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { Link, Outlet, useLocation } from "react-router";
import { useSession } from "~/api/auth.ts";
import { useDecisionSummary } from "~/api/decisions.ts";
import { AgentDrawer, AgentDrawerTrigger } from "~/components/agent/agent-drawer.tsx";
import { AccountMenu } from "~/components/console/account-menu.tsx";
import { AgentLauncher, AgentNavEntry } from "~/components/console/agent-launcher.tsx";
import { ErrorState } from "~/components/console/empty-state.tsx";
import { findNavItem, NAV_GROUPS, type NavGroup, type NavItem } from "~/components/console/nav.ts";
import { InboxBell } from "~/components/console/inbox-bell.tsx";
import { findNavItem, NAV_GROUPS, type NavGroup, pageTitle } from "~/components/console/nav.ts";
import PrismLeak from "~/components/prism-leak.tsx";
import { ThemeToggle } from "~/components/theme-toggle.tsx";
import { Button } from "~/components/ui/button.tsx";
@@ -18,31 +18,10 @@ import { cn } from "~/lib/utils.ts";
const PORTAL_NAME = "媒体自动化";
/** 侧栏角标:待处理未决数 */
function useNavBadges(): Record<NonNullable<NavItem["badge"]>, number> {
const summary = useDecisionSummary();
return { decisions: summary.data?.total ?? 0 };
}
function NavBadge({ n, className }: { n: number; className?: string }) {
if (!n) return null;
return (
<span
className={cn(
"min-w-5 rounded-full bg-warning/15 px-1.5 text-center text-micro leading-5 font-semibold text-warning tabular-nums",
className,
)}
>
{n > 99 ? "99+" : n}
</span>
);
}
// ─── 展开态导航:选中项由一枚玻璃滑块(seg-thumb)滑到激活链接背后 ───
function ExpandedNav({ groups, onNavigate }: { groups: NavGroup[]; onNavigate?: () => void }) {
const location = useLocation();
const activeTo = findNavItem(location.pathname)?.to;
const badges = useNavBadges();
const navRef = useRef<HTMLElement>(null);
const [pill, setPill] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
@@ -102,7 +81,6 @@ function ExpandedNav({ groups, onNavigate }: { groups: NavGroup[]; onNavigate?:
>
<item.icon className={cn("h-4 w-4 shrink-0", active && "text-primary")} />
<span className="flex-1 truncate">{item.label}</span>
{item.badge ? <NavBadge n={badges[item.badge]} /> : null}
</Link>
);
})}
@@ -117,7 +95,6 @@ function ExpandedNav({ groups, onNavigate }: { groups: NavGroup[]; onNavigate?:
function CollapsedNav() {
const location = useLocation();
const activeTo = findNavItem(location.pathname)?.to;
const badges = useNavBadges();
return (
<nav className="flex flex-1 flex-col items-center gap-1 overflow-y-auto py-4">
<AgentNavEntry collapsed />
@@ -126,12 +103,11 @@ function CollapsedNav() {
{gi > 0 ? <span aria-hidden className="my-1 h-px w-6 bg-border" /> : null}
{group.items.map((item) => {
const active = item.to === activeTo;
const n = item.badge ? badges[item.badge] : 0;
return (
<Link
key={item.to}
to={item.to}
title={n ? `${item.label}(${n})` : item.label}
title={item.label}
aria-current={active ? "page" : undefined}
className={cn(
"relative flex h-10 w-10 items-center justify-center rounded-xl transition-colors duration-micro",
@@ -141,9 +117,6 @@ function CollapsedNav() {
)}
>
<item.icon className="h-[18px] w-[18px]" />
{n ? (
<span aria-hidden className="absolute top-1.5 right-1.5 h-1.5 w-1.5 rounded-full bg-warning" />
) : null}
</Link>
);
})}
@@ -200,7 +173,7 @@ function ConsoleShell({ session }: { session: SessionInfo }) {
const location = useLocation();
const [collapsed, setCollapsed] = useState(false);
const [sheetOpen, setSheetOpen] = useState(false);
const activeLabel = findNavItem(location.pathname)?.label ?? PORTAL_NAME;
const activeLabel = pageTitle(location.pathname) ?? PORTAL_NAME;
return (
<div className="app-scrollbars relative isolate flex min-h-screen overflow-x-hidden bg-background">
@@ -261,6 +234,7 @@ function ConsoleShell({ session }: { session: SessionInfo }) {
</div>
<div className="ml-auto flex items-center gap-2">
{/* 桌面有侧栏主入口与右下角悬浮按钮,顶栏小按钮只在窄屏保留 */}
<InboxBell />
<AgentDrawerTrigger className="xl:hidden" />
<ThemeToggle />
<AccountMenu session={session} />