All API through Rust proxy, fix unregistered files null uuid, People enhancements

This commit is contained in:
2026-06-14 11:05:23 +08:00
parent a700cc983e
commit 3ba2120c8e
8 changed files with 265 additions and 45 deletions

View File

@@ -1,11 +1,14 @@
<template>
<div class="people-view">
<h1>People</h1>
<input v-model="search" class="search-input" placeholder="Search people..." />
<div class="toolbar">
<h1>People</h1>
<input v-model="search" class="search-input" placeholder="Search people..." />
</div>
<div v-if="loading" class="loading">Loading...</div>
<div v-else class="grid">
<div v-for="p in filteredPeople" :key="p.identity_uuid" class="person-card" @click="selectPerson(p)">
<div class="avatar">{{ p.name?.[0]?.toUpperCase() || '?' }}</div>
<img v-if="profiles[p.identity_uuid]" class="avatar-img" :src="profiles[p.identity_uuid]" alt="" loading="lazy" @vue:mounted="loadProfile(p.identity_uuid)">
<div v-else class="avatar" @vue:mounted="loadProfile(p.identity_uuid)">{{ p.name?.[0]?.toUpperCase() || '?' }}</div>
<p class="name">{{ p.name }}</p>
</div>
</div>
@@ -13,8 +16,19 @@
<div class="detail-box">
<button class="close-btn" @click="selected = null">×</button>
<div class="detail-header">
<div class="detail-avatar">{{ selected.name?.[0]?.toUpperCase() || '?' }}</div>
<div><h2>{{ selected.name }}</h2><p class="uuid">{{ selected.identity_uuid }}</p></div>
<img v-if="selectedProfile" class="detail-avatar-img" :src="selectedProfile" alt="">
<div v-else class="detail-avatar">{{ selected.name?.[0]?.toUpperCase() || '?' }}</div>
<div class="detail-info">
<div class="name-row">
<input v-if="editingName" v-model="editName" class="name-input" @keyup.enter="saveName" @blur="saveName" />
<h2 v-else @click="startEditName">{{ selected.name }}</h2>
<button class="edit-btn" @click="startEditName" title="Edit name"></button>
</div>
<p class="uuid">{{ selected.identity_uuid }}</p>
<div class="detail-actions">
<button class="action-btn delete" @click="confirmDelete">Delete</button>
</div>
</div>
</div>
<div class="detail-content">
<div class="section"><h3>Faces ({{ faces.length }})</h3>
@@ -64,6 +78,11 @@ const traces = ref<any[]>([])
const playing = ref(false)
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, title: '' })
const profiles = ref<Record<string, string>>({})
const selectedProfile = ref<string>('')
const editingName = ref(false)
const editName = ref('')
const filteredPeople = computed(() => {
if (!search.value) return people.value
const s = search.value.toLowerCase()
@@ -71,9 +90,13 @@ const filteredPeople = computed(() => {
})
onMounted(async () => {
console.log('PeopleView mounted')
try {
const result: any = await invoke('get_people', { page: 1, perPage: 1000 })
console.log('Calling get_people...')
const result: any = await invoke('get_people', { page: 1, per_page: 1000 })
console.log('get_people result:', result)
people.value = Array.isArray(result) ? result : []
console.log('People count:', people.value.length)
} catch (e) {
console.error('Failed to load people:', e)
} finally {
@@ -81,11 +104,21 @@ onMounted(async () => {
}
})
async function loadProfile(uuid: string) {
if (profiles.value[uuid]) return
try {
profiles.value[uuid] = await invoke('get_identity_profile', { uuid })
} catch {
// keep default avatar
}
}
async function selectPerson(p: any) {
selected.value = p
selectedProfile.value = profiles.value[p.identity_uuid] || ''
try {
const fResult: any = await invoke('get_faces', { uuid: p.identity_uuid, perPage: 100 })
const tResult: any = await invoke('get_traces', { uuid: p.identity_uuid, perPage: 100 })
const fResult: any = await invoke('get_faces', { uuid: p.identity_uuid, per_page: 100 })
const tResult: any = await invoke('get_traces', { uuid: p.identity_uuid, per_page: 100 })
faces.value = Array.isArray(fResult) ? fResult : []
traces.value = Array.isArray(tResult) ? tResult : []
} catch (e) {
@@ -93,6 +126,35 @@ async function selectPerson(p: any) {
}
}
function startEditName() {
editingName.value = true
editName.value = selected.value.name
}
async function saveName() {
editingName.value = false
if (editName.value === selected.value.name) return
try {
await invoke('update_identity_name', { uuid: selected.value.identity_uuid, name: editName.value })
selected.value.name = editName.value
const idx = people.value.findIndex((p: any) => p.identity_uuid === selected.value.identity_uuid)
if (idx >= 0) people.value[idx].name = editName.value
} catch (e) {
console.error('Failed to update name:', e)
}
}
async function confirmDelete() {
if (!confirm(`Delete "${selected.value.name}"?`)) return
try {
await invoke('delete_identity', { uuid: selected.value.identity_uuid })
people.value = people.value.filter((p: any) => p.identity_uuid !== selected.value.identity_uuid)
selected.value = null
} catch (e) {
console.error('Failed to delete:', e)
}
}
function formatTime(sec: number): string {
const m = Math.floor(sec / 60)
const s = Math.floor(sec % 60)
@@ -112,21 +174,32 @@ function playTrace(t: any) {
<style scoped>
.people-view { max-width: 1200px; }
h1 { margin-bottom: 20px; }
.search-input { padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 8px; width: 300px; margin-bottom: 20px; }
.toolbar { display: flex; align-items: center; gap: 20px; margin-bottom: 20px; }
h1 { margin: 0; }
.search-input { padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 8px; width: 300px; }
.loading { text-align: center; padding: 40px; color: #6b7280; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 16px; }
.person-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; text-align: center; cursor: pointer; }
.person-card:hover { border-color: #4f46e5; }
.avatar { width: 56px; height: 56px; border-radius: 50%; background: #eef2ff; color: #4f46e5; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 600; margin: 0 auto 8px; }
.avatar, .avatar-img { width: 56px; height: 56px; border-radius: 50%; background: #eef2ff; color: #4f46e5; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 600; margin: 0 auto 8px; object-fit: cover; }
.name { font-size: 0.75rem; color: #374151; }
.detail-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.detail-box { background: #fff; border-radius: 20px; padding: 32px; width: min(700px, 90vw); max-height: 80vh; overflow-y: auto; position: relative; }
.close-btn { position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border-radius: 50%; background: #f3f4f6; border: none; font-size: 1.2rem; cursor: pointer; }
.detail-header { display: flex; align-items: center; gap: 20px; margin-bottom: 24px; }
.detail-avatar { width: 80px; height: 80px; border-radius: 20px; background: #eef2ff; color: #4f46e5; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: 700; }
.detail-header h2 { margin-bottom: 4px; }
.uuid { color: #9ca3af; font-size: 0.8rem; font-family: monospace; }
.detail-avatar, .detail-avatar-img { width: 80px; height: 80px; border-radius: 20px; background: #eef2ff; color: #4f46e5; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: 700; object-fit: cover; flex-shrink: 0; }
.detail-info { flex: 1; }
.name-row { display: flex; align-items: center; gap: 8px; }
.name-row h2 { margin: 0; cursor: pointer; }
.name-row h2:hover { text-decoration: underline; }
.name-input { font-size: 1.25rem; font-weight: 700; border: 1px solid #d1d5db; border-radius: 6px; padding: 4px 8px; width: 200px; }
.edit-btn { background: none; border: none; cursor: pointer; font-size: 1rem; color: #6b7280; padding: 4px; }
.edit-btn:hover { color: #4f46e5; }
.uuid { color: #9ca3af; font-size: 0.8rem; font-family: monospace; margin: 4px 0 8px; }
.detail-actions { display: flex; gap: 8px; }
.action-btn { padding: 6px 14px; border-radius: 8px; border: 1px solid #d1d5db; background: #fff; cursor: pointer; font-size: 0.85rem; }
.action-btn.delete { color: #dc2626; border-color: #fca5a5; }
.action-btn.delete:hover { background: #fef2f2; }
.section { margin-bottom: 24px; }
.section h3 { margin-bottom: 12px; font-size: 1rem; }
.face-strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; }