feat(web): organize records page parity with MP history (HIS-1)

Poster, source -> target, size, downloader; type/time filters; status
counts; group by work; row menu; batch select, delete and reorganize;
delete confirm dialog with dry-run summary; detail delete and download info.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 23:57:19 +08:00
1 parent 51e82f1cd9
commit 03c651085f
8 files changed
+898 -110

No files matched your search

@@ -16,6 +16,7 @@ import {
useIdentify,
useOrganizeFiles,
useRecordSources,
useRecordSourcesMany,
} from "~/api/files.ts";
import { useRecord } from "~/api/records.ts";
import { useSettings } from "~/api/settings.ts";
@@ -64,7 +65,9 @@ export type OrganizeSource =
/** 做种中且 H&R 未满足:只能复制 */
hnrBlocked?: boolean;
}
| { kind: "record"; recordId: string };
| { kind: "record"; recordId: string }
/** HIS-1 批量重新整理:几条记录的源文件一起进向导;preset = 这几条同属一个作品时预选 */
| { kind: "records"; recordIds: string[]; preset?: WizardEntity | null };
const STEPS = ["文件与作品", "选项", "预览并执行"] as const;
@@ -90,6 +93,7 @@ function useResolvedSource(source: OrganizeSource, roots: FileRoot[] | undefined
});
const recSrc = useRecordSources(isRec ? source.recordId : null);
const rec = useRecord(isRec ? source.recordId : undefined);
const many = useRecordSourcesMany(source.kind === "records" ? source.recordIds : []);
return useMemo((): Resolved => {
const base = { unresolved: [], preset: null, pending: false, error: null };
if (source.kind === "files")
@@ -114,6 +118,28 @@ function useResolvedSource(source: OrganizeSource, roots: FileRoot[] | undefined
label: `下载任务 · ${source.name}`,
};
}
if (source.kind === "records") {
const label = `重新整理 ${source.recordIds.length} 条记录`;
if (many.isPending) return { ...base, items: [], hnrPaths: [], label, pending: true };
if (many.error) return { ...base, items: [], hnrPaths: [], label, error: many.error };
const seen = new Set<string>();
const items = (many.data ?? [])
.flatMap((r) => r.items)
.filter((it) => {
const k = `${it.root}\u0000${it.path}`;
if (seen.has(k)) return false;
seen.add(k);
return true;
});
return {
...base,
items,
hnrPaths: [],
unresolved: [...new Set((many.data ?? []).flatMap((r) => r.unresolved))],
preset: source.preset ?? null,
label,
};
}
if (recSrc.isPending) return { ...base, items: [], hnrPaths: [], label: "整理记录", pending: true };
if (recSrc.error) return { ...base, items: [], hnrPaths: [], label: "整理记录", error: recSrc.error };
const m = rec.data?.media;
@@ -128,7 +154,19 @@ function useResolvedSource(source: OrganizeSource, roots: FileRoot[] | undefined
: null,
label: `重新整理 · ${m ? entityLabel(m) : "整理记录"}`,
};
}, [source, roots, maps.isLoading, maps.data, recSrc.isPending, recSrc.error, recSrc.data, rec.data]);
}, [
source,
roots,
maps.isLoading,
maps.data,
recSrc.isPending,
recSrc.error,
recSrc.data,
rec.data,
many.isPending,
many.error,
many.data,
]);
}
/**