e75c4d6f07
- Remove session-ses_2f27.md (161KB raw session log) - Remove 49 ROOT_* duplicate files across REFERENCE/ - Remove 14 duplicate files between REFERENCE/ root and history/ - Remove asr_legacy.rs (dead code, replaced by asr.rs) - Remove src/core/worker/ (duplicate JobWorker) - Remove src/core/layers/ (empty directory) - Remove 4 .bak files in src/ - Remove 7 dead private methods in worker/processor.rs - Remove backup directory from git tracking
254 lines
9.4 KiB
Vue
254 lines
9.4 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<!-- Header with Search and Filters -->
|
|
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
|
<h2 class="text-2xl font-bold">檔案管理 (Demo)</h2>
|
|
<div class="flex items-center gap-3 w-full md:w-auto">
|
|
<!-- Status Filter -->
|
|
<div class="flex items-center bg-gray-700 rounded p-1">
|
|
<button
|
|
@click="setStatusFilter('all')"
|
|
:class="{'bg-blue-600 text-white': statusFilter === 'all', 'text-gray-300 hover:text-white': statusFilter !== 'all'}"
|
|
class="px-3 py-1 rounded text-sm transition"
|
|
>
|
|
全部
|
|
</button>
|
|
<button
|
|
@click="setStatusFilter('unregistered')"
|
|
:class="{'bg-blue-600 text-white': statusFilter === 'unregistered', 'text-gray-300 hover:text-white': statusFilter !== 'unregistered'}"
|
|
class="px-3 py-1 rounded text-sm transition"
|
|
>
|
|
未註冊
|
|
</button>
|
|
<button
|
|
@click="setStatusFilter('pending')"
|
|
:class="{'bg-blue-600 text-white': statusFilter === 'pending', 'text-gray-300 hover:text-white': statusFilter !== 'pending'}"
|
|
class="px-3 py-1 rounded text-sm transition"
|
|
>
|
|
待處理
|
|
</button>
|
|
<button
|
|
@click="setStatusFilter('processing')"
|
|
:class="{'bg-blue-600 text-white': statusFilter === 'processing', 'text-gray-300 hover:text-white': statusFilter !== 'processing'}"
|
|
class="px-3 py-1 rounded text-sm transition"
|
|
>
|
|
處理中
|
|
</button>
|
|
<button
|
|
@click="setStatusFilter('completed')"
|
|
:class="{'bg-blue-600 text-white': statusFilter === 'completed', 'text-gray-300 hover:text-white': statusFilter !== 'completed'}"
|
|
class="px-3 py-1 rounded text-sm transition"
|
|
>
|
|
已完成
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="flex justify-center py-12">
|
|
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-500"></div>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div v-else-if="error" class="bg-red-900/50 border border-red-700 rounded p-4 text-red-300">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<!-- File List -->
|
|
<div v-else class="bg-gray-800 rounded-lg border border-gray-700 overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-700">
|
|
<thead class="bg-gray-900">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">檔案名稱</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">狀態</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">UUID</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-700 bg-gray-800">
|
|
<tr v-for="file in displayFiles" :key="file.file_uuid || file.file_path" class="hover:bg-gray-750 transition">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-white truncate max-w-xs" :title="file.file_name">
|
|
{{ file.file_name }}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
<span v-if="file.status === 'completed'" class="px-2 py-0.5 rounded text-xs bg-green-900 text-green-200">
|
|
✅ 已完成
|
|
</span>
|
|
<span v-else-if="file.status === 'processing'" class="px-2 py-0.5 rounded text-xs bg-yellow-900 text-yellow-200">
|
|
🔄 處理中
|
|
</span>
|
|
<span v-else-if="file.status === 'pending'" class="px-2 py-0.5 rounded text-xs bg-blue-900 text-blue-200">
|
|
⏳ 待處理
|
|
</span>
|
|
<span v-else class="px-2 py-0.5 rounded text-xs bg-gray-600 text-gray-300">
|
|
📦 未註冊
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300 font-mono text-xs">
|
|
{{ file.file_uuid || '-' }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
<div class="flex justify-end gap-2">
|
|
<!-- Enter Demo / Workbench (Completed) -->
|
|
<button
|
|
v-if="file.status === 'completed'"
|
|
@click="enterWorkbench(file.file_uuid)"
|
|
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-xs rounded transition"
|
|
>
|
|
臉部工作台
|
|
</button>
|
|
|
|
<!-- Start Processing (Pending) -->
|
|
<button
|
|
v-if="file.status === 'pending'"
|
|
@click="startProcessing(file.file_uuid)"
|
|
class="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 text-white text-xs rounded transition"
|
|
>
|
|
開始處理
|
|
</button>
|
|
|
|
<!-- Register (Unregistered) -->
|
|
<button
|
|
v-if="!file.status || file.status === 'unregistered'"
|
|
@click="registerFile(file.file_path)"
|
|
class="px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-xs rounded transition"
|
|
>
|
|
註冊
|
|
</button>
|
|
|
|
<!-- Unregister (Pending/Processing/Completed) -->
|
|
<button
|
|
v-if="file.file_uuid"
|
|
@click="unregisterFile(file.file_uuid, file.file_name)"
|
|
class="px-3 py-1 bg-red-600 hover:bg-red-700 text-white text-xs rounded transition"
|
|
>
|
|
取消註冊
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { registerVideo, unregisterVideo, httpFetch, getCurrentConfig } from '@/api/client'
|
|
|
|
const router = useRouter()
|
|
const files = ref<any[]>([])
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const searchQuery = ref('')
|
|
const statusFilter = ref('all') // all, unregistered, pending, processing, completed
|
|
|
|
const displayFiles = computed(() => {
|
|
let result = files.value
|
|
|
|
// Filter by search
|
|
if (searchQuery.value) {
|
|
const q = searchQuery.value.toLowerCase()
|
|
result = result.filter(f =>
|
|
f.file_name.toLowerCase().includes(q) ||
|
|
(f.file_path && f.file_path.toLowerCase().includes(q))
|
|
)
|
|
}
|
|
|
|
// Filter by status
|
|
if (statusFilter.value !== 'all') {
|
|
result = result.filter(f => f.status === statusFilter.value)
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
function setStatusFilter(status: string) {
|
|
statusFilter.value = status
|
|
}
|
|
|
|
async function fetchFiles() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const config = getCurrentConfig()
|
|
// Call the scan endpoint to get list of files with status
|
|
// Note: /api/v1/files/scan returns unregistered files.
|
|
// We might need a combined list or call /api/v1/files for registered ones.
|
|
// For now, let's assume /api/v1/files/scan returns all files with is_registered flag.
|
|
const response: any = await httpFetch(`${config.api_base_url}/api/v1/files/scan`)
|
|
|
|
// Map scan response to our expected format
|
|
// Scan returns: { files: [ { file_uuid, file_name, is_registered, status... } ] }
|
|
files.value = response.files?.map((f: any) => ({
|
|
...f,
|
|
status: f.status || (f.is_registered ? 'pending' : 'unregistered')
|
|
})) || []
|
|
|
|
// To get actual processing status, we might need to cross reference with /api/v1/files
|
|
// But for Demo, let's rely on the scan endpoint providing status.
|
|
} catch (e) {
|
|
console.error('Failed to fetch files:', e)
|
|
error.value = String(e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function registerFile(filePath: string) {
|
|
try {
|
|
const result = await registerVideo(filePath)
|
|
// Refresh list
|
|
await fetchFiles()
|
|
} catch (e) {
|
|
console.error('Register failed:', e)
|
|
alert('註冊失敗:' + e)
|
|
}
|
|
}
|
|
|
|
async function unregisterFile(fileUuid: string, fileName: string) {
|
|
if (!confirm(`確定要取消註冊 "${fileName}" 嗎?這將刪除資料庫中的相關記錄。`)) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await unregisterVideo(fileUuid)
|
|
await fetchFiles()
|
|
} catch (e) {
|
|
console.error('Unregister failed:', e)
|
|
alert('取消註冊失敗:' + e)
|
|
}
|
|
}
|
|
|
|
async function startProcessing(fileUuid: string) {
|
|
if (!confirm('確定要開始分析處理此檔案嗎?')) return
|
|
|
|
try {
|
|
const config = getCurrentConfig()
|
|
await httpFetch(`${config.api_base_url}/api/v1/assets/${fileUuid}/process`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({})
|
|
})
|
|
|
|
// After triggering, status should change to processing
|
|
// We can poll or just refresh
|
|
await fetchFiles()
|
|
} catch (e) {
|
|
console.error('Start processing failed:', e)
|
|
alert('開始處理失敗:' + e)
|
|
}
|
|
}
|
|
|
|
function enterWorkbench(fileUuid: string) {
|
|
// Navigate to the new Face Workbench view
|
|
router.push(`/workbench/${fileUuid}`)
|
|
}
|
|
|
|
onMounted(fetchFiles)
|
|
</script>
|