feat: Library 批量注册 + PeopleView 修复 + 国际化

Library:
- 添加 checkbox 和鼠标框选功能
- Unregistered 卡片添加快速注册按钮
- 状态标签移至左下方,人脸图标移至左侧中间
- Deep Scan 改为 unregister -> register -> process
- 完整国际化(日语、繁体中文、英语)

PeopleView:
- 修复卡片不显示问题(使用 get_file_traces API)
- 移除对 identity_id 字段的依赖

其他:
- 添加日语语言包
- 添加 useDragSelect composable
- 新增文档:Face Bbox Null Bug Report
This commit is contained in:
2026-10-01 00:42:13 +08:00
parent 7b60ca3d2d
commit f3d75ebce8
35 changed files with 8054 additions and 961 deletions

View File

@@ -6,9 +6,14 @@ import BuildVersion from './components/BuildVersion.vue'
import { loadUserBehavior } from '@/stores/agentStore'
import { useI18n } from 'vue-i18n'
import { changeLanguage, getCurrentLanguage } from './locales'
import { useAdvancedMode } from '@/stores/advancedMode'
import { useTheme } from '@/stores/themeStore'
const { t } = useI18n()
const router = useRouter()
const { isAdvanced, toggleAdvanced } = useAdvancedMode()
const { theme, toggleTheme } = useTheme()
const appZoom = ref(100)
const loggedInUser = ref(localStorage.getItem('username') || '')
const currentLanguage = ref(getCurrentLanguage())
@@ -16,10 +21,25 @@ const currentLanguage = ref(getCurrentLanguage())
const sidebarWidth = ref(240)
const isDragging = ref(false)
const sidebarCollapsed = ref(true)
const isMobile = ref(window.innerWidth < 768)
const mobileOpen = ref(false)
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
localStorage.setItem('sidebar_collapsed', String(sidebarCollapsed.value))
if (isMobile.value) {
mobileOpen.value = !mobileOpen.value
} else {
sidebarCollapsed.value = !sidebarCollapsed.value
localStorage.setItem('sidebar_collapsed', String(sidebarCollapsed.value))
}
}
function closeMobileDrawer() {
mobileOpen.value = false
}
function handleResize() {
isMobile.value = window.innerWidth < 768
if (!isMobile.value) mobileOpen.value = false
}
function refreshUser() {
@@ -28,6 +48,7 @@ function refreshUser() {
}
function handleLogout() {
if (!confirm('Are you sure you want to logout?')) return
localStorage.removeItem('token')
localStorage.removeItem('username')
localStorage.removeItem('expires_at')
@@ -81,6 +102,7 @@ onMounted(() => {
document.addEventListener('keydown', handleKeydown)
document.addEventListener('mousemove', handleDrag)
document.addEventListener('mouseup', stopDrag)
window.addEventListener('resize', handleResize)
refreshUser()
loadUserBehavior()
@@ -93,18 +115,21 @@ onMounted(() => {
watch(() => router.currentRoute.value.path, () => {
refreshUser()
closeMobileDrawer()
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', stopDrag)
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<BuildVersion />
<div id="app" class="ms-app" :data-current-login="'guest'">
<aside class="ms-side" :class="{ collapsed: sidebarCollapsed }" :style="{ width: sidebarCollapsed ? '62px' : sidebarWidth + 'px', minWidth: sidebarCollapsed ? '62px' : sidebarWidth + 'px' }">
<div id="app" class="ms-app" :class="{ 'ms-app-mobile': isMobile }" :data-current-login="'guest'">
<div v-if="isMobile && mobileOpen" class="ms-overlay" @click="closeMobileDrawer"></div>
<aside class="ms-side" :class="{ collapsed: !isMobile && sidebarCollapsed, 'mobile-open': isMobile && mobileOpen }" :style="isMobile ? {} : { width: sidebarCollapsed ? '62px' : sidebarWidth + 'px', minWidth: sidebarCollapsed ? '62px' : sidebarWidth + 'px' }">
<div class="gs-sidebar-toggle" @click="toggleSidebar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
:style="{ transform: sidebarCollapsed ? 'rotate(0deg)' : 'rotate(180deg)' }">
@@ -129,14 +154,6 @@ onUnmounted(() => {
</svg>
<span class="gs-nav-label">{{ t('nav.library') }}</span>
</router-link>
<router-link to="/face" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="12" cy="10" r="4"/>
<path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/>
</svg>
<span class="gs-nav-label">Face</span>
</router-link>
<router-link to="/admin" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
@@ -153,12 +170,18 @@ onUnmounted(() => {
</nav>
<div class="gs-divider" :class="{ 'gs-hidden': sidebarCollapsed }"></div>
<div class="gs-footer" :class="{ 'gs-hidden': sidebarCollapsed }">
<div class="gs-lang-switcher">
<div class="gs-controls">
<select v-model="currentLanguage" @change="onChangeLanguage" class="gs-lang-select">
<option value="en">English</option>
<option value="zh-TW">繁體中文</option>
<option value="auto">Auto Detect</option>
<option value="en">{{ $t('language.en') }}</option>
<option value="zh-TW">{{ $t('language.zh-TW') }}</option>
<option value="ja">{{ $t('language.ja') }}</option>
<option value="auto">{{ $t('language.auto') }}</option>
</select>
<button class="gs-control-btn" :class="{ 'gs-control-active': isAdvanced }" :title="isAdvanced ? 'Advanced Mode: ON' : 'Advanced Mode: OFF'" @click="toggleAdvanced">
<svg v-if="isAdvanced" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 7.9-2"/></svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
</button>
<button class="gs-control-btn" :title="theme === 'dark' ? 'Switch to Light' : 'Switch to Dark'" @click="toggleTheme">{{ theme === 'dark' ? '☀️' : '🌙' }}</button>
</div>
<div class="gs-account-row">
<template v-if="loggedInUser">
@@ -181,8 +204,40 @@ onUnmounted(() => {
</aside>
<div class="ms-divider" :class="{ 'ms-divider-hidden': sidebarCollapsed }" @mousedown="startDrag"></div>
<main class="ms-main">
<div v-if="isMobile" class="ms-mobile-header">
<button class="ms-mobile-hamburger" @click="toggleSidebar" aria-label="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<span class="ms-mobile-title">Momentry Studio</span>
</div>
<router-view />
</main>
<nav v-if="isMobile" class="ms-bottom-nav">
<router-link to="/search" class="ms-bottom-nav-item" active-class="active" @click="closeMobileDrawer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<span>Search</span>
</router-link>
<router-link to="/library" class="ms-bottom-nav-item" active-class="active" @click="closeMobileDrawer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
<span>Library</span>
</router-link>
<router-link to="/admin" class="ms-bottom-nav-item" active-class="active" @click="closeMobileDrawer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<span>Admin</span>
</router-link>
<router-link to="/client" class="ms-bottom-nav-item" active-class="active" @click="closeMobileDrawer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
<span>Client</span>
</router-link>
<router-link v-if="!loggedInUser" to="/login" class="ms-bottom-nav-item" active-class="active" @click="closeMobileDrawer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
<span>Login</span>
</router-link>
<button v-else class="ms-bottom-nav-item ms-bottom-nav-btn-logout" @click="handleLogout">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
<span>Logout</span>
</button>
</nav>
<AgentTip v-if="loggedInUser" />
</div>
</template>
@@ -223,6 +278,10 @@ html, body { height: 100%; margin: 0; }
.gs-nav-icon { width: 22px; height: 22px; flex-shrink: 0; }
.gs-divider { height: 1px; background: var(--border-light); margin: 14px 8px; flex-shrink: 0; }
.gs-footer { margin-top: auto; padding: 10px 8px; border-radius: 12px; background: var(--hover-background); flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; }
.gs-controls { display: flex; align-items: center; gap: 6px; }
.gs-control-btn { width: 28px; height: 28px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--card-background); cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.15s; color: #888; flex-shrink: 0; filter: grayscale(1); }
.gs-control-btn:hover { background: var(--border-light); border-color: var(--text-secondary); filter: grayscale(0); }
.gs-control-active { color: var(--primary-color); border-color: var(--primary-color); background: var(--primary-color-light, rgba(66,133,244,0.1)); filter: grayscale(0); }
.gs-lang-switcher { width: 100%; }
.gs-lang-select { width: 100%; padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--card-background); font-size: 12px; font-family: inherit; color: var(--text-secondary); cursor: pointer; outline: none; }
.gs-lang-select:focus { border-color: var(--text-primary); }
@@ -234,4 +293,36 @@ html, body { height: 100%; margin: 0; }
.gs-login-link:hover { color: var(--text-primary); }
.ms-main { flex: 1; min-width: 0; background: var(--card-background) !important; border-radius: 18px; padding: 36px 42px; overflow-y: auto; overflow-x: hidden; height: 100%; }
.ms-content { max-width: 1200px; }
/* Mobile overlay */
.ms-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 999; }
/* Mobile header */
.ms-mobile-header { display: flex; align-items: center; gap: 10px; padding-bottom: 12px; }
.ms-mobile-hamburger { background: none; border: none; cursor: pointer; color: var(--text-primary); padding: 4px; }
.ms-mobile-title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
/* Bottom nav */
.ms-bottom-nav { display: flex; position: fixed; bottom: 0; left: 0; right: 0; z-index: 998;
background: var(--card-background); border-top: 1px solid var(--border-light);
justify-content: space-around; padding: 6px 0 env(safe-area-inset-bottom, 6px) 0;
box-shadow: 0 -2px 8px rgba(0,0,0,0.06); }
.ms-bottom-nav-item { display: flex; flex-direction: column; align-items: center; gap: 2px;
text-decoration: none; color: var(--text-secondary); font-size: 10px; padding: 2px 8px; }
.ms-bottom-nav-item.active { color: var(--primary-color); }
.ms-bottom-nav-item svg { width: 20px; height: 20px; }
.ms-bottom-nav-btn-logout { background: none; border: none; cursor: pointer; }
@media (max-width: 767px) {
.ms-app { padding: 0; max-width: 100%; height: 100dvh; }
.ms-app.ms-app-mobile { padding: 0; }
.ms-side { position: fixed; left: 0; top: 0; bottom: 0; z-index: 1000;
transform: translateX(-100%); transition: transform 0.25s ease;
width: 260px !important; min-width: 260px !important; border-radius: 0 18px 18px 0; }
.ms-side.mobile-open { transform: translateX(0); }
.ms-side .gs-sidebar-toggle { display: none; }
.ms-divider { display: none; }
.ms-main { padding: 12px 12px 60px; border-radius: 0; height: 100dvh; }
.gs-footer-compact { display: none !important; }
}
</style>