feat(timeline): 事件时间轴支持删除视频会话
三端联动实现(fam-ui -> fam-core -> fam-edge),沿用项目里"NAS 转发写请求 到 Oracle"的既有模式(照抄 identity_correct 那套骨架): - fam-edge:oracle_db.py 新增 delete_video()(删 events+videos 行 + 磁盘上 的运动片段文件;不清理 ss_motion_events 源事件,因为素材一旦处理完就不会 被生产者重新捡起,删片段不会触发重新分割);api_gateway.py 新增 POST /api/oracle/video/delete - fam-core:新增 db_layer.delete_sync_video()(第一个"NAS 直接写自己镜像表" 的函数——增量同步只做 upsert 感知不到删除,不能像纠错那样靠 trigger_now() 拉增量顺带清理)+ oracle_sync.push_video_delete() + ui_api.py 新增 DELETE /api/ui/videos/<id>(先回推 Oracle,成功后才清本地镜像,避免数据 不一致) - fam-ui:Timeline.vue 详情卡片加删除按钮(原生 confirm() 二次确认,项目里 之前没有确认弹窗组件先例);api.js 新增 deleteVideo() 新增 6 个 delete_video 单元测试;已在 Oracle/NAS 用自造测试数据完整跑通端 到端链路(转发成功、两端记录清理、磁盘文件删除、幂等 404),未触碰任何真 实监控数据。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ export const api = {
|
||||
|
||||
videos: (params = {}) => request(`/api/ui/videos${qs(params)}`),
|
||||
videoDetail: (id) => request(`/api/ui/videos/${id}`),
|
||||
deleteVideo: (id) => request(`/api/ui/videos/${id}`, { method: 'DELETE' }),
|
||||
stats: (date) => request(`/api/ui/stats${date ? '?date=' + date : ''}`),
|
||||
people: () => request('/api/ui/people'),
|
||||
peopleClips: (label, limit = 10) => request(`/api/ui/people/clips?label=${encodeURIComponent(label)}&limit=${limit}`),
|
||||
|
||||
@@ -94,6 +94,28 @@ const modelBadges = computed(() => {
|
||||
const provider = detail.value?.video?.compute_provider || ''
|
||||
return provider ? String(provider).split(',').map(p => p.trim()).filter(Boolean) : []
|
||||
})
|
||||
|
||||
const deleteBusy = ref(false)
|
||||
const deleteError = ref('')
|
||||
|
||||
async function deleteVideo(id) {
|
||||
if (!confirm('确定删除这个视频会话吗?此操作不可恢复,会同时删除对应的视频文件。')) return
|
||||
deleteBusy.value = true
|
||||
deleteError.value = ''
|
||||
try {
|
||||
await api.deleteVideo(id)
|
||||
videos.value = videos.value.filter(v => v.id !== id)
|
||||
if (selectedId.value === id) {
|
||||
selectedId.value = videos.value.length ? videos.value[0].id : null
|
||||
if (!selectedId.value) detail.value = null
|
||||
}
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
deleteError.value = e.message
|
||||
} finally {
|
||||
deleteBusy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -143,9 +165,14 @@ const modelBadges = computed(() => {
|
||||
<span>{{ detail.video.camera_name || '未知摄像头' }}</span>
|
||||
<Badge tone="ok">会话 #{{ detail.video.id }}</Badge>
|
||||
<Badge v-for="m in modelBadges" :key="m" tone="neutral">{{ m }}</Badge>
|
||||
<button :disabled="deleteBusy" @click="deleteVideo(detail.video.id)"
|
||||
class="ml-auto rounded-lg border border-danger/35 bg-danger/14 px-2.5 py-1 text-xs font-medium text-danger transition-colors hover:bg-danger/25 disabled:opacity-40">
|
||||
{{ deleteBusy ? '删除中…' : '🗑 删除会话' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-1.5 font-mono text-[13px] text-text-dim tabular">⏱ {{ rangeStr }} · 文件 {{ detail.video.filename }}</div>
|
||||
<div class="mt-2.5 whitespace-pre-wrap text-sm leading-relaxed text-[#d6dce6]">{{ detail.video.summary_json || '暂无全局摘要' }}</div>
|
||||
<div v-if="deleteError" class="mt-2.5 rounded-lg border border-danger/35 bg-danger/14 px-3 py-2 text-xs text-danger">{{ deleteError }}</div>
|
||||
</div>
|
||||
|
||||
<EmptyState v-if="!detail.events.length" icon="🎞" text="该会话暂无时间点事件" />
|
||||
|
||||
Reference in New Issue
Block a user