feat(web): downloads and organize records open the shared organize wizard (FE-4)
下载详情"整理"按内容路径(经下载器路径映射)换算成登记目录后打开向导;整理记录详情加"重新整理"; 整理记录页"手动整理"转到文件管理;删除旧的 downloads/organize-dialog。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c31df8b69a
commit
563098c77a
4 files changed
+25
-358
No files matched your search
@@ -17,13 +17,13 @@ import { DetailSection } from "~/components/console/detail-sheet.tsx";
|
|||||||
import { JsonView } from "~/components/console/json-view.tsx";
|
import { JsonView } from "~/components/console/json-view.tsx";
|
||||||
import { KvList } from "~/components/console/kv-list.tsx";
|
import { KvList } from "~/components/console/kv-list.tsx";
|
||||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||||
|
import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx";
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||||
import { errorMessage } from "~/lib/api.ts";
|
import { errorMessage } from "~/lib/api.ts";
|
||||||
import { planReasonText } from "~/lib/labels.ts";
|
import { planReasonText } from "~/lib/labels.ts";
|
||||||
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
|
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||||
import { BusinessTimeline } from "./business-timeline.tsx";
|
import { BusinessTimeline } from "./business-timeline.tsx";
|
||||||
import { OrganizeDialog } from "./organize-dialog.tsx";
|
|
||||||
|
|
||||||
const PLAN_PURPOSE_LABEL: Record<string, string> = { fill: "补缺", upgrade: "升级", replace_source: "换源" };
|
const PLAN_PURPOSE_LABEL: Record<string, string> = { fill: "补缺", upgrade: "升级", replace_source: "换源" };
|
||||||
|
|
||||||
@@ -197,7 +197,16 @@ export function DownloadDetail({ task }: { task: DownloadTask }) {
|
|||||||
{task.lastError ? <JsonView label="下载器错误" value={task.lastError} tone="destructive" defaultOpen /> : null}
|
{task.lastError ? <JsonView label="下载器错误" value={task.lastError} tone="destructive" defaultOpen /> : null}
|
||||||
|
|
||||||
{organizing ? (
|
{organizing ? (
|
||||||
<OrganizeDialog open onOpenChange={(o) => !o && setOrganizing(false)} downloadTaskId={task.id} />
|
<OrganizeWizard
|
||||||
|
source={{
|
||||||
|
kind: "download",
|
||||||
|
taskId: task.id,
|
||||||
|
instanceId: task.instanceId,
|
||||||
|
contentPath: task.contentPath,
|
||||||
|
name: task.name,
|
||||||
|
}}
|
||||||
|
onClose={() => setOrganizing(false)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,350 +0,0 @@
|
|||||||
import { OrganizeRequest } from "@umt/contracts";
|
|
||||||
import { Eye, Play } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { isMissingEndpoint } from "~/api/parse.ts";
|
|
||||||
import { OrganizePlanPreview, useArtifact, useEntitySearch, useLibraryRoots, useOrganize } from "~/api/records.ts";
|
|
||||||
import { LoadingState } from "~/components/console/empty-state.tsx";
|
|
||||||
import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx";
|
|
||||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "~/components/ui/dialog.tsx";
|
|
||||||
import { Input } from "~/components/ui/input.tsx";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
|
||||||
import { Textarea } from "~/components/ui/textarea.tsx";
|
|
||||||
import { errorMessage } from "~/lib/api.ts";
|
|
||||||
import { organizationActionLabel } from "~/lib/labels.ts";
|
|
||||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
|
||||||
|
|
||||||
const AUTO = "__auto__";
|
|
||||||
|
|
||||||
const FILE_ACTION: Record<string, string> = { transfer: "入库", skip: "跳过", replace: "替换", hold: "暂缓" };
|
|
||||||
const FILE_KIND: Record<string, string> = { video: "视频", subtitle: "字幕", audio: "音轨", nfo: "NFO", other: "其它" };
|
|
||||||
|
|
||||||
const EXCLUDE_REASON: Record<string, string> = {
|
|
||||||
sample: "样片",
|
|
||||||
too_small: "体积过小",
|
|
||||||
not_imported_kind: "不入库的文件类型",
|
|
||||||
orphan_sidecar: "孤立附属文件",
|
|
||||||
no_episode: "解析不出集号",
|
|
||||||
special_extra: "特典/花絮",
|
|
||||||
extra_content: "额外内容",
|
|
||||||
no_target_dir: "没有目标目录",
|
|
||||||
naming_failed: "命名失败",
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 预览里的计划能否执行:没生成整理计划或有阻塞冲突时不能执行(执行只会得到一条失败记录) */
|
|
||||||
function usePlanExecutable(planId: string | null) {
|
|
||||||
const art = useArtifact(planId);
|
|
||||||
const parsed = art.data ? OrganizePlanPreview.safeParse(art.data.content) : null;
|
|
||||||
const org = parsed?.success ? parsed.data.plan?.organize : undefined;
|
|
||||||
return !!org && !org.conflicts.some((c) => c.blocking);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PLAN_FAIL_TEXT: Record<string, string> = {
|
|
||||||
source_size_mismatch: "源文件大小与下载器记录不一致,可能还在写入;稍后重新预览",
|
|
||||||
source_missing: "源文件不存在,检查路径映射与挂载",
|
|
||||||
source_path_unknown: "找不到下载任务的保存路径或路径映射",
|
|
||||||
};
|
|
||||||
|
|
||||||
function PlanPreview({ planId }: { planId: string }) {
|
|
||||||
const art = useArtifact(planId);
|
|
||||||
if (art.isPending) return <LoadingState rows={3} />;
|
|
||||||
if (art.error) {
|
|
||||||
return (
|
|
||||||
<p className="rounded-lg border border-border px-3 py-2 text-caption text-muted-foreground">
|
|
||||||
计划已生成(planId {planId.slice(0, 8)}…),
|
|
||||||
{isMissingEndpoint(art.error) ? "计划已过期,重新预览。" : `读取计划失败:${errorMessage(art.error)}`}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const parsed = OrganizePlanPreview.safeParse(art.data.content);
|
|
||||||
if (!parsed.success) return <p className="text-caption text-muted-foreground">计划内容格式无法识别。</p>;
|
|
||||||
const p = parsed.data;
|
|
||||||
const org = p.plan?.organize;
|
|
||||||
if (!org)
|
|
||||||
return (
|
|
||||||
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
|
|
||||||
无法生成计划:{(p.reason && PLAN_FAIL_TEXT[p.reason]) ?? p.reason ?? p.status ?? "未知原因"}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
const blocking = org.conflicts.filter((c) => c.blocking);
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
<StatusBadge>{organizationActionLabel(org.operation)}</StatusBadge>
|
|
||||||
{org.crossDevice ? <StatusBadge tone="warning">跨卷:复制 → 核验 → 删源</StatusBadge> : null}
|
|
||||||
{org.requiresApproval ? <StatusBadge tone="warning">需要审批</StatusBadge> : null}
|
|
||||||
{blocking.length ? <StatusBadge tone="destructive">阻塞冲突 {blocking.length}</StatusBadge> : null}
|
|
||||||
</div>
|
|
||||||
<ul className="max-h-56 space-y-1.5 overflow-auto text-meta">
|
|
||||||
{org.files.map((f) => (
|
|
||||||
<li key={f.src} className="rounded-lg border border-border/60 px-2.5 py-1.5">
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
|
||||||
<StatusBadge tone={f.action === "transfer" ? "success" : f.action === "skip" ? "neutral" : "warning"}>
|
|
||||||
{FILE_ACTION[f.action] ?? f.action}
|
|
||||||
</StatusBadge>
|
|
||||||
<span className="text-muted-foreground">{FILE_KIND[f.kind] ?? f.kind}</span>
|
|
||||||
<span className="text-muted-foreground">{formatBytes(f.size, 1)}</span>
|
|
||||||
{f.skipReason ? <span className="text-muted-foreground">({f.skipReason})</span> : null}
|
|
||||||
</div>
|
|
||||||
<p className="mt-0.5 font-mono break-all text-muted-foreground">{f.src}</p>
|
|
||||||
<p className="font-mono break-all text-foreground">→ {f.dst}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{org.excluded.length ? (
|
|
||||||
<p className="text-meta text-muted-foreground">
|
|
||||||
排除 {org.excluded.length} 个:
|
|
||||||
{org.excluded
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((e) => `${e.src.split("/").pop()}(${EXCLUDE_REASON[e.reason] ?? e.reason})`)
|
|
||||||
.join(",")}
|
|
||||||
{org.excluded.length > 5 ? "…" : ""}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{org.conflicts.map((c) => (
|
|
||||||
<p
|
|
||||||
key={`${c.kind}${c.src ?? ""}`}
|
|
||||||
className={cn("text-meta", c.blocking ? "text-destructive" : "text-warning")}
|
|
||||||
>
|
|
||||||
{c.blocking ? "阻塞" : "提示"}:{c.kind}
|
|
||||||
{c.detail ? ` · ${c.detail}` : ""}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手动发起整理:先 dryRun 预览计划,再执行。自动与手动产生同格式整理记录。
|
|
||||||
*/
|
|
||||||
export function OrganizeDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
downloadTaskId,
|
|
||||||
defaultPaths,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (o: boolean) => void;
|
|
||||||
downloadTaskId?: string;
|
|
||||||
defaultPaths?: string[];
|
|
||||||
}) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const organize = useOrganize();
|
|
||||||
const roots = useLibraryRoots();
|
|
||||||
const [paths, setPaths] = useState((defaultPaths ?? []).join("\n"));
|
|
||||||
const [action, setAction] = useState<"copy" | "move">("copy");
|
|
||||||
const [root, setRoot] = useState("");
|
|
||||||
const [entityQ, setEntityQ] = useState("");
|
|
||||||
const [entity, setEntity] = useState<{ id: string; title: string } | null>(null);
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
||||||
const [planId, setPlanId] = useState<string | null>(null);
|
|
||||||
const search = useEntitySearch(entity ? "" : entityQ);
|
|
||||||
const executable = usePlanExecutable(planId);
|
|
||||||
|
|
||||||
const body = (dryRun: boolean) => {
|
|
||||||
const sourcePaths = paths
|
|
||||||
.split("\n")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
return OrganizeRequest.safeParse({
|
|
||||||
...(downloadTaskId ? { downloadTaskId } : { sourcePaths }),
|
|
||||||
...(entity ? { mediaEntityId: entity.id } : {}),
|
|
||||||
...(root ? { targetLibraryRootId: root } : {}),
|
|
||||||
action,
|
|
||||||
dryRun,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const run = (dryRun: boolean) => {
|
|
||||||
const parsed = body(dryRun);
|
|
||||||
if (!parsed.success) {
|
|
||||||
const e = zodFieldErrors(parsed.error.issues);
|
|
||||||
const pathErr = Object.entries(e).find(([k]) => k.startsWith("sourcePaths"));
|
|
||||||
setErrors({
|
|
||||||
...e,
|
|
||||||
paths: pathErr ? "每行一个以 / 开头的绝对路径" : e[""] ? "填写源路径" : "",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setErrors({});
|
|
||||||
organize.mutate(parsed.data, {
|
|
||||||
onSuccess: (r) => {
|
|
||||||
if (dryRun) {
|
|
||||||
setPlanId(r.planId);
|
|
||||||
if (r.truncated)
|
|
||||||
toast.warning("源目录文件过多,预览只含前一部分", {
|
|
||||||
description: "执行会被拒绝。把源路径缩小到具体的作品目录",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.success("已发起整理");
|
|
||||||
onOpenChange(false);
|
|
||||||
if (r.recordId) navigate(`/console/organize/${r.recordId}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (e) => toast.error(errorMessage(e)),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>手动整理</DialogTitle>
|
|
||||||
<DialogDescription>先预览计划(不动文件),确认后执行。跨卷移动按复制 → 核验 → 删源执行。</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{downloadTaskId ? (
|
|
||||||
<p className="mb-2 text-caption text-muted-foreground">
|
|
||||||
源:下载任务 {downloadTaskId.slice(0, 8)}… 的内容路径
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<FormField
|
|
||||||
label="源路径"
|
|
||||||
htmlFor="org-paths"
|
|
||||||
required
|
|
||||||
error={errors.paths}
|
|
||||||
hint="执行进程可访问的绝对路径,每行一个(文件或目录)"
|
|
||||||
>
|
|
||||||
<Textarea
|
|
||||||
id="org-paths"
|
|
||||||
rows={3}
|
|
||||||
className="font-mono text-caption"
|
|
||||||
value={paths}
|
|
||||||
aria-invalid={!!errors.paths}
|
|
||||||
onChange={(e) => {
|
|
||||||
setPaths(e.target.value);
|
|
||||||
setPlanId(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
)}
|
|
||||||
<FormField label="作品" htmlFor="org-entity" hint={entity ? undefined : "可选;不选时按文件名识别"}>
|
|
||||||
{entity ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<StatusBadge>{entity.title}</StatusBadge>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
|
||||||
setEntity(null);
|
|
||||||
setPlanId(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
更换
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="org-entity"
|
|
||||||
value={entityQ}
|
|
||||||
placeholder="搜索本地作品"
|
|
||||||
onChange={(e) => setEntityQ(e.target.value)}
|
|
||||||
/>
|
|
||||||
{search.data?.items.length ? (
|
|
||||||
<ul className="pg-float absolute inset-x-0 top-full z-10 mt-1 max-h-48 overflow-auto rounded-xl p-1">
|
|
||||||
{search.data.items.map((it) => (
|
|
||||||
<li key={it.entity.id}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full cursor-pointer rounded-lg px-2.5 py-1.5 text-left text-caption hover:bg-foreground/[0.06]"
|
|
||||||
onClick={() => {
|
|
||||||
setEntity({
|
|
||||||
id: it.entity.id,
|
|
||||||
title: `${it.entity.title}${it.entity.year ? ` (${it.entity.year})` : ""}`,
|
|
||||||
});
|
|
||||||
setEntityQ("");
|
|
||||||
setPlanId(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{it.entity.title}
|
|
||||||
{it.entity.year ? ` (${it.entity.year})` : ""}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
<div className="grid gap-x-3 sm:grid-cols-2">
|
|
||||||
<FormField
|
|
||||||
label="动作"
|
|
||||||
htmlFor="org-action"
|
|
||||||
hint={action === "move" ? "移动需要对应授权" : "保留源文件,不影响做种"}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={action}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setAction(v as "copy" | "move");
|
|
||||||
setPlanId(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="org-action" className="w-full">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent position="popper">
|
|
||||||
<SelectItem value="copy">复制</SelectItem>
|
|
||||||
<SelectItem value="move">移动</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormField>
|
|
||||||
<FormField label="目标库根" htmlFor="org-root">
|
|
||||||
<Select
|
|
||||||
value={root || AUTO}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setRoot(v === AUTO ? "" : v);
|
|
||||||
setPlanId(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="org-root" className="w-full">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent position="popper">
|
|
||||||
<SelectItem value={AUTO}>按作品类型自动</SelectItem>
|
|
||||||
{(roots.data?.items ?? []).map((r) => (
|
|
||||||
<SelectItem key={r.id} value={r.id}>
|
|
||||||
{r.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
{planId ? (
|
|
||||||
<div className="mt-1">
|
|
||||||
<p className="mb-1.5 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
|
||||||
计划预览
|
|
||||||
</p>
|
|
||||||
<PlanPreview planId={planId} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<DialogFooter className="gap-2">
|
|
||||||
<Button variant="outline" onClick={() => run(true)} disabled={organize.isPending}>
|
|
||||||
<Eye />
|
|
||||||
预览计划
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => run(false)}
|
|
||||||
disabled={organize.isPending || !planId || !executable}
|
|
||||||
title={!planId ? "先预览计划" : executable ? undefined : "计划不可执行"}
|
|
||||||
>
|
|
||||||
<Play />
|
|
||||||
执行
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PublicOrganizationFile, PublicPathRef } from "@umt/contracts";
|
import type { PublicOrganizationFile, PublicPathRef } from "@umt/contracts";
|
||||||
import { ArrowLeft, ArrowRight, Bot, ChevronRight } from "lucide-react";
|
import { ArrowLeft, ArrowRight, Bot, ChevronRight, Wand2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link, useParams } from "react-router";
|
import { Link, useParams } from "react-router";
|
||||||
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecord } from "~/api/records.ts";
|
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecord } from "~/api/records.ts";
|
||||||
@@ -8,6 +8,7 @@ import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
|||||||
import { KvList } from "~/components/console/kv-list.tsx";
|
import { KvList } from "~/components/console/kv-list.tsx";
|
||||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||||
|
import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx";
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
import { actorTypeLabel } from "~/lib/labels.ts";
|
import { actorTypeLabel } from "~/lib/labels.ts";
|
||||||
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||||
@@ -118,6 +119,7 @@ export default function OrganizeRecordPage() {
|
|||||||
const { id = "" } = useParams();
|
const { id = "" } = useParams();
|
||||||
const rec = useRecord(id);
|
const rec = useRecord(id);
|
||||||
const r = rec.data;
|
const r = rec.data;
|
||||||
|
const [reorganize, setReorganize] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -134,6 +136,12 @@ export default function OrganizeRecordPage() {
|
|||||||
全部记录
|
全部记录
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
{r && (r.action === "copy" || r.action === "move") && r.status !== "queued" && r.status !== "running" ? (
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setReorganize(true)}>
|
||||||
|
<Wand2 />
|
||||||
|
重新整理
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
{r && (r.status === "failed" || r.status === "partial") ? (
|
{r && (r.status === "failed" || r.status === "partial") ? (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -236,6 +244,9 @@ export default function OrganizeRecordPage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{reorganize && r ? (
|
||||||
|
<OrganizeWizard source={{ kind: "record", recordId: r.id }} onClose={() => setReorganize(false)} />
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"
|
|||||||
import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
|
import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
|
||||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||||
import { OrganizeDialog } from "~/components/downloads/organize-dialog.tsx";
|
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||||
import { useSse } from "~/lib/sse.ts";
|
import { useSse } from "~/lib/sse.ts";
|
||||||
@@ -89,7 +88,6 @@ export default function OrganizePage() {
|
|||||||
const [trigger, setTrigger] = useState("");
|
const [trigger, setTrigger] = useState("");
|
||||||
const [qInput, setQInput] = useState("");
|
const [qInput, setQInput] = useState("");
|
||||||
const [q, setQ] = useState("");
|
const [q, setQ] = useState("");
|
||||||
const [organizing, setOrganizing] = useState(false);
|
|
||||||
const entityId = params.get("mediaEntityId") ?? undefined;
|
const entityId = params.get("mediaEntityId") ?? undefined;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
@@ -113,7 +111,7 @@ export default function OrganizePage() {
|
|||||||
title="整理记录"
|
title="整理记录"
|
||||||
description="每次整理的前后路径与逐文件结果,自动和手动整理都记在这里。"
|
description="每次整理的前后路径与逐文件结果,自动和手动整理都记在这里。"
|
||||||
actions={
|
actions={
|
||||||
<Button size="sm" onClick={() => setOrganizing(true)}>
|
<Button size="sm" onClick={() => navigate("/console/files")}>
|
||||||
<FolderInput />
|
<FolderInput />
|
||||||
手动整理
|
手动整理
|
||||||
</Button>
|
</Button>
|
||||||
@@ -165,10 +163,9 @@ export default function OrganizePage() {
|
|||||||
empty={{
|
empty={{
|
||||||
icon: FileStack,
|
icon: FileStack,
|
||||||
title: filtered ? "没有符合筛选的整理记录" : "还没有整理记录",
|
title: filtered ? "没有符合筛选的整理记录" : "还没有整理记录",
|
||||||
description: "下载完成后会自动整理,也可以点“手动整理”。",
|
description: "下载完成后会自动整理,也可以在文件管理里选中文件整理。",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{organizing ? <OrganizeDialog open onOpenChange={(o) => !o && setOrganizing(false)} /> : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user