feat(web): nav regroup, site RSS tab and site types (IA-2)
- sidebar: 概览 / 获取 / 整理 / 系统. Tasks moves to 系统; sites, subscriptions and rules form the 获取 group (ACQUIRE_GROUP_TITLE in nav.ts, the single place to rename it); standalone RSS nav item removed - site detail gets an RSS tab with everything the RSS page had (create, edit, enable, fetch now, fetch state) plus recent items; tabs and header actions follow the site's capabilities - old /console/sources links redirect to the site's RSS tab (?feed= looks up the owning site) or the site list; task links to RSS point at sites - site list shows and filters by type; add-site wizard starts with a type step (PT still picks from the definition library; public BT / custom RSS source only need name, URL and RSS URL) Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ec65435328
commit
614262378e
20 files changed
+772
-304
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { SITE_KINDS, type SiteKind } from "@umt/contracts";
|
||||
import { Globe, LayoutGrid, List, PackageSearch, Plus, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
@@ -13,11 +14,15 @@ import { needsAttention, type SortKey, sortSites } from "~/components/sites/site
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
// 资源 → 站点:站点、凭证、健康、账号数据的唯一入口(docs/design/site-center.md §4)
|
||||
// 获取 → 站点:站点、凭证、健康、账号数据、RSS 的唯一入口(docs/design/site-center.md §4);
|
||||
// IA-2 起站点有类型(PT 站 / 公开 BT 站 / 自定义 RSS 源),列表可按类型筛选,?new=<类型> 直接打开添加。
|
||||
|
||||
const VIEW_KEY = "umt.sites.view";
|
||||
type View = "cards" | "list";
|
||||
type Filter = "all" | "enabled" | "attention";
|
||||
const KIND_OPTIONS = (Object.keys(SITE_KINDS) as SiteKind[])
|
||||
.filter((k) => SITE_KINDS[k].implemented)
|
||||
.map((k) => ({ value: k, label: SITE_KINDS[k].label }));
|
||||
|
||||
const FILTERS: { value: Filter; label: string }[] = [
|
||||
{ value: "all", label: "全部" },
|
||||
@@ -45,7 +50,8 @@ export const meta = () => [{ title: "站点 · UHD 媒体自动化控制台" }];
|
||||
export default function SitesPage() {
|
||||
const sites = useSites();
|
||||
const [params, setParams] = useSearchParams();
|
||||
const [adding, setAdding] = useState(false);
|
||||
const newParam = params.get("new");
|
||||
const [adding, setAdding] = useState<SiteKind | "any" | null>(null);
|
||||
const [dialog, setDialog] = useState<SiteDialog | null>(null);
|
||||
const checks = useSiteChecks(setDialog);
|
||||
|
||||
@@ -58,6 +64,7 @@ export default function SitesPage() {
|
||||
const view: View = urlView === "list" || urlView === "cards" ? urlView : storedView;
|
||||
const filter = (FILTERS.find((f) => f.value === params.get("filter"))?.value ?? "all") as Filter;
|
||||
const sort = (SORTS.find((s) => s.value === params.get("sort"))?.value ?? "name") as SortKey;
|
||||
const kindFilter = KIND_OPTIONS.find((k) => k.value === params.get("kind"))?.value ?? "";
|
||||
|
||||
const setParam = (k: string, v: string | null) =>
|
||||
setParams(
|
||||
@@ -78,6 +85,14 @@ export default function SitesPage() {
|
||||
setParam("view", v);
|
||||
};
|
||||
|
||||
// 命令面板 / 旧 RSS 页的 ?new=1、?new=rss_feed:打开添加(带类型时跳过选类型)
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: 只在参数出现时处理一次
|
||||
useEffect(() => {
|
||||
if (!newParam) return;
|
||||
setAdding(newParam in SITE_KINDS ? (newParam as SiteKind) : "any");
|
||||
setParam("new", null);
|
||||
}, [newParam]);
|
||||
|
||||
// 待处理"改用 Cookie"深链:?credentials=<siteId>
|
||||
const credFor = params.get("credentials");
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: 只在数据到达时处理一次
|
||||
@@ -92,7 +107,11 @@ export default function SitesPage() {
|
||||
const [searchDefaults, setSearchDefaults] = useState(false);
|
||||
const attention = all.filter(needsAttention).length;
|
||||
const shown = sortSites(
|
||||
all.filter((s) => (filter === "enabled" ? s.enabled : filter === "attention" ? needsAttention(s) : true)),
|
||||
all.filter(
|
||||
(s) =>
|
||||
(filter === "enabled" ? s.enabled : filter === "attention" ? needsAttention(s) : true) &&
|
||||
(!kindFilter || s.kind === kindFilter),
|
||||
),
|
||||
sort,
|
||||
);
|
||||
|
||||
@@ -110,7 +129,7 @@ export default function SitesPage() {
|
||||
<PackageSearch />
|
||||
搜索站点
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => setAdding(true)}>
|
||||
<Button size="sm" onClick={() => setAdding("any")}>
|
||||
<Plus />
|
||||
添加站点
|
||||
</Button>
|
||||
@@ -130,6 +149,14 @@ export default function SitesPage() {
|
||||
label: f.value === "attention" && attention ? `${f.label} ${attention}` : f.label,
|
||||
}))}
|
||||
/>
|
||||
{new Set(all.map((s) => s.kind)).size > 1 || kindFilter ? (
|
||||
<FilterSelect
|
||||
label="类型"
|
||||
value={kindFilter}
|
||||
onChange={(v) => setParam("kind", v || null)}
|
||||
options={KIND_OPTIONS}
|
||||
/>
|
||||
) : null}
|
||||
<FilterSelect
|
||||
label="排序"
|
||||
value={sort === "name" ? "" : sort}
|
||||
@@ -160,9 +187,9 @@ export default function SitesPage() {
|
||||
<EmptyState
|
||||
icon={Globe}
|
||||
title="还没有站点"
|
||||
description="添加站点并填写凭证后,这里显示账号数据和健康状态"
|
||||
description="添加 PT 站、公开 BT 站或 RSS 源后,这里显示账号数据、健康和 RSS 状态"
|
||||
action={
|
||||
<Button size="sm" onClick={() => setAdding(true)}>
|
||||
<Button size="sm" onClick={() => setAdding("any")}>
|
||||
<Plus />
|
||||
添加站点
|
||||
</Button>
|
||||
@@ -171,8 +198,8 @@ export default function SitesPage() {
|
||||
) : !shown.length ? (
|
||||
<EmptyState
|
||||
icon={Globe}
|
||||
title={filter === "attention" ? "没有需处理的站点" : "没有已启用的站点"}
|
||||
description="切换到「全部」查看其余站点"
|
||||
title={kindFilter ? "没有这类站点" : filter === "attention" ? "没有需处理的站点" : "没有已启用的站点"}
|
||||
description={kindFilter ? "清除类型筛选查看其余站点" : "切换到「全部」查看其余站点"}
|
||||
/>
|
||||
) : view === "list" ? (
|
||||
<SiteTable sites={shown} checks={checks} onDialog={setDialog} />
|
||||
@@ -184,7 +211,7 @@ export default function SitesPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{adding ? <SiteAddDialog onClose={() => setAdding(false)} /> : null}
|
||||
{adding ? <SiteAddDialog initialKind={adding === "any" ? null : adding} onClose={() => setAdding(null)} /> : null}
|
||||
<SiteDialogHost dialog={dialog} onClose={() => setDialog(null)} />
|
||||
<SearchDefaultDialog open={searchDefaults} onOpenChange={setSearchDefaults} sites={all} />
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user