release: v1.4.0 — 标签系统、操作日志扩展与文档截图更新
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s

- 每用户标签与彩色打标,图库 OR/AND 筛选与批量打标
- 操作日志记录登录、删除、设置与标签操作
- API 标签/打标接口;README 与文档站截图同步
This commit is contained in:
O96u committed 2026-09-09 14:46:52 +08:00
1 parent f17d79b236
commit 737235acff
110 files changed
+5881 -778

No files matched your search

+112 -3
View File
@@ -46,9 +46,17 @@ const {
activeUploadSource,
setActiveUploadSource,
resetFilters,
loadStorageBackendOptions
loadStorageBackendOptions,
activeTagIds,
activeTagMode,
activeUntaggedOnly,
setActiveTagFilter,
toggleTagFilter,
listQueryParams
} = useImageList()
const { items: tagItems, fetchTags, batchUpdateTags } = useTags()
const { isChecking, isAuthenticated, checkSession, handleAuthError, fetchStatus } = useAuth()
const toast = useToast()
const { t } = useI18n()
@@ -66,6 +74,7 @@ const showScrollTop = ref(false)
const previewOpen = ref(false)
const previewImage = ref<ImageItem | null>(null)
const viewMode = ref<'grid' | 'list'>('grid')
const batchTagOpen = ref(false)
const currentUploadSource = ref('all')
const currentStorageBackend = ref('all')
@@ -96,7 +105,8 @@ function scrollToTop() {
async function fetchStats() {
stats.value = await $fetch<StatsResponse>('/api/stats', {
credentials: 'include'
credentials: 'include',
query: listQueryParams()
})
}
@@ -131,7 +141,8 @@ async function refreshAll() {
await Promise.all([
fetchStats(),
reloadGallery(),
loadStorageBackendOptions()
loadStorageBackendOptions(),
fetchTags()
])
currentUploadSource.value = activeUploadSource.value
currentStorageBackend.value = activeStorageBackend.value
@@ -174,11 +185,85 @@ async function handleResetFilters() {
currentStorageBackend.value = 'all'
try {
await resetFilters()
await fetchStats()
} catch (error: unknown) {
handleAuthError(error)
}
}
async function handleTagIdsChange(tagIds: number[]) {
if (!isAuthenticated.value) return
selectedKeys.value = new Set()
try {
await setActiveTagFilter({ tagIds, untaggedOnly: false })
await fetchStats()
} catch (error: unknown) {
handleAuthError(error)
}
}
async function handleTagModeChange(tagMode: 'or' | 'and') {
if (!isAuthenticated.value) return
try {
await setActiveTagFilter({ tagMode })
await fetchStats()
} catch (error: unknown) {
handleAuthError(error)
}
}
async function handleUntaggedOnlyChange(untaggedOnly: boolean) {
if (!isAuthenticated.value) return
selectedKeys.value = new Set()
try {
await setActiveTagFilter({ untaggedOnly, tagIds: [] })
await fetchStats()
} catch (error: unknown) {
handleAuthError(error)
}
}
async function handleTagFilterClick(tagId: number) {
if (!isAuthenticated.value) return
selectedKeys.value = new Set()
try {
await toggleTagFilter(tagId)
await fetchStats()
} catch (error: unknown) {
handleAuthError(error)
}
}
async function handleUntaggedFilterClick() {
if (!isAuthenticated.value) return
selectedKeys.value = new Set()
try {
await setActiveTagFilter({ untaggedOnly: true, tagIds: [] })
await fetchStats()
} catch (error: unknown) {
handleAuthError(error)
}
}
async function confirmBatchTags(tagIds: number[], action: 'add' | 'remove') {
const keys = Array.from(selectedKeys.value)
if (!keys.length || !tagIds.length) return
try {
await batchUpdateTags(keys, tagIds, action)
batchTagOpen.value = false
selectedKeys.value = new Set()
toast.add({ title: t('tags.batchSuccess'), color: 'success' })
await Promise.all([reloadGallery(), fetchStats(), fetchTags(true)])
} catch (error: unknown) {
handleAuthError(error)
toast.add({ title: t('tags.batchFailed'), color: 'error' })
}
}
function goToTagSettings() {
void navigateTo({ path: '/settings', query: { tab: 'tags' } })
}
function updateSelectedKeys(keys: Set<string>) {
selectedKeys.value = keys
}
@@ -398,16 +483,26 @@ watch(isAuthenticated, async (authed, prev) => {
:storage-backend-items="storageBackendItems"
:upload-source="currentUploadSource"
:upload-source-items="uploadSourceItems"
:tag-ids="activeTagIds"
:tag-mode="activeTagMode"
:untagged-only="activeUntaggedOnly"
:tags="tagItems"
:view-mode="viewMode"
:selected-count="selectedCount"
:loading="galleryLoading"
:show-batch-delete="true"
:show-batch-tags="selectedCount > 0"
@update:storage-backend="handleStorageBackendChange"
@update:upload-source="handleUploadSourceChange"
@update:tag-ids="handleTagIdsChange"
@update:tag-mode="handleTagModeChange"
@update:untagged-only="handleUntaggedOnlyChange"
@update:view-mode="viewMode = $event"
@search="handleSearch"
@reset="handleResetFilters"
@batch-delete="requestBatchDelete"
@batch-tags="batchTagOpen = true"
@manage-tags="goToTagSettings"
/>
</div>
@@ -429,6 +524,7 @@ watch(isAuthenticated, async (authed, prev) => {
gallery
:show-key="false"
show-storage
show-tags
:items="items"
:selected-keys="selectedKeys"
:selectable="true"
@@ -436,11 +532,14 @@ watch(isAuthenticated, async (authed, prev) => {
@update:selected-keys="updateSelectedKeys"
@preview="openPreview"
@delete="(image) => requestDelete(image.key)"
@tag-click="handleTagFilterClick"
@untagged-click="handleUntaggedFilterClick"
/>
<GalleryImageList
v-else
show-storage
show-tags
:items="items"
:selected-keys="selectedKeys"
:selectable="true"
@@ -448,12 +547,15 @@ watch(isAuthenticated, async (authed, prev) => {
@update:selected-keys="updateSelectedKeys"
@preview="openPreview"
@delete="(image) => requestDelete(image.key)"
@tag-click="handleTagFilterClick"
@untagged-click="handleUntaggedFilterClick"
/>
<ImagePreviewModal
v-model:open="previewOpen"
:image="previewImage"
show-storage
show-tags
:deleting="previewImage ? deletingKeys.has(previewImage.key) : false"
:allow-delete="true"
@delete="requestPreviewDelete"
@@ -479,6 +581,13 @@ watch(isAuthenticated, async (authed, prev) => {
@confirm="confirmDelete"
/>
<BatchTagModal
v-model:open="batchTagOpen"
:tags="tagItems"
:selected-count="selectedCount"
@confirm="confirmBatchTags"
/>
<UButton
v-show="showScrollTop"
icon="i-lucide-arrow-up"