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

86 lines
3.7 KiB
Vue

<template>
<div class="space-y-6">
<div class="flex items-center space-x-4">
<button @click="$router.back()" class="text-gray-400 hover:text-white text-lg"> 返回</button>
<h2 class="text-2xl font-bold">Trace #{{ traceId }}</h2>
<span class="text-gray-400 text-sm">{{ fileUuid?.substring(0, 12) }}...</span>
</div>
<div v-if="loading" class="text-center py-12"><div class="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-500 mx-auto"></div></div>
<div v-else-if="trace" class="grid gap-6">
<!-- Summary -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 grid grid-cols-2 md:grid-cols-4 gap-4">
<div><span class="text-xs text-gray-500">DETECTIONS</span><p class="text-white text-lg font-semibold">{{ trace.face_count }}</p></div>
<div><span class="text-xs text-gray-500">DURATION</span><p class="text-white text-lg font-semibold">{{ trace.duration_sec?.toFixed(1) }}s</p></div>
<div><span class="text-xs text-gray-500">CONFIDENCE</span><p class="text-white text-lg font-semibold">{{ (trace.avg_confidence * 100).toFixed(0) }}%</p></div>
<div><span class="text-xs text-gray-500">TIME</span><p class="text-white text-lg font-semibold">{{ trace.first_sec?.toFixed(0) }}s - {{ trace.last_sec?.toFixed(0) }}s</p></div>
</div>
<!-- Video -->
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
<video controls autoplay class="w-full" @error="videoError = true">
<source :src="videoUrl" type="video/mp4" />
</video>
<div v-if="videoError" class="p-4 text-center text-gray-500">Video unavailable</div>
</div>
<!-- Recent Faces -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="text-lg font-semibold mb-4 text-blue-400">Recent Detections</h3>
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
<div v-for="f in recentFaces" :key="f.id" class="bg-gray-900 rounded overflow-hidden">
<img :src="thumbUrl(f)" class="w-full aspect-square object-cover" loading="lazy" @error="e => (e.target as HTMLElement).style.display='none'" />
<div class="p-1 text-[9px] text-gray-400 truncate">#{{ f.start_frame }}<br/>{{ (f.confidence * 100).toFixed(0) }}%</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getCurrentConfig, httpFetch } from '@/api/client'
const route = useRoute()
const fileUuid = route.params.file_uuid as string
const traceId = route.params.trace_id as string
const config = getCurrentConfig()
const trace = ref<any>(null)
const faces = ref<any[]>([])
const loading = ref(true)
const videoError = ref(false)
const videoUrl = computed(() =>
`${config.api_base_url}/api/v1/file/${fileUuid}/trace/${traceId}/video?padding=1`
)
const recentFaces = computed(() => faces.value.slice(0, 40))
function thumbUrl(f: any): string {
return `${config.api_base_url}/api/v1/file/${fileUuid}/thumbnail?frame=${f.start_frame}&x=${f.x}&y=${f.y}&w=${f.width}&h=${f.height}`
}
async function loadData() {
try {
const traces = await httpFetch<any>(`${config.api_base_url}/api/v1/file/${fileUuid}/face_trace/sortby`, {
method: 'POST',
body: JSON.stringify({ limit: 500 })
})
trace.value = (traces.traces || []).find((t: any) => String(t.trace_id) === traceId)
const faceData = await httpFetch<any>(`${config.api_base_url}/api/v1/file/${fileUuid}/trace/${traceId}/faces?limit=50`)
faces.value = faceData.faces || []
} catch (e) {
console.error('Failed to load trace:', e)
} finally {
loading.value = false
}
}
onMounted(() => loadData())
</script>