feat(web): 站点按类型分组、列表模式修样式、下掉自定义 RSS 源与索引器入口;智能体会话分对话 / 自动巡检两栏
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8da8679a04
commit
8eb8a68295
11 files changed
+364
-312
No files matched your search
@@ -6,25 +6,27 @@ import { useSites } from "~/api/sites.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { FilterSelect } from "~/components/console/filter-bar.tsx";
|
||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||
import { useListPagination } from "~/components/console/use-list-pagination.ts";
|
||||
import { SearchDefaultDialog } from "~/components/sites/search-default-dialog.tsx";
|
||||
import { SiteAddDialog } from "~/components/sites/site-add-dialog.tsx";
|
||||
import { SiteCard, SiteTable } from "~/components/sites/site-card.tsx";
|
||||
import { type SiteDialog, SiteDialogHost, useSiteChecks } from "~/components/sites/site-dialogs.tsx";
|
||||
import { needsAttention, type SortKey, sortSites } from "~/components/sites/site-fields.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
// 获取 → 站点:站点、凭证、健康、账号数据、RSS 的唯一入口(docs/design/site-center.md §4);
|
||||
// IA-2 起站点有类型(PT 站 / 公开 BT 站 / 自定义 RSS 源),列表可按类型筛选,?new=<类型> 直接打开添加。
|
||||
// 按类型分组显示: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 }));
|
||||
/** 分组顺序;公开 BT 站的表只列它用得到的列 */
|
||||
const GROUPS: { kind: SiteKind; title: string; variant: "pt" | "public" }[] = [
|
||||
{ kind: "pt", title: "PT 站", variant: "pt" },
|
||||
{ kind: "public_bt", title: "公开 BT 站", variant: "public" },
|
||||
{ kind: "rss_feed", title: "RSS 源", variant: "public" },
|
||||
{ kind: "torznab", title: "索引器", variant: "public" },
|
||||
];
|
||||
|
||||
const FILTERS: { value: Filter; label: string }[] = [
|
||||
{ value: "all", label: "全部" },
|
||||
@@ -66,7 +68,6 @@ 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(
|
||||
@@ -109,15 +110,12 @@ 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) &&
|
||||
(!kindFilter || s.kind === kindFilter),
|
||||
),
|
||||
all.filter((s) => (filter === "enabled" ? s.enabled : filter === "attention" ? needsAttention(s) : true)),
|
||||
sort,
|
||||
);
|
||||
|
||||
const paged = useListPagination(shown, { resetKey: JSON.stringify({ filter, kindFilter, sort }) });
|
||||
const groups = GROUPS.map((g) => ({ ...g, sites: shown.filter((s) => s.kind === g.kind) })).filter(
|
||||
(g) => g.sites.length,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -153,14 +151,6 @@ 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}
|
||||
@@ -191,7 +181,7 @@ export default function SitesPage() {
|
||||
<EmptyState
|
||||
icon={Globe}
|
||||
title="还没有站点"
|
||||
description="添加 PT 站、公开 BT 站或 RSS 源后,这里显示账号数据、健康和 RSS 状态"
|
||||
description="添加 PT 站或公开 BT 站后,这里显示账号数据、健康和资源更新"
|
||||
action={
|
||||
<Button size="sm" onClick={() => setAdding("any")}>
|
||||
<Plus />
|
||||
@@ -202,23 +192,31 @@ export default function SitesPage() {
|
||||
) : !shown.length ? (
|
||||
<EmptyState
|
||||
icon={Globe}
|
||||
title={kindFilter ? "没有这类站点" : filter === "attention" ? "没有需处理的站点" : "没有已启用的站点"}
|
||||
description={kindFilter ? "清除类型筛选查看其余站点" : "切换到「全部」查看其余站点"}
|
||||
title={filter === "attention" ? "没有需处理的站点" : "没有已启用的站点"}
|
||||
description="切换到「全部」查看其余站点"
|
||||
/>
|
||||
) : view === "list" ? (
|
||||
<SiteTable sites={shown} checks={checks} onDialog={setDialog} />
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{paged.items?.map((s) => (
|
||||
<SiteCard key={s.id} site={s} checks={checks} onDialog={setDialog} />
|
||||
<div className="space-y-6">
|
||||
{groups.map((g) => (
|
||||
<section key={g.kind} aria-label={g.title} className="min-w-0">
|
||||
<h2 className="mb-2 flex items-baseline gap-2 text-caption font-semibold text-foreground">
|
||||
{g.title}
|
||||
<span className="text-meta font-normal text-muted-foreground">{g.sites.length}</span>
|
||||
</h2>
|
||||
{view === "list" ? (
|
||||
<SiteTable sites={g.sites} checks={checks} onDialog={setDialog} variant={g.variant} />
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{g.sites.map((s) => (
|
||||
<SiteCard key={s.id} site={s} checks={checks} onDialog={setDialog} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view !== "list" && shown.length > 0 ? (
|
||||
<Pagination {...paged.pagination} className="mt-2 border-t-0 px-0" />
|
||||
) : 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