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:
1 parent
51e82f1cd9
commit
03c651085f
8 files changed
+898
-110
No files matched your search
@@ -1,4 +1,11 @@
|
||||
import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQueries,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
cursorPage,
|
||||
type FileEntry,
|
||||
@@ -281,6 +288,28 @@ export function useRecordSources(recordId: string | null) {
|
||||
});
|
||||
}
|
||||
|
||||
/** HIS-1 批量重新整理:多条记录的源路径一并换算(合并 items / unresolved) */
|
||||
export function useRecordSourcesMany(recordIds: string[]) {
|
||||
return useQueries({
|
||||
queries: recordIds.map((id) => ({
|
||||
queryKey: ["files", "record-sources", id],
|
||||
retry: false,
|
||||
queryFn: ({ signal }: { signal: AbortSignal }) =>
|
||||
call(FilesRecordSourcesResult, "/files/record-sources", { params: { recordId: id }, signal }),
|
||||
})),
|
||||
combine: combineRecordSources,
|
||||
});
|
||||
}
|
||||
|
||||
// 模块级函数:引用稳定,结果不变时 useQueries 不重算(避免下游 useMemo 每次渲染都变)
|
||||
function combineRecordSources(rs: { isPending: boolean; error: unknown; data?: FilesRecordSourcesResult }[]) {
|
||||
return {
|
||||
isPending: rs.some((r) => r.isPending),
|
||||
error: rs.find((r) => r.error)?.error ?? null,
|
||||
data: rs.every((r) => r.data) ? rs.map((r) => r.data!) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 作品选择器里的本地作品 */
|
||||
export type LocalEntityHit = { id: string; title: string; year: number | null; kind: string; alias: string | null };
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
cursorPage,
|
||||
LibraryItem,
|
||||
LibraryRoot,
|
||||
type OrganizationDeleteRequest,
|
||||
OrganizationDeleteResult,
|
||||
OrganizationStats,
|
||||
type OrganizationStatus,
|
||||
type OrganizationTrigger,
|
||||
OrganizeAccepted,
|
||||
@@ -23,6 +26,10 @@ export type RecordFilters = {
|
||||
trigger?: OrganizationTrigger;
|
||||
mediaEntityId?: string;
|
||||
q?: string;
|
||||
kind?: "movie" | "series" | "none";
|
||||
/** ISO 时间:只看之后创建的 */
|
||||
since?: string;
|
||||
organizeOnly?: boolean;
|
||||
};
|
||||
|
||||
export const recordKeys = {
|
||||
@@ -30,8 +37,34 @@ export const recordKeys = {
|
||||
list: (f: RecordFilters) => ["records", "list", f] as const,
|
||||
one: (id: string) => ["records", "one", id] as const,
|
||||
entity: (id: string) => ["records", "entity", id] as const,
|
||||
stats: (f: Omit<RecordFilters, "status">) => ["records", "stats", f] as const,
|
||||
};
|
||||
|
||||
/** HIS-1:按状态计数(筛选同列表,不含状态) */
|
||||
export function useRecordStats(f: Omit<RecordFilters, "status">) {
|
||||
return useQuery({
|
||||
queryKey: recordKeys.stats(f),
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: ({ signal }) =>
|
||||
apiParsed(OrganizationStats, "/api/v1/organization-records/stats", { params: cleanParams(f), signal }),
|
||||
});
|
||||
}
|
||||
|
||||
/** HIS-1:删除整理记录(dryRun 只核对会删什么) */
|
||||
export function useDeleteRecords() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: OrganizationDeleteRequest) =>
|
||||
apiMutate("/api/v1/organization-records/delete", { body }).then((d) => OrganizationDeleteResult.parse(d)),
|
||||
onSuccess: (r) => {
|
||||
if (!r.dryRun) {
|
||||
void qc.invalidateQueries({ queryKey: recordKeys.all });
|
||||
if (r.trashRecordId) void qc.invalidateQueries({ queryKey: ["files"] });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRecords(f: RecordFilters) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: recordKeys.list(f),
|
||||
@@ -109,6 +142,18 @@ export const RECORD_STATUS_TONE: Record<OrganizationStatus, "success" | "warning
|
||||
|
||||
export const TRIGGER_LABEL: Record<OrganizationTrigger, string> = { auto: "自动", manual: "手动", agent: "智能体" };
|
||||
|
||||
/** 整理方式 / 文件操作(记录的 action) */
|
||||
export const ACTION_LABEL = {
|
||||
copy: "复制",
|
||||
move: "移动",
|
||||
rename: "重命名",
|
||||
relocate: "目录调整",
|
||||
hardlink: "硬链接",
|
||||
trash: "移入回收站",
|
||||
delete: "永久删除",
|
||||
restore: "从回收站还原",
|
||||
} as const;
|
||||
|
||||
/** dryRun 生成的整理计划冻结为 artifact(kind=plan),GET /artifacts/{id} 读取逐文件预览 */
|
||||
export function useArtifact(id: string | null | undefined) {
|
||||
return useQuery({
|
||||
|
||||
Reference in new issue
Block a user