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

@@ -2,19 +2,19 @@
<div id="ms-view-client" class="client-layout">
<div class="client-header">
<div class="header-top-row">
<h1>File Manager</h1>
<h1>{{ t('client.title') }}</h1>
<div class="header-buttons">
<button @click="goUp" :disabled="currentPath === '/'">Up</button>
<button @click="showCreateFolderDialog">New Folder</button>
<button @click="showUploadDialog">Upload</button>
<button @click="confirmDelete" :disabled="!selectedFile">Delete</button>
<button @click="showMoveDialog" :disabled="!selectedFile">Move</button>
<button @click="refreshFiles">Refresh</button>
<button @click="showCreateFolderDialog">{{ t('client.button.newFolder') }}</button>
<button @click="showUploadDialog">{{ t('client.button.upload') }}</button>
<button @click="confirmDelete" :disabled="!selectedFile">{{ t('client.button.delete') }}</button>
<button @click="showMoveDialog" :disabled="!selectedFile">{{ t('client.button.move') }}</button>
<button @click="refreshFiles">{{ t('client.button.refresh') }}</button>
</div>
</div>
<div class="header-path-row">Path: {{ currentPath }}</div>
<div class="client-search">
<input v-model="searchQuery" type="text" placeholder="Search files..." @input="onSearch" />
<input v-model="searchQuery" type="text" :placeholder="t('client.search.placeholder')" @input="onSearch" />
</div>
</div>
@@ -24,10 +24,10 @@
<table v-else class="file-table">
<thead>
<tr>
<th @click="sortBy('name')" class="sortable">Name <span v-if="sortField==='name'">{{ sortAsc ? '▲' : '▼' }}</span></th>
<th @click="sortBy('size')" class="sortable">Size <span v-if="sortField==='size'">{{ sortAsc ? '▲' : '▼' }}</span></th>
<th>Type</th>
<th>Modified</th>
<th @click="sortBy('name')" class="sortable">{{ t('client.table.name') }} <span v-if="sortField==='name'">{{ sortAsc ? '▲' : '▼' }}</span></th>
<th @click="sortBy('size')" class="sortable">{{ t('client.table.size') }} <span v-if="sortField==='size'">{{ sortAsc ? '▲' : '▼' }}</span></th>
<th>{{ t('client.table.type') }}</th>
<th>{{ t('client.table.modified') }}</th>
</tr>
</thead>
<tbody>
@@ -78,11 +78,11 @@
<!-- Create Folder Dialog -->
<div v-if="showCreateFolder" class="dialog-overlay" @click="hideCreateFolderDialog">
<div class="dialog" @click.stop>
<h3>Create New Folder</h3>
<input v-model="newFolderName" type="text" placeholder="Folder name" @keyup.enter="createFolder" />
<h3>{{ t('client.dialog.createFolder') }}</h3>
<input v-model="newFolderName" type="text" :placeholder="t('client.dialog.folderName')" @keyup.enter="createFolder" />
<div class="dialog-buttons">
<button @click="createFolder">Create</button>
<button @click="hideCreateFolderDialog">Cancel</button>
<button @click="createFolder">{{ t('client.button.create') }}</button>
<button @click="hideCreateFolderDialog">{{ t('client.button.cancel') }}</button>
</div>
</div>
</div>
@@ -90,14 +90,14 @@
<!-- Delete Confirm Dialog -->
<div v-if="showDeleteConfirm" class="dialog-overlay" @click="cancelDelete">
<div class="dialog" @click.stop>
<h3>Delete "{{ deleteTarget }}"</h3>
<h3>{{ t('client.dialog.deleteTitle', { name: deleteTarget }) }}</h3>
<p v-if="selectedFile?.isDir && selectedFile.fileCount">
This folder contains <strong>{{ selectedFile.fileCount }}</strong> file(s). They will be permanently deleted.
</p>
<p v-else>Are you sure you want to delete this item?</p>
<p v-else>{{ t('client.dialog.deleteMessage', { type: selectedFile?.isDir ? t('client.type.folder') : t('client.type.file') }) }}</p>
<div class="dialog-buttons">
<button @click="doDelete" class="danger-btn">Delete</button>
<button @click="cancelDelete">Cancel</button>
<button @click="doDelete" class="danger-btn">{{ t('client.button.delete') }}</button>
<button @click="cancelDelete">{{ t('client.button.cancel') }}</button>
</div>
</div>
</div>
@@ -105,11 +105,11 @@
<!-- Rename Dialog -->
<div v-if="showRename" class="dialog-overlay" @click="hideRenameDialog">
<div class="dialog" @click.stop>
<h3>Rename "{{ renameOldName }}"</h3>
<input v-model="renameNewName" type="text" placeholder="New name" @keyup.enter="doRename" autofocus />
<h3>{{ t('client.dialog.renameTitle', { name: renameOldName }) }}</h3>
<input v-model="renameNewName" type="text" :placeholder="t('client.dialog.newName')" @keyup.enter="doRename" autofocus />
<div class="dialog-buttons">
<button @click="doRename">Rename</button>
<button @click="hideRenameDialog">Cancel</button>
<button @click="doRename">{{ t('client.button.rename') }}</button>
<button @click="hideRenameDialog">{{ t('client.button.cancel') }}</button>
</div>
</div>
</div>
@@ -117,7 +117,7 @@
<!-- File Preview Dialog -->
<div v-if="showPreview" class="dialog-overlay" @click="hidePreview">
<div class="dialog preview-dialog" @click.stop>
<h3>File Preview: {{ selectedFile?.name }}</h3>
<h3>{{ t('client.dialog.previewTitle', { name: selectedFile?.name }) }}</h3>
<div v-if="previewLoading" class="loading">Loading preview...</div>
<div v-else-if="previewContent" class="preview-content">
<img v-if="isImage" :src="previewContent" alt="preview" />
@@ -131,8 +131,8 @@
<pre v-else>{{ previewContent }}</pre>
</div>
<div class="dialog-buttons">
<button @click="downloadFile">Download</button>
<button @click="hidePreview">Close</button>
<button @click="downloadFile">{{ t('client.button.download') }}</button>
<button @click="hidePreview">{{ t('common.close') }}</button>
</div>
</div>
</div>
@@ -140,7 +140,7 @@
<!-- Upload Dialog -->
<div v-if="showUpload" class="dialog-overlay" @click="hideUploadDialog">
<div class="dialog upload-dialog" @click.stop>
<h3>Upload File{{ uploadFiles.length > 1 ? 's' : '' }}</h3>
<h3>{{ t('client.dialog.uploadTitle', { s: uploadFiles.length > 1 ? 's' : '' }) }}</h3>
<input ref="fileInput" type="file" multiple @change="selectUploadFile" />
<div v-if="uploadFiles.length > 0" class="upload-info">
<p v-if="!uploading">{{ uploadFiles.length }} file(s) selected</p>
@@ -155,9 +155,9 @@
</div>
<div class="dialog-buttons">
<button @click="uploadFileToServer" :disabled="uploadFiles.length === 0 || uploading">
{{ uploading ? 'Uploading...' : 'Upload' }}
{{ uploading ? t('client.status.uploading') : t('client.button.upload') }}
</button>
<button @click="hideUploadDialog">Cancel</button>
<button @click="hideUploadDialog">{{ t('client.button.cancel') }}</button>
</div>
</div>
</div>
@@ -165,7 +165,7 @@
<!-- Move Dialog -->
<div v-if="showMove" class="dialog-overlay" @click="showMove = false">
<div class="dialog" @click.stop>
<h3>{{ selectedFile?.isDir ? 'Move Folder' : 'Move File' }} "{{ selectedFile?.name }}"</h3>
<h3>{{ t('client.dialog.moveTitle', { type: selectedFile?.isDir ? t('client.type.folder') : t('client.type.file'), name: selectedFile?.name }) }}</h3>
<select v-model="moveTarget" class="move-select">
<option value="" disabled>Select a folder...</option>
<option v-for="folder in availableFolders" :key="folder.name" :value="folder.name">
@@ -173,8 +173,8 @@
</option>
</select>
<div class="dialog-buttons">
<button @click="doMove" :disabled="!moveTarget">Move</button>
<button @click="showMove = false">Cancel</button>
<button @click="doMove" :disabled="!moveTarget">{{ t('client.button.move') }}</button>
<button @click="showMove = false">{{ t('client.button.cancel') }}</button>
</div>
</div>
</div>
@@ -183,6 +183,9 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const currentPath = ref('/')
const files = ref<any[]>([])
@@ -1305,4 +1308,9 @@ onMounted(() => {
font-size: 12px;
color: var(--text-secondary);
}
@media (max-width: 767px) {
table { display: block; overflow-x: auto; white-space: nowrap; }
.client-view { padding: 8px; }
}
</style>