Files
UHD_Media_Tool/apps/web/app/routes/console/sources.tsx
T
rootandClaude Opus 5.5 ddb15145d8 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]>
2026-09-28 02:46:55 +08:00

146 lines
5.3 KiB
TypeScript

import type { FeedHealth, Site } from "@umt/contracts";
import { Globe, Plus, RefreshCw } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { useFeeds } from "~/api/feeds.ts";
import { snapshotStale, useSiteAccounts, useSites } from "~/api/sites.ts";
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
import { DetailSection } from "~/components/console/detail-sheet.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 { useQueryState } from "~/components/console/use-query-state.ts";
import { Button } from "~/components/ui/button.tsx";
import { FEED_HEALTH, FeedTable } from "~/components/workbench/feed-table.tsx";
import { accountSummary, circuitLabel, relTime, SITE_HEALTH } from "~/components/workbench/release-fields.ts";
import { legacyRedirectLoader } from "./redirects.ts";
// 来源健康:跨站点的 Feed 列表与站点健康总表;单个站点的快照、Feed、账号在站点详情。
export const meta = () => [{ title: "来源健康 · UHD 媒体自动化控制台" }];
// 旧的 ?tab=snapshots&siteId= 跳到站点详情的"快照与 Feed"
export const clientLoader = legacyRedirectLoader;
export default function SourcesPage() {
const f = useQueryState(["health", "siteId"] as const);
const [creating, setCreating] = useState(false);
const sites = useSites();
const accounts = useSiteAccounts();
const feeds = useFeeds({
health: (f.values.health || undefined) as FeedHealth | undefined,
siteId: f.values.siteId || undefined,
});
const siteColumns: DataColumn<Site>[] = [
{
key: "name",
header: "站点",
cell: (s) => (
<Link
to={`${consolePath("sites", s.id)}?tab=snapshots`}
className="font-medium text-foreground hover:underline"
onClick={(e) => e.stopPropagation()}
>
{s.name}
</Link>
),
},
{
key: "health",
header: "健康",
cell: (s) => (
<div className="flex flex-wrap gap-1">
<StatusBadge tone={SITE_HEALTH[s.healthStatus].tone}>{SITE_HEALTH[s.healthStatus].label}</StatusBadge>
{circuitLabel(s) ? <StatusBadge tone="warning">{circuitLabel(s)}</StatusBadge> : null}
{!s.enabled && s.healthStatus !== "disabled" ? <StatusBadge>已停用</StatusBadge> : null}
</div>
),
},
{
key: "account",
header: "账号",
hideBelow: "md",
cell: (s) => {
const a = accountSummary((accounts.data?.items ?? []).filter((x) => x.siteId === s.id));
return <StatusBadge tone={a.tone}>{a.label}</StatusBadge>;
},
},
{
key: "snapshot",
header: "最近快照",
hideBelow: "sm",
cell: (s) => (
<span className="flex flex-wrap items-center gap-1.5 whitespace-nowrap text-muted-foreground">
{s.snapshotEnabled ? relTime(s.lastSnapshotAt) : "未开启"}
{s.snapshotEnabled && snapshotStale(s) ? <StatusBadge tone="warning">已过期</StatusBadge> : null}
</span>
),
},
];
return (
<>
<PageHeader
title="来源健康"
description="单个站点的快照、Feed 与账号在站点工作台。"
actions={
<Button size="sm" onClick={() => setCreating(true)}>
<Plus />
新建 Feed
</Button>
}
/>
<div className="space-y-8">
<DetailSection title="站点">
<DataTable
columns={siteColumns}
rows={sites.data?.items}
rowKey={(s) => s.id}
loading={sites.isPending}
error={sites.error}
onRetry={() => void sites.refetch()}
empty={{
icon: Globe,
title: "还没有登记站点",
description: "在站点工作台登记。",
}}
/>
</DetailSection>
<DetailSection title="Feed">
<FilterBar
onReset={f.active ? f.reset : undefined}
actions={
<Button variant="ghost" size="sm" onClick={() => void feeds.refetch()} disabled={feeds.isFetching}>
<RefreshCw className={feeds.isFetching ? "animate-spin" : undefined} />
刷新
</Button>
}
>
<FilterSelect
label="健康"
value={f.values.health}
onChange={(v) => f.set("health", v)}
options={Object.entries(FEED_HEALTH).map(([value, h]) => ({ value, label: h.label }))}
/>
<FilterSelect
label="站点"
value={f.values.siteId}
onChange={(v) => f.set("siteId", v)}
options={(sites.data?.items ?? []).map((s) => ({ value: s.id, label: s.name }))}
/>
</FilterBar>
<FeedTable
health={(f.values.health || undefined) as FeedHealth | undefined}
siteId={f.values.siteId || undefined}
filtered={f.active}
creating={creating}
onCreatingChange={setCreating}
/>
</DetailSection>
</div>
</>
);
}