docs: People 頁面設計文件,基於實測 API 定義

This commit is contained in:
2026-06-14 12:52:22 +08:00
parent 6d6fd2d43c
commit d910bac0bb

205
docs/PEOPLE_DESIGN.md Normal file
View File

@@ -0,0 +1,205 @@
# People 頁面設計文件
## 1. 分層架構
| 層級 | 負責 |
|---|---|
| **Rust (Backend)** | API 代理、檔案系統存取、API key 管理 |
| **Vue (Frontend)** | UI 渲染、狀態管理、使用者交互 |
---
## 2. Core API 端點(已實測驗證)
詳見 `CORE_API_REFERENCE.md`。以下摘要 People 頁面使用的端點:
| 功能 | Method | 端點 | Response Key | 備註 |
|---|---|---|---|---|
| 列出人物 | GET | `/api/v1/identities` | `identities` | 含 metadata.starred |
| 人物詳情 | GET | `/api/v1/identity/{uuid}` | 頂層 | 含 tmdb_profile 路徑 |
| 搜尋人物 | GET | `/api/v1/identities/search?q=` | `results` | 欄位不同於 list |
| 大頭照 | — | 本地檔案讀取 | — | 非 Core API |
| 人臉列表 | GET | `/api/v1/identity/{uuid}/faces` | `data` | **不是** `faces` |
| 追蹤列表 | GET | `/api/v1/identity/{uuid}/traces` | `traces` | |
| 改名 | PATCH | `/api/v1/identity/{uuid}` | 頂層 | Body: `{"name": "..."}` |
| 刪除 | DELETE | `/api/v1/identity/{uuid}` | — | 204/404 |
| 建立 | POST | `/api/v1/identity` | — | 需要 face_json_path |
| 合併 | POST | `/api/v1/identity/{uuid}/mergeinto` | — | Body: `{"into_uuid": "..."}` |
| 綁定 | POST | `/api/v1/identity/{uuid}/bind` | — | Body: `{"face_id": "string", "file_uuid": "..."}` |
| 解綁 | POST | `/api/v1/identity/{uuid}/unbind` | — | Body: 同上 |
| 候選人臉 | GET | `/api/v1/faces/candidates` | `candidates` | |
| 縮圖 | GET | `/api/v1/file/{uuid}/thumbnail` | Binary | |
| 影片 | GET | `/api/v1/file/{uuid}/video` | Binary | |
---
## 3. Rust Structs 定義
```rust
struct PersonInfo {
identity_uuid: String,
name: String,
starred: bool, // 從 metadata.starred 提取
}
struct FaceInfo {
id: i64,
file_uuid: String,
frame_number: i64,
timestamp_secs: f64,
face_id: Option<String>,
confidence: f64,
}
struct TraceInfo {
trace_id: i64,
file_uuid: String,
frame_count: i64,
first_sec: f64,
last_sec: f64,
avg_confidence: f64,
}
struct FaceCandidate {
id: i64,
face_id: Option<String>,
file_uuid: String,
frame_number: i64,
confidence: f64,
}
struct SearchIdentityResult {
identity_id: i64,
name: String,
source: String,
tmdb_id: Option<i64>,
file_uuid: Option<String>,
start_time: f64,
end_time: f64,
text_content: Option<String>,
}
```
---
## 4. Rust Commands 清單
### 4.1 現有(需修改)
| Command | 修改內容 |
|---|---|
| `get_people` | PersonInfo 加 `starred: bool`,解析 metadata.starred |
| `get_faces` | 解析 `data` key 而非 `faces`FaceInfo 加 timestamp_secs |
| `get_video_stream` | **重構**:寫入 temp file → 回傳 `file://` URL |
### 4.2 現有(不變)
| Command | 行為 |
|---|---|
| `get_traces` | `GET /identity/{uuid}/traces` |
| `get_identity_profile` | 讀本地 profile.jpg → base64 |
| `update_identity_name` | `PATCH /identity/{uuid}` |
| `delete_identity` | `DELETE /identity/{uuid}` |
| `get_thumbnail` | 抓縮圖 → base64 |
### 4.3 新增
| Command | Core API | Body/Params | 回傳 |
|---|---|---|---|
| `search_identities` | `GET /identities/search?q=...` | `query: String, limit: usize` | `Vec<SearchIdentityResult>` |
| `merge_identities` | `POST /identity/{uuid}/mergeinto` | `uuid: String, into_uuid: String` | `()` |
| `bind_face` | `POST /identity/{uuid}/bind` | `uuid, face_id: String, file_uuid: String` | `()` |
| `unbind_face` | `POST /identity/{uuid}/unbind` | `uuid, face_id: String, file_uuid: String` | `()` |
| `get_face_candidates` | `GET /faces/candidates` | `page: usize, per_page: usize` | `Vec<FaceCandidate>` |
**注意create_identity 需要 face_json_path不在 People 頁面直接使用。**
---
## 5. Vue 前端元件樹
```
PeopleView.vue
├── Toolbar
│ ├── h1 "People"
│ ├── 搜尋框 → debounce 300ms → invoke('search_identities')
│ └── 建立按鈕暫留create 需要 face_json_path
├── PeopleGrid
│ └── PersonCard × N (v-for filteredPeople)
│ ├── 大頭照 → invoke('get_identity_profile') cache
│ ├── 名稱
│ ├── ⭐ 星號 (starred)
│ └── @click → selectPerson()
├── DetailModal
│ ├── Header: 大頭照 + 可編輯名稱 + UUID
│ ├── FacesTab (橫向捲軸)
│ │ └── face × N → confidence %
│ ├── TracesTab (縱向列表)
│ │ └── trace × N → 時間 + 信心% → @click → VideoPlayer
│ └── ActionsTab
│ ├── Delete → confirm + invoke('delete_identity')
│ ├── Merge → 搜尋目標 + invoke('merge_identities')
│ ├── Bind Face → 從 Candidates 選取 → invoke('bind_face')
│ └── Unbind Face → invoke('unbind_face')
├── CandidatesModal
│ └── FaceCandidate × N → @click → bind_face()
└── VideoPlayer (共用元件)
└── <video :src="videoSrc"> (file:// URL from temp file)
```
---
## 6. CSS 設計系統
### 色彩
| 用途 | 色碼 |
|---|---|
| Primary | `#2563eb` |
| Secondary | `#64748b` |
| Success | `#22c55e` |
| Warning | `#f59e0b` |
| Error | `#ef4444` |
| Card BG | `#ffffff` |
| Text Primary | `#1e293b` |
| Text Secondary | `#64748b` |
### 元件樣式
| 元件 | 樣式 |
|---|---|
| Grid | `repeat(auto-fill, minmax(100px, 1fr))` gap 16px |
| Card | bg #fff, border 1px #e5e7eb, radius 12px, padding 16px, hover border #4f46e5 |
| Avatar | 56px 圓形, bg #eef2ff, color #4f46e5 |
| Modal | fixed inset:0, bg rgba(0,0,0,.6), centered, max-w 700px, max-h 80vh |
| VideoPlayer | fixed inset:0, bg rgba(0,0,0,.85), video max-w 80vw max-h 60vh, radius 16px |
---
## 7. 資料流
```
事件 Vue 呼叫 Rust Command
───────────────── ────────────── ────────────
頁面載入 invoke('get_people') get_people()
點擊人物 invoke('get_faces') get_faces()
invoke('get_traces') get_traces()
invoke('get_identity_profile') get_identity_profile()
搜尋輸入(debounce) invoke('search_identities') search_identities()
編輯名稱 invoke('update_identity_name') update_identity_name()
刪除人物 invoke('delete_identity') delete_identity()
合併人物 invoke('merge_identities') merge_identities()
綁定人臉 invoke('bind_face') bind_face()
解綁人臉 invoke('unbind_face') unbind_face()
載入候選人臉 invoke('get_face_candidates') get_face_candidates()
播放影片 invoke('get_video_stream') get_video_stream() → temp file
```
---
## 8. 實作順序
1. **Phase 1**: 修改 `PersonInfo` + `FaceInfo` struct + 重構 `get_video_stream` (temp file)
2. **Phase 2**: 新增 `search_identities` + `get_face_candidates`
3. **Phase 3**: 新增 `merge_identities` + `bind_face` + `unbind_face`
4. **Phase 4**: Vue PeopleView 完整 UIgrid + modal + tabs + actions + 搜尋 + 合併 + 綁定)
5. **Phase 5**: 更新 AGENTS.md + 提交