feat: add 待定人臉 section with face candidates
This commit is contained in:
@@ -82,6 +82,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<div v-if="ctxMenu.show" class="ms-ctx-menu" :style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px', display: 'block' }">
|
<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 mergeTarget = ref('')
|
||||||
const candidates = ref<any[]>([])
|
const candidates = ref<any[]>([])
|
||||||
const candidateThumbs = ref<Record<string, string>>({})
|
const candidateThumbs = ref<Record<string, string>>({})
|
||||||
|
const faceCandidates = ref<any[]>([])
|
||||||
const ctxMenu = ref({ show: false, x: 0, y: 0, person: null as any })
|
const ctxMenu = ref({ show: false, x: 0, y: 0, person: null as any })
|
||||||
|
|
||||||
const confirmedPeople = computed(() => {
|
const confirmedPeople = computed(() => {
|
||||||
@@ -239,6 +258,12 @@ onMounted(async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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)
|
document.addEventListener('click', closeCtxMenu)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -309,7 +334,7 @@ async function confirmDelete() {
|
|||||||
|
|
||||||
async function loadCandidates() {
|
async function loadCandidates() {
|
||||||
try {
|
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 : []
|
candidates.value = Array.isArray(result) ? result : []
|
||||||
} catch (e) { console.error('Failed to load candidates:', e) }
|
} catch (e) { console.error('Failed to load candidates:', e) }
|
||||||
}
|
}
|
||||||
@@ -333,6 +358,10 @@ async function confirmMerge() {
|
|||||||
} catch (e) { console.error('Merge failed:', e) }
|
} 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 {
|
function formatTime(sec: number): string {
|
||||||
const m = Math.floor(sec / 60); const s = Math.floor(sec % 60)
|
const m = Math.floor(sec / 60); const s = Math.floor(sec % 60)
|
||||||
return `${m}:${s.toString().padStart(2, '0')}`
|
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 { 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-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-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; }
|
.ms-ppl-media-label { font-size: 13px; color: #5f6368; margin-bottom: 16px; }
|
||||||
.close-btn { position: absolute; top: 16px; right: 16px; }
|
.close-btn { position: absolute; top: 16px; right: 16px; }
|
||||||
.detail-header { display: flex; align-items: center; gap: 20px; margin-bottom: 24px; }
|
.detail-header { display: flex; align-items: center; gap: 20px; margin-bottom: 24px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user