refactor(web): 下载盘、RSS、订阅、设置与待处理的剩余去噪项(DEAI-2)
- 下载面板:下载盘只显示「已用 x / 总量」与进度条,暂停线 / 在途 / 预留 / 可清理 / 做种保留收进详情; 阈值文案统一「暂停线 / 恢复线 / 下载盘满暂停」 - RSS 页去掉与站点页重复的站点表,节标题「RSS 地址」;用途只在非通用时显示 - 订阅:一条都没有时不铺筛选;表单里「空 = 24;0 = 关」等等式改成 placeholder + 一句说明 - 设置:浏览器服务、元数据识别、MCP 说明去掉组件名与实现细节;会话目录与 fake 脚本只在开发构建显示; API Key scope 前加中文名;命名模板变量去掉「MP 无」 - 待处理顶部计数卡改成一行筛选按钮 - 智能体悬浮按钮只在没有侧栏的窄屏出现,抽屉打开时不再显示「收起」胶囊 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
57ce40168d
commit
2c8c33c31c
15 files changed
+185
-237
No files matched your search
@@ -12,7 +12,6 @@ import { FormField } from "~/components/console/form-field.tsx";
|
||||
import { JsonView } from "~/components/console/json-view.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||
import { StatTile } from "~/components/console/stat-tile.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { TechInfo } from "~/components/console/tech-info.tsx";
|
||||
import { EntityPicker, type PickedEntity } from "~/components/library/identity-entity-picker.tsx";
|
||||
@@ -520,25 +519,24 @@ export default function InboxPage() {
|
||||
{summary.isError ? (
|
||||
<ErrorState className="mb-4" title="汇总加载失败" error={summary.error} onRetry={() => summary.refetch()} />
|
||||
) : (
|
||||
<div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6">
|
||||
<StatTile
|
||||
label={isOpenView ? "未关闭" : (DECISION_STATUS[status as DecisionStatus]?.label ?? "全部")}
|
||||
value={summary.isPending ? "…" : (summary.data?.total ?? 0)}
|
||||
hint={isOpenView ? "含调查中、等你决定" : undefined}
|
||||
icon={Inbox}
|
||||
active={!kind}
|
||||
onClick={() => set("kind", "")}
|
||||
/>
|
||||
<div className="mb-4 flex flex-wrap items-center gap-1.5" role="group" aria-label="按问题类型筛选">
|
||||
<Button variant={kind ? "ghost" : "secondary"} size="sm" onClick={() => set("kind", "")} aria-pressed={!kind}>
|
||||
<Inbox />
|
||||
{isOpenView ? "全部" : (DECISION_STATUS[status as DecisionStatus]?.label ?? "全部")}
|
||||
<span className="tabular-nums">{summary.isPending ? "…" : (summary.data?.total ?? 0)}</span>
|
||||
</Button>
|
||||
{byKind.map(([k, v]) => (
|
||||
<StatTile
|
||||
<Button
|
||||
key={k}
|
||||
label={DECISION_KIND[k]}
|
||||
value={v.count}
|
||||
hint={`最高级别:${SEVERITY[v.sev].label}`}
|
||||
tone={SEVERITY[v.sev].tone}
|
||||
active={kind === k}
|
||||
variant={kind === k ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
aria-pressed={kind === k}
|
||||
onClick={() => set("kind", kind === k ? "" : k)}
|
||||
/>
|
||||
className={v.sev === "critical" ? "text-destructive" : undefined}
|
||||
>
|
||||
{DECISION_KIND[k]}
|
||||
<span className="tabular-nums">{v.count}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import type { FeedHealth, Site } from "@umt/contracts";
|
||||
import { Globe, Plus, RefreshCw } from "lucide-react";
|
||||
import type { FeedHealth } from "@umt/contracts";
|
||||
import { 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 { useSites } from "~/api/sites.ts";
|
||||
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、账号在站点详情。
|
||||
// RSS:跨站点的 RSS 列表;站点健康只在站点页(DEAI-2 去掉重复的站点表)。
|
||||
|
||||
export const meta = () => [{ title: "RSS · UHD 媒体自动化控制台" }];
|
||||
|
||||
@@ -27,59 +22,11 @@ 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
|
||||
@@ -92,22 +39,7 @@ export default function SourcesPage() {
|
||||
}
|
||||
/>
|
||||
<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="RSS">
|
||||
<DetailSection title="RSS 地址">
|
||||
<FilterBar
|
||||
onReset={f.active ? f.reset : undefined}
|
||||
actions={
|
||||
|
||||
@@ -275,63 +275,68 @@ export default function SubscriptionsPage() {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<Pills
|
||||
label="分组"
|
||||
value={v.group as SubscriptionGroup | ""}
|
||||
onChange={(x) => f.set("group", x)}
|
||||
items={[
|
||||
{ value: "", label: "全部", count: allCount },
|
||||
...GROUP_ORDER.map((g) => ({ value: g, label: GROUP_LABEL[g], count: counts?.[g] })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<FilterBar
|
||||
search={{ value: q, onChange: setQ, placeholder: "作品标题 / 别名 / 检索词" }}
|
||||
onReset={
|
||||
filtered
|
||||
? () => {
|
||||
setQ("");
|
||||
for (const k of ["q", "site", "rg", "origin", "up"] as const) f.set(k, "");
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<FilterSelect
|
||||
label="站点"
|
||||
value={v.site}
|
||||
onChange={(x) => f.set("site", x)}
|
||||
options={(sites.data?.items ?? []).map((s) => ({ value: s.id, label: s.name }))}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="规则组"
|
||||
value={v.rg}
|
||||
onChange={(x) => f.set("rg", x)}
|
||||
options={(groups.data?.items ?? []).map((g) => ({ value: g.id, label: g.name }))}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="来源"
|
||||
value={v.origin}
|
||||
onChange={(x) => f.set("origin", x)}
|
||||
options={opts(SUB_ORIGIN)}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="洗版"
|
||||
value={v.up}
|
||||
onChange={(x) => f.set("up", x)}
|
||||
options={[
|
||||
{ value: "true", label: "洗版中" },
|
||||
{ value: "false", label: "未洗版" },
|
||||
]}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="排序"
|
||||
value={v.sort}
|
||||
onChange={(x) => f.set("sort", x)}
|
||||
allLabel="按分组"
|
||||
options={opts(SORT_LABEL)}
|
||||
/>
|
||||
</FilterBar>
|
||||
{/* 一条订阅都没有时只留空态和「新建订阅」,不铺筛选 */}
|
||||
{allCount === 0 && !filtered && !v.group && !v.kind ? null : (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<Pills
|
||||
label="分组"
|
||||
value={v.group as SubscriptionGroup | ""}
|
||||
onChange={(x) => f.set("group", x)}
|
||||
items={[
|
||||
{ value: "", label: "全部", count: allCount },
|
||||
...GROUP_ORDER.map((g) => ({ value: g, label: GROUP_LABEL[g], count: counts?.[g] })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<FilterBar
|
||||
search={{ value: q, onChange: setQ, placeholder: "作品标题 / 别名 / 检索词" }}
|
||||
onReset={
|
||||
filtered
|
||||
? () => {
|
||||
setQ("");
|
||||
for (const k of ["q", "site", "rg", "origin", "up"] as const) f.set(k, "");
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<FilterSelect
|
||||
label="站点"
|
||||
value={v.site}
|
||||
onChange={(x) => f.set("site", x)}
|
||||
options={(sites.data?.items ?? []).map((s) => ({ value: s.id, label: s.name }))}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="规则组"
|
||||
value={v.rg}
|
||||
onChange={(x) => f.set("rg", x)}
|
||||
options={(groups.data?.items ?? []).map((g) => ({ value: g.id, label: g.name }))}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="来源"
|
||||
value={v.origin}
|
||||
onChange={(x) => f.set("origin", x)}
|
||||
options={opts(SUB_ORIGIN)}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="洗版"
|
||||
value={v.up}
|
||||
onChange={(x) => f.set("up", x)}
|
||||
options={[
|
||||
{ value: "true", label: "洗版中" },
|
||||
{ value: "false", label: "未洗版" },
|
||||
]}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="排序"
|
||||
value={v.sort}
|
||||
onChange={(x) => f.set("sort", x)}
|
||||
allLabel="按分组"
|
||||
options={opts(SORT_LABEL)}
|
||||
/>
|
||||
</FilterBar>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selecting ? (
|
||||
<div className="pg-slab sticky top-2 z-20 mb-3 flex flex-wrap items-center gap-2 rounded-xl px-3 py-2">
|
||||
@@ -393,7 +398,7 @@ export default function SubscriptionsPage() {
|
||||
icon={CalendarClock}
|
||||
title={filtered || v.group || v.kind ? "没有符合条件的订阅" : "还没有订阅"}
|
||||
description={
|
||||
filtered || v.group || v.kind ? undefined : "从媒体库、搜索或智能体添加,也可以点「新建订阅」。"
|
||||
filtered || v.group || v.kind ? undefined : "在搜索或媒体库里点「订阅」,或点右上角「新建订阅」。"
|
||||
}
|
||||
/>
|
||||
) : grouped ? (
|
||||
|
||||
Reference in new issue
Block a user