feat(web): make the site workbench the home of sites

- 站点工作台: 登记站点 dialog (built-in site definitions or custom).
- Site detail tabs: 整理视图 / 原站视图 / 快照与 Feed / 账号 (account list +
  credential form).
- 来源健康 (was 来源/Feed/快照): cross-site health table + all feeds; site
  names deep-link to the site's 快照与 Feed tab. Feed table extracted to
  components/workbench/feed-table.tsx.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 02:46:55 +08:00
1 parent 909cd2ad49
commit ddb15145d8
7 files changed
+805 -296

No files matched your search

+52 -6
View File
@@ -1,18 +1,21 @@
import type { ResolutionStatus, WorkbenchRelease } from "@umt/contracts";
import { ArrowLeft, Bot, Camera, Globe, Radar, SearchX, TriangleAlert } from "lucide-react";
import { ArrowLeft, Bot, Camera, Globe, Plus, Radar, SearchX, TriangleAlert } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Link, useParams } from "react-router";
import { Link, useParams, useSearchParams } from "react-router";
import { toast } from "sonner";
import { useInventory } from "~/api/inventory.ts";
import { type ReleaseFilters, useSiteReleases } from "~/api/releases.ts";
import { snapshotStale, useSite, useSiteAccounts, useSnapshotSite } from "~/api/sites.ts";
import { openAgentDrawer } from "~/components/agent/agent-store.ts";
import { DetailSection } from "~/components/console/detail-sheet.tsx";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
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 { Button } from "~/components/ui/button.tsx";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx";
import { FeedTable } from "~/components/workbench/feed-table.tsx";
import { ReleaseDetailSheet } from "~/components/workbench/release-detail-sheet.tsx";
import {
accountSummary,
@@ -24,6 +27,8 @@ import {
SITE_HEALTH,
} from "~/components/workbench/release-fields.ts";
import { ReleaseRow } from "~/components/workbench/release-list.tsx";
import { SiteAccountsPanel } from "~/components/workbench/site-accounts.tsx";
import { SnapshotPanel } from "~/components/workbench/snapshot-panel.tsx";
import { errorMessage } from "~/lib/api.ts";
const PROMOS = [
@@ -84,8 +89,16 @@ function useDebounced<T>(value: T, ms = 400) {
return v;
}
const TABS = ["organized", "origin", "snapshots", "accounts"] as const;
export const meta = () => [{ title: "站点 · UHD 媒体自动化控制台" }];
export default function SiteWorkbenchPage() {
const { siteId = "" } = useParams();
const [params, setParams] = useSearchParams();
const rawTab = params.get("tab") ?? "";
const tab = (TABS as readonly string[]).includes(rawTab) ? rawTab : "organized";
const [creatingFeed, setCreatingFeed] = useState(false);
const site = useSite(siteId);
const accounts = useSiteAccounts(siteId);
const snap = useSnapshotSite();
@@ -169,9 +182,9 @@ export default function SiteWorkbenchPage() {
actions={
<>
<Button asChild variant="ghost" size="sm">
<Link to="/console/sites">
<Link to={consolePath("sites")}>
<ArrowLeft />
站点
站点工作台
</Link>
</Button>
<Button
@@ -185,10 +198,24 @@ export default function SiteWorkbenchPage() {
}
/>
<Tabs defaultValue="organized">
<TabsList>
<Tabs
value={tab}
onValueChange={(v) =>
setParams(
(p) => {
if (v === "organized") p.delete("tab");
else p.set("tab", v);
return p;
},
{ replace: true },
)
}
>
<TabsList aria-label="站点分节">
<TabsTrigger value="organized">整理视图</TabsTrigger>
<TabsTrigger value="origin">原站视图</TabsTrigger>
<TabsTrigger value="snapshots">快照与 Feed</TabsTrigger>
<TabsTrigger value="accounts">账号</TabsTrigger>
</TabsList>
<TabsContent value="organized">
@@ -337,6 +364,25 @@ export default function SiteWorkbenchPage() {
description={<>计划在 M2 提供。资源详情里有站点 ID 和来源时间,可据此到原站查看。</>}
/>
</TabsContent>
<TabsContent value="snapshots">
<div className="space-y-8">
{s ? <SnapshotPanel site={s} /> : <LoadingState rows={3} />}
<DetailSection
title="Feed"
actions={
<Button size="sm" variant="outline" onClick={() => setCreatingFeed(true)}>
<Plus />
新建 Feed
</Button>
}
>
<FeedTable siteId={siteId} creating={creatingFeed} onCreatingChange={setCreatingFeed} />
</DetailSection>
</div>
</TabsContent>
<TabsContent value="accounts">{s ? <SiteAccountsPanel site={s} /> : <LoadingState rows={3} />}</TabsContent>
</Tabs>
<ReleaseDetailSheet