feat: add 待定人臉 section with face candidates

This commit is contained in:
2026-06-15 01:09:31 +08:00
parent d900cb9545
commit 9a6abcbdcb

View File

@@ -82,6 +82,24 @@
</div>
</div>
</div>
<hr class="ms-ppl-hr">
<!-- 待定人臉 -->
<div v-if="faceCandidates.length" class="ms-ppl-section">
<div class="ms-ppl-section-toolbar">
<div class="ms-ppl-section-title">待定人臉</div>
</div>
<div class="ms-ppl-face-grid ms-uface-grid">
<div v-for="c in faceCandidates.slice(0, 50)" :key="c.id" class="ms-ppl-face-card" @click="showAssignModal(c)">
<div class="ms-ppl-face-img-wrap">
<img v-if="candidateThumbs[c.file_uuid]" :src="candidateThumbs[c.file_uuid]" alt="" @vue:mounted="loadCandidateThumb(c.file_uuid)">
<div v-else class="face-placeholder" @vue:mounted="loadCandidateThumb(c.file_uuid)">{{ Math.round(c.confidence * 100) }}%</div>
</div>
<span class="ms-ppl-face-name">{{ c.file_uuid.slice(0, 8) }}... #{{ c.frame_number }}</span>
</div>
</div>
</div>
</template>
<div v-if="ctxMenu.show" class="ms-ctx-menu" :style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px', display: 'block' }">
@@ -207,6 +225,7 @@ const showMerge = ref(false)
const mergeTarget = ref('')
const candidates = ref<any[]>([])
const candidateThumbs = ref<Record<string, string>>({})
const faceCandidates = ref<any[]>([])
const ctxMenu = ref({ show: false, x: 0, y: 0, person: null as any })
const confirmedPeople = computed(() => {
@@ -239,6 +258,12 @@ onMounted(async () => {
} finally {
loading.value = false
}
try {
const fc: any = await invoke('getFaceCandidates', { page: 1, perPage: 100 })
faceCandidates.value = Array.isArray(fc) ? fc : []
} catch (e) {
console.error('Failed to load face candidates:', e)
}
document.addEventListener('click', closeCtxMenu)
})
@@ -309,7 +334,7 @@ async function confirmDelete() {
async function loadCandidates() {
try {
const result: any = await invoke('get_face_candidates', { page: 1, perPage: 50 })
const result: any = await invoke('getFaceCandidates', { page: 1, perPage: 50 })
candidates.value = Array.isArray(result) ? result : []
} catch (e) { console.error('Failed to load candidates:', e) }
}
@@ -333,6 +358,10 @@ async function confirmMerge() {
} catch (e) { console.error('Merge failed:', e) }
}
function showAssignModal(c: any) {
alert(`Assign face ${c.id} to existing person - not yet implemented`)
}
function formatTime(sec: number): string {
const m = Math.floor(sec / 60); const s = Math.floor(sec % 60)
return `${m}:${s.toString().padStart(2, '0')}`
@@ -416,6 +445,8 @@ h1 { margin: 0; }
.ms-ppl-strip-face { position: relative; flex-shrink: 0; cursor: pointer; }
.ms-ppl-strip-face-img { width: 52px; height: 52px; border-radius: 12px; border: 2px solid transparent; background: #e8eaed; overflow: hidden; transition: border-color .15s; }
.ms-ppl-strip-face:hover .ms-ppl-strip-face-img { border-color: #202124; }
.ms-uface-grid .ms-ppl-face-card { width: 120px; }
.ms-uface-grid .ms-ppl-face-img-wrap { width: 120px; height: 120px; border-radius: 20px; }
.ms-ppl-media-label { font-size: 13px; color: #5f6368; margin-bottom: 16px; }
.close-btn { position: absolute; top: 16px; right: 16px; }
.detail-header { display: flex; align-items: center; gap: 20px; margin-bottom: 24px; }