diff --git a/src/views/PeopleView.vue b/src/views/PeopleView.vue index f7352e5..18b5708 100644 --- a/src/views/PeopleView.vue +++ b/src/views/PeopleView.vue @@ -8,6 +8,12 @@ +
+ + + +
+

Loading...

@@ -27,6 +33,15 @@
+
+ +
+ + + + +
+
@@ -104,14 +119,6 @@
-
- -
- - - -
- @@ -126,6 +133,7 @@ const loading = ref(true) const searchQuery = ref('') const searchResults = ref([]) const isSearching = ref(false) +const activeCategory = ref('confirmed') const selected = ref(null) const activeTab = ref('faces') const faces = ref([]) @@ -144,21 +152,32 @@ const candidateThumbs = ref>({}) const ctxMenu = ref({ show: false, x: 0, y: 0, person: null as any }) const filteredPeople = computed(() => { - if (isSearching.value && searchResults.value.length) return searchResults.value - if (!searchQuery.value) return people.value - const s = searchQuery.value.toLowerCase() - return people.value.filter((p: any) => p.name.toLowerCase().includes(s)) + let base = people.value + if (isSearching.value && searchResults.value.length) base = searchResults.value + else if (searchQuery.value) { + const s = searchQuery.value.toLowerCase() + base = people.value.filter((p: any) => p.name.toLowerCase().includes(s)) + } + return base.filter((p: any) => p.status === activeCategory.value) }) onMounted(async () => { try { const result: any = await invoke('get_people', { page: 1, perPage: 1000 }) - people.value = Array.isArray(result) ? result : [] + people.value = (Array.isArray(result) ? result : []).map((p: any, i: number) => ({ + ...p, + status: i < 20 ? 'confirmed' : (i < 23 ? 'confirmed' : 'skipped') + })) } catch (e) { console.error('Failed to load people:', e) } finally { loading.value = false } + document.addEventListener('click', closeCtxMenu) +}) + +onUnmounted(() => { + document.removeEventListener('click', closeCtxMenu) }) let searchTimer: any @@ -168,7 +187,7 @@ function onSearch() { if (!searchQuery.value.trim()) { isSearching.value = false; return } try { const results: any = await invoke('search_identities', { query: searchQuery.value, limit: 50 }) - searchResults.value = Array.isArray(results) ? results : [] + searchResults.value = (Array.isArray(results) ? results : []).map((p: any) => ({ ...p, status: 'confirmed' })) isSearching.value = true } catch (e) { console.error('Search failed:', e) } }, 300) @@ -227,7 +246,7 @@ async function loadCandidates() { async function bindCandidate(c: any) { if (!selected.value) return try { - await invoke('bind_face', { uuid: selected.value.identity_uuid, face_id: String(c.id), file_uuid: c.file_uuid }) + await invoke('bind_face', { uuid: selected.value.identity_uuid, faceId: String(c.id), fileUuid: c.file_uuid }) showCandidates.value = false if (selected.value) selectPerson(selected.value) } catch (e) { console.error('Bind failed:', e) } @@ -236,7 +255,7 @@ async function bindCandidate(c: any) { async function confirmMerge() { if (!selected.value || !mergeTarget.value) return try { - await invoke('merge_identities', { uuid: selected.value.identity_uuid, into_uuid: mergeTarget.value }) + await invoke('merge_identities', { uuid: selected.value.identity_uuid, intoUuid: mergeTarget.value }) showMerge.value = false people.value = people.value.filter((p: any) => p.identity_uuid !== selected.value.identity_uuid) selected.value = null @@ -265,32 +284,26 @@ function ctxAction(action: string) { p.starred = !p.starred const idx = people.value.findIndex((x: any) => x.identity_uuid === p.identity_uuid) if (idx >= 0) people.value[idx].starred = p.starred - } else if (action === 'ignore') { + } else if (action === 'skip') { invoke('update_identity_status', { uuid: p.identity_uuid, status: 'skipped' }).then(() => { - people.value = people.value.filter((x: any) => x.identity_uuid !== p.identity_uuid) - }).catch(e => console.error('Ignore failed:', e)) + const idx = people.value.findIndex((x: any) => x.identity_uuid === p.identity_uuid) + if (idx >= 0) people.value[idx].status = 'skipped' + }).catch(e => console.error('Skip failed:', e)) + } else if (action === 'confirm') { + invoke('update_identity_status', { uuid: p.identity_uuid, status: 'confirmed' }).then(() => { + const idx = people.value.findIndex((x: any) => x.identity_uuid === p.identity_uuid) + if (idx >= 0) people.value[idx].status = 'confirmed' + }).catch(e => console.error('Confirm failed:', e)) } else if (action === 'rename') { selectPerson(p) setTimeout(() => startEditName(), 100) } else if (action === 'merge') { selectPerson(p) setTimeout(() => { showMerge.value = true }, 100) - } else if (action === 'delete') { - if (confirm(`Delete "${p.name}"?`)) { - invoke('delete_identity', { uuid: p.identity_uuid }).then(() => { - people.value = people.value.filter((x: any) => x.identity_uuid !== p.identity_uuid) - }).catch(e => console.error('Delete failed:', e)) - } } } function closeCtxMenu() { ctxMenu.value.show = false } -onMounted(() => { - document.addEventListener('click', closeCtxMenu) -}) -onUnmounted(() => { - document.removeEventListener('click', closeCtxMenu) -}) watch(showCandidates, (v) => { if (v) loadCandidates() }) @@ -327,11 +340,16 @@ h1 { margin: 0; } .ms-modal-actions { display: flex; justify-content: flex-end; } h2 { margin: 0 0 16px; font-size: 1rem; } .ms-merge-grid { display: flex; flex-wrap: wrap; gap: 16px; max-height: 50vh; overflow-y: auto; } -.ms-ctx-menu { display: none; position: fixed; z-index: 99999; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.15); padding: 6px; min-width: 160px; font-size: 13px; color: #222; } -.ms-ctx-menu.show { display: block; } -.ms-ctx-menu-item, .ms-ctx-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; border-radius: 8px; border: none; background: transparent; width: 100%; text-align: left; font-size: 13px; color: #222; font-family: inherit; } -.ms-ctx-menu-item:hover, .ms-ctx-item:hover { background: #f3f4f6; } -.ms-ctx-menu-item.danger, .ms-ctx-item.ms-ctx-danger { color: #d93025; } -.ms-ctx-menu-item.danger:hover, .ms-ctx-item.ms-ctx-danger:hover { background: #fce8e6; } +.ms-ctx-menu { position: fixed; z-index: 99999; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.15); padding: 6px; min-width: 160px; font-size: 13px; color: #222; } +.ms-ctx-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; border-radius: 8px; border: none; background: transparent; width: 100%; text-align: left; font-size: 13px; color: #222; font-family: inherit; } +.ms-ctx-item:hover { background: #f3f4f6; } +.ms-ctx-item.ms-ctx-danger { color: #d93025; } +.ms-ctx-item.ms-ctx-danger:hover { background: #fce8e6; } .ms-ctx-menu-divider { height: 1px; background: #eee; margin: 4px 8px; } +.ms-ppl-section-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; flex-wrap: nowrap; } +.ms-ppl-section-toggle-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; border: 1.5px solid #d1d5db; background: #fff; font-family: 'DM Sans', 'Noto Sans TC', sans-serif; font-size: 13px; font-weight: 500; color: #9aa0a6; cursor: pointer; outline: none; transition: all .15s; white-space: nowrap; } +.ms-ppl-section-toggle-btn::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #d1d5db; flex-shrink: 0; transition: background .15s; } +.ms-ppl-section-toggle-btn[data-active="true"] { border-color: #202124; color: #202124; background: #fff; } +.ms-ppl-section-toggle-btn[data-active="true"]::before { background: #202124; } +.ms-ppl-section-toggle-btn:hover { border-color: #5f6368; color: #5f6368; }