feat(web): regroup console nav into 7 groups, split downloads/plans/tasks, add legacy redirects

- Sidebar: 概览/资源/订阅/下载/整理/Agent/系统, 18 items (+ 定时任务); single
  active item via findNavItem; 待处理 badge; collapsed rail groups; mobile drawer
  shares NAV_GROUPS.
- New pages: /downloads (queue + cache watermark header), /downloads/plans,
  /downloads/:id, /tasks (business tasks only) + /tasks/:id detail, /rules.
- Legacy links: ?tab= on tasks/subscriptions/sources redirect via clientLoader;
  /tasks/:downloadId falls back to /downloads/:id.
- List filters in URL query; settingsPath() helper for settings deep links.
- Copy per glossary (待处理, 收录策略, 引擎面板, 文件管理, 版本目标).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 02:46:47 +08:00
1 parent 5843527317
commit 909cd2ad49
31 files changed
+1247 -609

No files matched your search

+74 -40
View File
@@ -1,13 +1,14 @@
import type { SessionInfo } from "@umt/contracts";
import { ChevronLeft, ChevronRight, Clapperboard, LoaderCircle, Menu } from "lucide-react";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { Link, NavLink, Outlet, useLocation } from "react-router";
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 { CONSOLE_BASE, findNavItem, NAV_GROUPS, NAV_ITEMS, type NavGroup } from "~/components/console/nav.ts";
import { findNavItem, NAV_GROUPS, type NavGroup, type NavItem } 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";
@@ -17,9 +18,31 @@ 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);
@@ -62,29 +85,27 @@ function ExpandedNav({ groups, onNavigate }: { groups: NavGroup[]; onNavigate?:
{group.title}
</p>
<div className="space-y-0.5">
{group.items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.to === CONSOLE_BASE}
onClick={onNavigate}
className={({ isActive }) =>
cn(
{group.items.map((item) => {
const active = item.to === activeTo;
return (
<Link
key={item.to}
to={item.to}
onClick={onNavigate}
aria-current={active ? "page" : undefined}
className={cn(
"flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-base",
isActive
active
? "text-foreground"
: "text-muted-foreground hover:bg-foreground/[0.04] hover:text-foreground",
)
}
>
{({ isActive }) => (
<>
<item.icon className={cn("h-4 w-4 shrink-0", isActive && "text-primary")} />
<span className="flex-1 truncate">{item.label}</span>
</>
)}
</NavLink>
))}
)}
>
<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>
);
})}
</div>
</div>
))}
@@ -92,28 +113,41 @@ 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 />
{NAV_ITEMS.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.to === CONSOLE_BASE}
title={item.label}
className={({ isActive }) =>
cn(
"flex h-10 w-10 items-center justify-center rounded-xl transition-colors duration-micro",
isActive
? "seg-thumb text-primary"
: "text-muted-foreground hover:bg-foreground/[0.04] hover:text-foreground",
)
}
>
<item.icon className="h-[18px] w-[18px]" />
</NavLink>
{NAV_GROUPS.map((group, gi) => (
<div key={group.title} className="flex flex-col items-center gap-1">
{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}
aria-current={active ? "page" : undefined}
className={cn(
"relative flex h-10 w-10 items-center justify-center rounded-xl transition-colors duration-micro",
active
? "seg-thumb text-primary"
: "text-muted-foreground hover:bg-foreground/[0.04] hover:text-foreground",
)}
>
<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>
);
})}
</div>
))}
</nav>
);