[功能] 人物管理按运动视频重设计 - 人物卡新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳时间轴定位); 新增 GET /api/ui/people/clips 后端接口(按 label/canonical_name 查关联运动片段,含 first_ts/clip_events); Timeline 支持 ?video= 定位
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api, fmtMonthDayTime } from '../api.js'
|
||||
import Badge from './Badge.vue'
|
||||
|
||||
@@ -8,6 +9,7 @@ const props = defineProps({
|
||||
allLabels: { type: Array, default: () => [] },
|
||||
})
|
||||
const emit = defineEmits(['named', 'merged'])
|
||||
const router = useRouter()
|
||||
|
||||
const isNamed = computed(() => props.group.is_named)
|
||||
const firstSeenStr = computed(() => {
|
||||
@@ -77,6 +79,31 @@ async function doMerge(sourceLabel, target) {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 运动片段区块:某人物出现过的运动片段(按运动视频提取)
|
||||
const clipsOpen = ref(false)
|
||||
const clips = ref([])
|
||||
const clipsLoading = ref(false)
|
||||
const clipsError = ref('')
|
||||
|
||||
async function toggleClips() {
|
||||
clipsOpen.value = !clipsOpen.value
|
||||
if (!clipsOpen.value || clips.value.length || clipsLoading.value) return
|
||||
clipsLoading.value = true
|
||||
clipsError.value = ''
|
||||
try {
|
||||
const data = await api.peopleClips(props.group.display, 10)
|
||||
clips.value = data.clips || []
|
||||
} catch (e) {
|
||||
clipsError.value = e.message
|
||||
} finally {
|
||||
clipsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToClip(videoId) {
|
||||
router.push({ path: '/timeline', query: { video: videoId } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,6 +128,28 @@ async function doMerge(sourceLabel, target) {
|
||||
</div>
|
||||
<div v-else class="mt-2 text-[11px] text-text-faint">特征待大模型补充(下段视频分析时由 VLM 落库)</div>
|
||||
|
||||
<button @click="toggleClips" class="mt-3 w-full rounded-lg border border-border bg-panel px-3 py-1.5 text-xs font-medium text-text-dim transition-colors hover:border-border-hi">
|
||||
{{ clipsOpen ? '收起' : '查看' }}运动片段{{ clips.length ? `(${clips.length})` : '' }}
|
||||
</button>
|
||||
<div v-if="clipsOpen" class="mt-2 space-y-2">
|
||||
<div v-if="clipsLoading" class="py-2 text-center text-[11px] text-text-faint">加载中…</div>
|
||||
<div v-else-if="clipsError" class="py-2 text-center text-[11px] text-danger">{{ clipsError }}</div>
|
||||
<div v-else-if="!clips.length" class="py-2 text-center text-[11px] text-text-faint">暂无该人物的运动片段</div>
|
||||
<button v-for="c in clips" :key="c.video_id" @click="goToClip(c.video_id)"
|
||||
class="flex w-full items-start gap-2.5 rounded-xl border border-border bg-panel-3 p-2 text-left transition-colors hover:border-border-hi">
|
||||
<img v-if="c.first_ts" loading="lazy" alt="片段缩略图"
|
||||
:src="`/api/proxy/frame?video_id=${c.video_id}&ts=${encodeURIComponent(c.first_ts)}&w=160`"
|
||||
class="h-[54px] w-[96px] shrink-0 rounded-lg border border-border object-cover" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-[12px] font-medium text-text">{{ fmtMonthDayTime(c.event_start_time) }}
|
||||
<span v-if="c.duration_sec" class="ml-1 font-normal text-text-mute">{{ Math.round(c.duration_sec) }}s</span>
|
||||
</div>
|
||||
<div class="mt-0.5 line-clamp-2 text-[11px] leading-snug text-text-dim">{{ c.summary_json || '(暂无摘要)' }}</div>
|
||||
<div v-if="c.clip_events" class="mt-0.5 text-[10px] text-text-faint">{{ c.clip_events }} 个事件 · {{ c.camera_name || '未知' }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMsg" class="mt-2 text-xs text-danger">{{ errorMsg }}</p>
|
||||
|
||||
<div v-if="!isNamed" class="mt-3 space-y-2.5">
|
||||
|
||||
Reference in New Issue
Block a user