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

+64 -6
View File
@@ -1,5 +1,5 @@
import type { LibraryItem, LibraryListQuery, MediaKind, SlotStatus } from "@umt/contracts";
import { Download, Library as LibraryIcon, RotateCw } from "lucide-react";
import { Download, Inbox, Library as LibraryIcon, RotateCw } from "lucide-react";
import { useDeferredValue } from "react";
import { Link, useNavigate, useSearchParams } from "react-router";
import { useLibraryList } from "~/api/library.ts";
@@ -8,9 +8,15 @@ import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { PageHeader } from "~/components/console/page-header.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { AliasesPanel } from "~/components/library/identity-aliases.tsx";
import { INBOX_IDENTITY } from "~/components/library/identity-pool-sheet.tsx";
import { ExternalTab, PoolTab } from "~/components/library/identity-views.tsx";
import { ENTITY_STATUS, MEDIA_KIND, opts, SLOT_STATUS, SLOT_STATUS_ORDER } from "~/components/library/labels.ts";
import { CoverPlaceholder, relTime, Tone } from "~/components/library/parts.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx";
export const meta = () => [{ title: "媒体概览 · UHD 媒体自动化控制台" }];
const FILTERS: { value: NonNullable<LibraryListQuery["filter"]>; label: string }[] = [
{ value: "gap", label: "有缺口" },
@@ -34,7 +40,62 @@ function SlotSummary({ counts }: { counts: LibraryItem["slotCounts"] }) {
);
}
type View = "works" | "unidentified" | "identities" | "aliases";
function viewOf(sp: URLSearchParams): View {
if (sp.get("filter") === "unidentified") return "unidentified";
const v = sp.get("view");
return v === "identities" || v === "aliases" ? v : "works";
}
const VIEW_QUERY: Record<View, string> = {
works: "",
unidentified: "filter=unidentified",
identities: "view=identities",
aliases: "view=aliases",
};
export default function LibraryPage() {
const [sp, setSp] = useSearchParams();
const view = viewOf(sp);
return (
<>
<PageHeader
title="媒体概览"
description="本地账本:作品、版本目标、资产与外部身份。自动匹配不覆盖人工锁定的外部身份。"
actions={
view === "unidentified" ? (
<Button asChild variant="outline" size="sm">
<Link to={INBOX_IDENTITY}>
<Inbox />
确认身份
</Link>
</Button>
) : undefined
}
/>
<Tabs value={view} onValueChange={(v) => setSp(new URLSearchParams(VIEW_QUERY[v as View]), { replace: true })}>
<TabsList className="mb-4">
<TabsTrigger value="works">作品</TabsTrigger>
<TabsTrigger value="unidentified">待识别</TabsTrigger>
<TabsTrigger value="identities">外部身份</TabsTrigger>
<TabsTrigger value="aliases">别名</TabsTrigger>
</TabsList>
</Tabs>
{view === "unidentified" ? (
<PoolTab />
) : view === "identities" ? (
<ExternalTab />
) : view === "aliases" ? (
<AliasesPanel />
) : (
<WorksView />
)}
</>
);
}
function WorksView() {
const [sp, setSp] = useSearchParams();
const navigate = useNavigate();
const kind = (sp.get("kind") ?? "") as MediaKind | "";
@@ -121,16 +182,13 @@ export default function LibraryPage() {
return (
<>
<PageHeader
title="媒体库"
<FilterBar
actions={
<Button variant="outline" size="sm" onClick={() => list.refetch()} disabled={list.isFetching}>
<RotateCw className={list.isFetching ? "animate-spin" : undefined} />
刷新
</Button>
}
/>
<FilterBar
search={{ value: q, onChange: (v) => set("q", v), placeholder: "标题 / 原名 / 别名" }}
onReset={filtered ? () => setSp(new URLSearchParams(), { replace: true }) : undefined}
>
@@ -149,7 +207,7 @@ export default function LibraryPage() {
filtered
? { title: "没有符合条件的作品", description: "换个关键字或清除筛选。", icon: LibraryIcon }
: {
title: "媒体库为空",
title: "还没有作品",
description: "扫描库根或新建订阅后,作品会出现在这里。",
icon: LibraryIcon,
action: (