feat(organize,web): sidecars follow selected videos; /console/organize?ids= prefill (FE-4 follow-ups)

- planImport (sourcePaths mode): when a selected source is a video file,
  same-directory subtitles / external audio whose stem (language tags
  stripped) matches it join the source list, so the organize wizard, which
  expands a directory into individual videos, no longer drops them.
  Unrelated sidecars stay out; pathGuard applies to the added files.
- Organize records page honours ?ids= (wizard multi-group redirect): shows
  just those records via /files/records, refreshes while running, clear chip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:52:33 +08:00
1 parent 363f85b1a1
commit 2ae780d2a1
5 files changed
+310 -7

No files matched your search

+24 -7
View File
@@ -4,6 +4,7 @@ import { EventType } from "@umt/contracts";
import { FileStack, FolderInput, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { useFileRecords } from "~/api/files.ts";
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecords } from "~/api/records.ts";
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
@@ -89,6 +90,12 @@ export default function OrganizePage() {
const [qInput, setQInput] = useState("");
const [q, setQ] = useState("");
const entityId = params.get("mediaEntityId") ?? undefined;
// 整理向导执行多组后跳来:?ids=<记录 id,…> 只看这几条(执行中每 3 秒刷新)
const ids = (params.get("ids") ?? "")
.split(",")
.map((x) => x.trim())
.filter(Boolean);
const byIds = useFileRecords({ ids }, ids.length > 0);
const navigate = useNavigate();
const qc = useQueryClient();
useEffect(() => {
@@ -101,9 +108,13 @@ export default function OrganizePage() {
mediaEntityId: entityId,
q: q || undefined,
});
useSse([EventType.organizationChanged], () => void qc.invalidateQueries({ queryKey: ["records"] }));
const rows = records.data?.pages.flatMap((p) => p.items);
const filtered = !!(status || trigger || q || entityId);
useSse([EventType.organizationChanged], () => {
void qc.invalidateQueries({ queryKey: ["records"] });
if (ids.length) void qc.invalidateQueries({ queryKey: ["files", "records"] });
});
const pinned = ids.length > 0;
const rows = pinned ? byIds.data?.items : records.data?.pages.flatMap((p) => p.items);
const filtered = !!(status || trigger || q || entityId || pinned);
return (
<>
@@ -148,16 +159,22 @@ export default function OrganizePage() {
<X />
</Button>
) : null}
{pinned ? (
<Button variant="outline" size="sm" onClick={() => setParams({})}>
本次整理的 {ids.length} 条
<X />
</Button>
) : null}
</FilterBar>
<DataTable
columns={COLUMNS}
rows={rows}
rowKey={(r) => r.id}
loading={records.isPending}
error={records.error}
onRetry={() => void records.refetch()}
loading={pinned ? byIds.isPending : records.isPending}
error={pinned ? byIds.error : records.error}
onRetry={() => void (pinned ? byIds.refetch() : records.refetch())}
onRowClick={(r) => navigate(`/console/organize/${r.id}`)}
hasMore={records.hasNextPage}
hasMore={!pinned && records.hasNextPage}
onLoadMore={() => void records.fetchNextPage()}
loadingMore={records.isFetchingNextPage}
empty={{
+1
View File
@@ -28,6 +28,7 @@ const STATIC_ROUTES = [
"/console/library?filter=unidentified",
"/console/library?view=identities",
"/console/organize",
"/console/organize?ids=00000000-0000-4000-8000-000000000000",
"/console/files",
// 文件管理 v2(FE-4):筛选/排序组合与演示数据(?mock=1 只在开发构建生效,随后 ?mock=0 关掉,免得影响后面的页)
"/console/files?f=video&sort=size&order=desc&hidden=1",