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
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:
1 parent
f17d79b236
commit
737235acff
110 files changed
+5881
-778
No files matched your search
+112
-3
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user