diff --git a/web/src/pages/ToolsPage.tsx b/web/src/pages/ToolsPage.tsx index 31c0396..54b844e 100644 --- a/web/src/pages/ToolsPage.tsx +++ b/web/src/pages/ToolsPage.tsx @@ -7,6 +7,7 @@ import { libraryAPI, mediaAPI } from '../api/library' import { toolsAPI, type OrganizeSource } from '../api/tools' import type { Library, Media, Setting } from '../types' import { ManagementShortcuts } from '../components/ManagementShortcuts' +import { confirmAction } from '../components/ConfirmDialog' // ToolsPage gathers admin-only one-off operations that don't belong on a // dedicated screen of their own: @@ -94,6 +95,7 @@ function OrganizePanel() { const [sources, setSources] = useState([]) const [sourceDir, setSourceDir] = useState('') const [organizingDir, setOrganizingDir] = useState(false) + const [ingestingDir, setIngestingDir] = useState(false) const overrides = () => { const o: { source_path?: string; dest_path?: string; transfer_mode?: string } = {} @@ -104,7 +106,7 @@ function OrganizePanel() { } useEffect(() => { - libraryAPI.list().then(setLibraries).catch(() => undefined) + libraryAPI.list({ includeHidden: true }).then(setLibraries).catch(() => undefined) toolsAPI .organizeSources() .then((s) => { @@ -169,14 +171,24 @@ function OrganizePanel() { } } - const onOrganizeDir = async (e: FormEvent) => { - e.preventDefault() + const runOrganizeDir = async (scanAfter: boolean) => { const src = (sourceDir || sourcePath).trim() if (!src) { toast.error('请选择或填写整理来源目录') return } - setOrganizingDir(true) + const ok = await confirmAction({ + title: scanAfter ? '确认整理并入库' : '确认整理来源目录', + message: scanAfter + ? '将从来源目录整理/重命名媒体文件到目的地目录,然后扫描媒体库把新文件入库。若开启洗版,高分辨率来源可能会替换低分辨率旧版本。' + : '将从来源目录整理/重命名媒体文件到目的地目录,并执行去重与洗版判断。此操作不会自动扫描入库。', + confirmText: scanAfter ? '开始整理入库' : '确认整理', + danger: false, + }) + if (!ok) return + + if (scanAfter) setIngestingDir(true) + else setOrganizingDir(true) try { const r = await toolsAPI.organizeDirectory({ source_path: src, @@ -184,8 +196,12 @@ function OrganizePanel() { transfer_mode: transferMode || undefined, }) const replaced = r.replaced ?? 0 + let scanSummary = '' + if (scanAfter) { + scanSummary = await scanLibrariesForIngest() + } toast.success( - `整理完成:新增 ${r.organized} · 替换(洗版) ${replaced} · 去重跳过 ${r.skipped}`, + `整理完成:新增 ${r.organized} · 替换(洗版) ${replaced} · 去重跳过 ${r.skipped}${scanSummary}`, ) } catch (err: unknown) { const msg = @@ -194,9 +210,47 @@ function OrganizePanel() { toast.error(msg) } finally { setOrganizingDir(false) + setIngestingDir(false) } } + const onOrganizeDir = async (e: FormEvent) => { + e.preventDefault() + await runOrganizeDir(false) + } + + const scanLibrariesForIngest = async (): Promise => { + let availableLibraries = libraries + if (availableLibraries.length === 0) { + availableLibraries = await libraryAPI.list({ includeHidden: true }).catch(() => []) + if (availableLibraries.length > 0) setLibraries(availableLibraries) + } + const targets = (libraryID + ? availableLibraries.filter((l) => l.id === libraryID) + : availableLibraries.filter((l) => l.enabled !== false) + ) + if (targets.length === 0) return ' · 未找到可扫描媒体库' + + let added = 0 + let updated = 0 + let visited = 0 + const failed: string[] = [] + for (const lib of targets) { + try { + const res = await libraryAPI.scan(lib.id) + added += res.added ?? 0 + updated += res.updated ?? 0 + visited += res.visited ?? 0 + } catch { + failed.push(lib.name) + } + } + if (failed.length > 0) { + toast.error(`部分媒体库扫描失败:${failed.join('、')}`) + } + return ` · 入库扫描 ${targets.length - failed.length}/${targets.length} 个库 · 新入库 ${added} · 更新 ${updated} · 访问 ${visited}` + } + const doSearch = async (e?: FormEvent) => { e?.preventDefault() if (!query.trim()) return @@ -407,7 +461,7 @@ function OrganizePanel() {

- 整理来源目录:直接整理整个目录(如下载目录 /downloads),无需是已登记的媒体库。已存在于目的地的媒体会自动去重跳过,来源分辨率更高时会替换(洗版)旧版本。目的地留空则使用「整理目的地目录」设置或媒体目录。 + 整理来源目录:直接整理整个目录(如下载目录 /downloads),无需是已登记的媒体库。已存在于目的地的媒体会自动去重跳过,来源分辨率更高时会替换(洗版)旧版本。点击「开始整理入库」会在整理完成后自动扫描媒体库。