文案(下载):重写下载面板、后台任务、下载盘与手动下载文案

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-07 01:48:14 +08:00
1 parent 5aa19a9a9b
commit 3853a65754
7 files changed
+30 -30

No files matched your search

@@ -24,15 +24,15 @@ import { formatDateTime } from "~/lib/utils.ts";
const TaskPage = cursorPage(Task); const TaskPage = cursorPage(Task);
export const TASK_KIND_LABEL: Record<TaskKind, string> = { export const TASK_KIND_LABEL: Record<TaskKind, string> = {
ingestFeed: "抓取 Feed", ingestFeed: "抓取 RSS",
resolveObservation: "识别资源", resolveObservation: "识别资源",
evaluateAcquisition: "评估候选", evaluateAcquisition: "评估资源",
submitDownload: "提交下载", submitDownload: "提交下载",
verifyAndImport: "核验整理", verifyAndImport: "核验整理",
refreshMetadata: "刷新元数据", refreshMetadata: "刷新元数据",
reconcileLibrary: "文件核对", reconcileLibrary: "文件核对",
cleanupDownloadCache: "清理缓存", cleanupDownloadCache: "清理缓存",
resolveDecision: "处理待处理事项", resolveDecision: "处理事项",
agentRun: "智能体运行", agentRun: "智能体运行",
}; };
@@ -40,13 +40,13 @@ export const TASK_STATUS: Record<
TaskStatus, TaskStatus,
{ label: string; tone: "success" | "warning" | "destructive" | "neutral" } { label: string; tone: "success" | "warning" | "destructive" | "neutral" }
> = { > = {
pending: { label: "排队", tone: "neutral" }, pending: { label: "排队中", tone: "neutral" },
running: { label: "运行中", tone: "neutral" }, running: { label: "运行中", tone: "neutral" },
waiting: { label: "等待", tone: "warning" }, waiting: { label: "等待", tone: "warning" },
succeeded: { label: "成功", tone: "success" }, succeeded: { label: "成功", tone: "success" },
failed: { label: "失败", tone: "destructive" }, failed: { label: "失败", tone: "destructive" },
cancelled: { label: "已取消", tone: "neutral" }, cancelled: { label: "已取消", tone: "neutral" },
dead: { label: "放弃", tone: "destructive" }, dead: { label: "已放弃", tone: "destructive" },
}; };
export const ACTIVE_TASK: TaskStatus[] = ["pending", "running", "waiting"]; export const ACTIVE_TASK: TaskStatus[] = ["pending", "running", "waiting"];
@@ -245,8 +245,8 @@ export function BusinessTasks() {
loadingMore={list.isFetchingNextPage} loadingMore={list.isFetchingNextPage}
empty={{ empty={{
icon: ListTodo, icon: ListTodo,
title: f.active ? "没有符合筛选的任务" : "还没有业务任务", title: f.active ? "没有符合筛选的任务" : "还没有后台任务",
description: f.active ? undefined : "提交下载、核验整理、清理缓存、对账等后台任务会出现在这里。", description: f.active ? undefined : "提交下载、核验整理、清理缓存、文件核对等任务会出现在这里。",
}} }}
/> />
<DetailSheet <DetailSheet
@@ -259,7 +259,7 @@ export function BusinessTasks() {
<> <>
<TaskActions t={open} /> <TaskActions t={open} />
<Button asChild size="sm"> <Button asChild size="sm">
<Link to={consolePath("tasks", open.id)}>在独立页面打开</Link> <Link to={consolePath("tasks", open.id)}>打开详情页</Link>
</Button> </Button>
</> </>
) : null ) : null
@@ -36,7 +36,7 @@ export function BusinessTimeline({ events, loading }: { events: TimelineEvent[];
})} })}
</div> </div>
{ongoing.length ? ( {ongoing.length ? (
<p className="text-caption text-muted-foreground">当前:{ongoing.map((e) => e.title).join(";")}</p> <p className="text-caption text-muted-foreground">当前:{ongoing.map((e) => e.title).join(";")}</p>
) : null} ) : null}
{loading && !events.length ? ( {loading && !events.length ? (
<LoadingState rows={3} /> <LoadingState rows={3} />
@@ -60,7 +60,7 @@ export function BusinessTimeline({ events, loading }: { events: TimelineEvent[];
/> />
)} )}
{loading && events.length ? ( {loading && events.length ? (
<p className="text-meta text-muted-foreground">正在加载关联的整理记录与引擎运行…</p> <p className="text-meta text-muted-foreground">加载整理记录和引擎运行…</p>
) : null} ) : null}
</div> </div>
); );
@@ -81,9 +81,9 @@ function VolumeBar({ v }: { v: Volume }) {
/> />
<div className="mt-1.5 flex flex-wrap gap-1.5"> <div className="mt-1.5 flex flex-wrap gap-1.5">
{pct === null ? <StatusBadge tone="warning">读不到磁盘状态,已暂停新提交</StatusBadge> : null} {pct === null ? <StatusBadge tone="warning">读不到磁盘状态,已暂停新提交</StatusBadge> : null}
{tripped ? <StatusBadge tone="destructive">超过暂停线,停止新下载</StatusBadge> : null} {tripped ? <StatusBadge tone="destructive">超过暂停线,已停止新下载</StatusBadge> : null}
{v.pausedByThreshold ? ( {v.pausedByThreshold ? (
<StatusBadge tone="warning">因下载盘暂停 {v.pausedByThreshold} 个任务</StatusBadge> <StatusBadge tone="warning">超过暂停线,暂停了 {v.pausedByThreshold} 个任务</StatusBadge>
) : null} ) : null}
</div> </div>
{v.notes.map((n) => ( {v.notes.map((n) => (
@@ -119,7 +119,7 @@ export function CacheVolumes() {
</Button> </Button>
} }
title="按规则清理下载缓存?" title="按规则清理下载缓存?"
description="只清理已整理、且满足保留期与 H&R 的本工具任务,其余不动。" description="只清理本工具管理、已整理且满足保留期和 H&R 要求的任务,其余不动。"
confirmLabel="清理" confirmLabel="清理"
onConfirm={() => onConfirm={() =>
cleanup.mutateAsync(undefined).then( cleanup.mutateAsync(undefined).then(
@@ -138,7 +138,7 @@ export function CacheVolumes() {
) : vols.error ? ( ) : vols.error ? (
<ErrorState plain error={vols.error} onRetry={() => void vols.refetch()} /> <ErrorState plain error={vols.error} onRetry={() => void vols.refetch()} />
) : !vols.data.length ? ( ) : !vols.data.length ? (
<p className="text-caption text-muted-foreground">还没有缓存卷数据。登记下载器并完成巡检后显示。</p> <p className="text-caption text-muted-foreground">还没有下载盘。添加下载器后显示。</p>
) : ( ) : (
<div className="grid gap-5 md:grid-cols-2"> <div className="grid gap-5 md:grid-cols-2">
{vols.data.map((v) => ( {vols.data.map((v) => (
@@ -333,11 +333,11 @@ export const BOARD_EMPTY: Record<DownloadBoardState, { title: string; descriptio
active: { active: {
icon: Download, icon: Download,
title: "没有进行中的下载", title: "没有进行中的下载",
description: "自动收录或在站点页下载后出现在这里,等批准、等磁盘空间的也在这里;下载完成的在「已完成」。", description: "自动收录和手动下载的任务都在这里,包括等批准、等磁盘空间的。下载完成后移到「已完成」。",
}, },
completed: { completed: {
icon: CheckCircle2, icon: CheckCircle2,
title: "还没有下载完成的任务", title: "还没有下载完成的任务",
description: "下载完成(含做种)后出现在这里。", description: "下载完成的任务在这里,包括做种中的。",
}, },
}; };
@@ -49,7 +49,7 @@ export function DownloadStatusBadges({ t }: { t: DownloadTask }) {
<span className="flex flex-wrap gap-1"> <span className="flex flex-wrap gap-1">
<StatusBadge tone={DOWNLOAD_STATUS_TONE[t.status]}>{DOWNLOAD_STATUS_LABEL[t.status]}</StatusBadge> <StatusBadge tone={DOWNLOAD_STATUS_TONE[t.status]}>{DOWNLOAD_STATUS_LABEL[t.status]}</StatusBadge>
{t.pauseReason === "threshold" ? ( {t.pauseReason === "threshold" ? (
<StatusBadge tone="warning">下载盘满,已暂停</StatusBadge> <StatusBadge tone="warning">超过暂停线,已暂停</StatusBadge>
) : t.pauseReason ? ( ) : t.pauseReason ? (
<StatusBadge>{PAUSE_REASON_LABEL[t.pauseReason]}</StatusBadge> <StatusBadge>{PAUSE_REASON_LABEL[t.pauseReason]}</StatusBadge>
) : null} ) : null}
@@ -151,7 +151,7 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
) : null} ) : null}
{task.noProgressSince ? ( {task.noProgressSince ? (
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning"> <p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
自 {formatDateTime(task.noProgressSince)} 起无进展,不会自动换源;开了自动调查时智能体会在授权内尝试恢复。 {formatDateTime(task.noProgressSince)} 起没有进展,不会自动换源。开启自动调查时,智能体会在持续授权范围内尝试恢复。
</p> </p>
) : null} ) : null}
@@ -85,7 +85,7 @@ describe("保存目录", () => {
expect(custom).not.toHaveProperty("downloaderInstanceId"); expect(custom).not.toHaveProperty("downloaderInstanceId");
}); });
it("界面:并排两个下拉,下方写明将保存到哪;自定义时展开输入框", () => { it("界面:并排两个下拉,下方写明保存到哪;自定义时展开输入框", () => {
const props = { const props = {
formId: "f", formId: "f",
instances: paths.instances, instances: paths.instances,
@@ -101,10 +101,10 @@ describe("保存目录", () => {
const html = render(createElement(SaveTargetFields, props)); const html = render(createElement(SaveTargetFields, props));
expect(html).toContain("下载器"); expect(html).toContain("下载器");
expect(html).toContain("自动:qb-main"); expect(html).toContain("自动:qb-main");
expect(html).toContain("将保存到:/data/downloads(本机 /mnt/dl)"); expect(html).toContain("保存到:/data/downloads(本机 /mnt/dl)");
expect(html).not.toContain("自定义路径</label>"); expect(html).not.toContain("自定义路径</label>");
const custom = render(createElement(SaveTargetFields, { ...props, dir: CUSTOM_DIR, custom: "/x" })); const custom = render(createElement(SaveTargetFields, { ...props, dir: CUSTOM_DIR, custom: "/x" }));
expect(custom).toContain("将保存到:/x"); expect(custom).toContain("保存到:/x");
expect(custom).toContain('id="f-custom"'); expect(custom).toContain('id="f-custom"');
const none = render( const none = render(
createElement(SaveTargetFields, { ...props, instances: [], autoInstanceId: null, target: null }), createElement(SaveTargetFields, { ...props, instances: [], autoInstanceId: null, target: null }),
@@ -136,7 +136,7 @@ describe("作品身份", () => {
expect(html).toContain("未识别"); expect(html).toContain("未识别");
expect(html).toContain("TMDB 编号"); expect(html).toContain("TMDB 编号");
expect(html).toContain('id="f-tmdb"'); expect(html).toContain('id="f-tmdb"');
expect(html).toContain("暂未识别到作品"); expect(html).toContain("没识别出作品");
}); });
it("已识别:折叠只显示当前作品", () => { it("已识别:折叠只显示当前作品", () => {
@@ -282,7 +282,7 @@ export function ManualDownloadDialog({
}} }}
> >
<Bot /> <Bot />
委托智能体 交给智能体
</Button> </Button>
) : null} ) : null}
<Button <Button
@@ -329,7 +329,7 @@ export function ResourceSummary({ resource: r }: { resource: ManualDownloadResou
{r.leechers ?? "—"} {r.leechers ?? "—"}
</span> </span>
) : null} ) : null}
{r.publishedAt ? <span title={absTime(r.publishedAt)}>发布 {relTime(r.publishedAt)}</span> : null} {r.publishedAt ? <span title={absTime(r.publishedAt)}>发布于 {relTime(r.publishedAt)}</span> : null}
</div> </div>
</div> </div>
); );
@@ -372,7 +372,7 @@ export function SaveTargetFields({
: !target && dir !== CUSTOM_DIR : !target && dir !== CUSTOM_DIR
? "没有可用的下载器,先在设置里添加下载器和保存目录" ? "没有可用的下载器,先在设置里添加下载器和保存目录"
: resolved : resolved
? `将保存到:${savePathText(resolved)}` ? `保存到:${savePathText(resolved)}`
: "填写下载器能访问的路径"; : "填写下载器能访问的路径";
return ( return (
<div className="grid items-start gap-x-3 sm:grid-cols-2"> <div className="grid items-start gap-x-3 sm:grid-cols-2">
@@ -504,7 +504,7 @@ export function IdentitySection({
<div id={`${formId}-identity`} className="space-y-3 border-t border-border px-3 pt-3 pb-1"> <div id={`${formId}-identity`} className="space-y-3 border-t border-border px-3 pt-3 pb-1">
{unrecognized && !changed ? ( {unrecognized && !changed ? (
<p className="text-caption text-warning"> <p className="text-caption text-warning">
暂未识别到作品。指定后绑定到这个资源;不指定也能下载,整理时再补全识别和元数据。 没识别出作品。指定后绑定到这个资源;不指定也能下载,整理时再识别。
</p> </p>
) : null} ) : null}
{picked ? ( {picked ? (
@@ -533,7 +533,7 @@ export function IdentitySection({
<FormField <FormField
label="TMDB 编号" label="TMDB 编号"
htmlFor={`${formId}-tmdb`} htmlFor={`${formId}-tmdb`}
error={tmdbId && !valid ? "TMDB 编号是数字" : preview.error ? errorMessage(preview.error) : null} error={tmdbId && !valid ? "TMDB 编号只能是数字" : preview.error ? errorMessage(preview.error) : null}
hint={ hint={
preview.data preview.data
? `${preview.data.title}${preview.data.year ? ` (${preview.data.year})` : ""}${preview.data.localEntityId ? " · 在媒体库" : ""}` ? `${preview.data.title}${preview.data.year ? ` (${preview.data.year})` : ""}${preview.data.localEntityId ? " · 在媒体库" : ""}`
@@ -602,8 +602,8 @@ export function ExistingVersionsSection({
selected: string[]; selected: string[];
onSelectedChange: (ids: string[]) => void; onSelectedChange: (ids: string[]) => void;
}) { }) {
if (loading) return <p className="text-caption text-muted-foreground">正在读取已有版本…</p>; if (loading) return <p className="text-caption text-muted-foreground">读取已有版本…</p>;
if (error) return <p className="text-caption text-destructive">已有版本读取失败,重试后再选洗版</p>; if (error) return <p className="text-caption text-destructive">读取已有版本失败,重新打开后再选洗版</p>;
if (!versions.length) return null; if (!versions.length) return null;
return ( return (
<div className="min-w-0 space-y-2"> <div className="min-w-0 space-y-2">
@@ -640,7 +640,7 @@ export function ExistingVersionsSection({
))} ))}
</fieldset> </fieldset>
<p className="text-meta text-muted-foreground"> <p className="text-meta text-muted-foreground">
本次新文件整理成功后替换所选旧文件。剧集只替换成功对应的季集,其余保留。 新文件整理成功后替换所选旧文件。剧集只替换整理成功的季集,其余保留。
</p> </p>
</> </>
) : null} ) : null}