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:
1 parent
5843527317
commit
909cd2ad49
31 files changed
+1247
-609
No files matched your search
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user