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:
1 parent
4bb2583f6c
commit
738d4ce752
18 files changed
+647
-1104
No files matched your search
@@ -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: (
|
||||
|
||||
Reference in new issue
Block a user