feat: 添加批量 Deep Scan 功能

功能:
- 在 Registered 和 Completed section 添加批量 Deep Scan 按钮
- 显示条件:当有已选择的 registered/completed 文件时
- 使用 p-limit 控制并发(maxConcurrent = 3)
- 超过并发的文件自动排队等待

实现细节:
- 添加 batchProcessing ref 跟踪批量处理状态
- 实现 batchDeepScan 函数,调用 re_scan_file API
- 显示处理进度和队列信息
- 成功后更新文件状态为 'scanning'
- 处理完成后清空选择并刷新文件列表

用户体验:
- 点击按钮 → 确认对话框 → 显示进度
- 显示格式:"Processing 1/10 (7 in queue)..."
- 完成后显示成功/失败统计
This commit is contained in:
2026-10-08 00:50:24 +08:00
parent a1e0519170
commit d3df1e54d0

View File

@@ -99,6 +99,9 @@
<div v-if="completedFiles.length && showCompletedSection" class="ms-fm-section">
<div class="ms-fm-section-title">
✅ {{ t('library.status.completed') }} ({{ completedFiles.length }})
<button v-if="selectedRegisteredFiles.length > 0" class="ms-batch-scan-btn" @click="batchDeepScan">
🔍 Deep Scan ({{ selectedRegisteredFiles.length }})
</button>
<button v-if="selectedRegisteredFiles.length > 0" class="ms-batch-unregister-btn" @click="batchUnregister">
🗑️ {{ t('library.context.unregister') }} ({{ selectedRegisteredFiles.length }})
</button>
@@ -145,6 +148,9 @@
<div v-if="showRegisteredSection" class="ms-fm-section">
<div class="ms-fm-section-title">
📋 {{ t('library.status.registered') }} ({{ registeredFiles.length }})
<button v-if="selectedRegisteredFiles.length > 0" class="ms-batch-scan-btn" @click="batchDeepScan">
🔍 Deep Scan ({{ selectedRegisteredFiles.length }})
</button>
<button v-if="selectedRegisteredFiles.length > 0" class="ms-batch-unregister-btn" @click="batchUnregister">
🗑️ {{ t('library.context.unregister') }} ({{ selectedRegisteredFiles.length }})
</button>
@@ -1069,6 +1075,7 @@ async function playVideo(f: any) {
const registering = ref(false)
const processing = ref(false)
const unregistering = ref(false)
const batchProcessing = ref(false)
const reprocessing = ref(false)
const confirmDialog = ref(false)
const confirmTarget = ref<{ mode: 'register' | 'process' | 'reprocess'; file: any } | null>(null)
@@ -1871,6 +1878,84 @@ async function batchRegister() {
setTimeout(() => { actionMsg.value = '' }, 8000)
}
async function batchDeepScan() {
if (batchProcessing.value) return
if (selectedRegisteredFiles.value.length === 0) return
const filesToProcess = selectedRegisteredFiles.value
if (!confirm(`Deep scan ${filesToProcess.length} files?\n\nThis will process all selected files with default settings.`)) return
batchProcessing.value = true
actionMsg.value = `Starting deep scan for ${filesToProcess.length} files...`
const maxConcurrent = 3
const limit = pLimit(maxConcurrent)
let successCount = 0
let failCount = 0
let queueCount = 0
const tasks = filesToProcess.map((f, index) => {
return limit(async () => {
const fileUuid = f.file_uuid
if (!fileUuid) {
failCount++
return
}
const position = index + 1
if (position > maxConcurrent) {
queueCount++
actionMsg.value = `Processing ${successCount + 1}/${filesToProcess.length} (${queueCount} in queue)...`
} else {
actionMsg.value = `Processing ${position}/${filesToProcess.length}...`
}
try {
const file = filesCache.value.find((x: any) => x.file_uuid === fileUuid)
if (!file) {
failCount++
return
}
const result: any = await apiCall('re_scan_file', {
fileUuid: fileUuid,
autoPipeline: true
})
if (result.success) {
successCount++
if (file) {
file.status = 'scanning'
fileStatusCache.value[fileUuid] = 'scanning'
await syncFileStatus(fileUuid)
}
} else {
failCount++
}
} catch (e: any) {
failCount++
console.error('[batchDeepScan] Failed:', f.file_name, e)
}
})
})
await Promise.all(tasks)
selectedFiles.value = []
batchProcessing.value = false
if (successCount > 0) {
actionMsg.value = `Deep scan started for ${successCount} files${failCount > 0 ? `, ${failCount} failed` : ''}`
} else {
actionMsg.value = `Failed to start deep scan for all ${failCount} files`
}
await loadFiles()
setTimeout(() => { actionMsg.value = '' }, 8000)
}
async function batchUnregister() {
if (unregistering.value) return
if (selectedRegisteredFiles.value.length === 0) return
@@ -2248,7 +2333,9 @@ function resetFilters() {
.ms-fm-section-title { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px; padding-left: 4px; display: flex; align-items: center; gap: 12px; }
.ms-batch-register-btn { margin-left: auto; padding: 4px 12px; font-size: 12px; font-weight: 600; background: var(--primary-color); color: white; border: none; border-radius: 6px; cursor: pointer; }
.ms-batch-register-btn:hover { background: var(--primary-hover); }
.ms-batch-unregister-btn { margin-left: auto; padding: 4px 12px; font-size: 12px; font-weight: 600; background: var(--danger-color, #dc3545); color: white; border: none; border-radius: 6px; cursor: pointer; }
.ms-batch-scan-btn { margin-left: 8px; padding: 4px 12px; font-size: 12px; font-weight: 600; background: var(--info-color, #17a2b8); color: white; border: none; border-radius: 6px; cursor: pointer; }
.ms-batch-scan-btn:hover { background: var(--info-hover, #138496); }
.ms-batch-unregister-btn { margin-left: 8px; padding: 4px 12px; font-size: 12px; font-weight: 600; background: var(--danger-color, #dc3545); color: white; border: none; border-radius: 6px; cursor: pointer; }
.ms-batch-unregister-btn:hover { background: var(--danger-hover, #c82333); }
.ms-fm-card-checkbox { position: absolute; bottom: 8px; right: 8px; width: 24px; height: 24px; border: 2px solid var(--border-color); border-radius: 4px; background: var(--card-background); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--primary-color); z-index: 10; }
.ms-fm-card-checkbox:hover { border-color: var(--primary-color); }