feat: add 待定人臉 section with face candidates
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user