Files
momentry_core/portal/src/views/FilesView.vue

277 lines
10 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>
<!-- Search input -->
<input
v-model="searchQuery"
type="text"
placeholder="搜尋檔名..."
class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white text-sm w-full md:w-48 focus:outline-none focus:border-blue-500"
/>
</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
try {
const config = getCurrentConfig()
const scanResp = await httpFetch<any>(`${config.api_base_url}/api/v1/files/scan`)
const scanFiles: any[] = (scanResp?.files || []).map((f: any) => ({
...f,
status: f.is_registered ? 'registered_scan' : 'unregistered'
}))
// Get registered files with real processing status
let regFiles: any[] = []
try {
const regResp = await httpFetch<any>(`${config.api_base_url}/api/v1/files?page=1&page_size=100`)
regFiles = (regResp?.files || regResp?.data || []).map((f: any) => ({
...f,
status: f.status || 'pending'
}))
} catch {
// Registered files API may not be available; use scan data only
}
// Merge: scan results first, then overlay with registered statuses
const merged = new Map<string, any>()
for (const f of scanFiles) {
merged.set(f.file_path, f)
}
for (const f of regFiles) {
merged.set(f.file_path, f)
}
files.value = Array.from(merged.values())
} catch (e) {
console.error('Failed to fetch files:', e)
error.value = String(e)
} finally {
loading.value = false
}
}
async function registerFile(filePath: string) {
if (!filePath) { alert('無法註冊:缺少檔案路徑'); return }
try {
await registerVideo(filePath)
// Refresh list
await fetchFiles()
} catch (e) {
console.error('Register failed:', e)
alert('註冊失敗:' + e)
}
}
async function unregisterFile(fileUuid: string, fileName: string) {
if (!fileUuid) { alert('無法取消註冊:缺少 UUID'); return }
const displayName = fileName || '未知檔案'
if (!confirm(`確定要取消註冊 "${displayName}" 嗎?這將刪除資料庫中的相關記錄。`)) {
return
}
try {
await unregisterVideo(fileUuid)
await fetchFiles()
} catch (e) {
console.error('Unregister failed:', e)
alert('取消註冊失敗:' + e)
}
}
async function startProcessing(fileUuid: string) {
if (!fileUuid) { alert('無法處理:缺少 UUID'); return }
if (!confirm('確定要開始分析處理此檔案嗎?')) return
try {
const config = getCurrentConfig()
await httpFetch(`${config.api_base_url}/api/v1/file/${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) {
if (!fileUuid) { alert('無法開啟工作台:缺少 UUID'); return }
router.push(`/file/${fileUuid}`)
}
onMounted(fetchFiles)
</script>