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

+85 -3
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ImageItem } from '~/types/image'
import type { ImageItem, ImageTag } from '~/types/image'
import { buildImageBbcode } from '~/utils/image-display'
const { uploading, progressItems, uploadFiles } = useImageUpload()
@@ -9,6 +10,9 @@ const {
loadPreferences
} = useUploadPreferences()
const { items: tagItems, fetchTags, batchUpdateTags } = useTags()
const { recordRecent } = useRecentTags()
const { isChecking, isAuthenticated, checkSession, handleAuthError, fetchStatus } = useAuth()
const toast = useToast()
const { t } = useI18n()
@@ -17,6 +21,9 @@ const deletingKeys = ref<Set<string>>(new Set())
const emptySelection = ref(new Set<string>())
const previewOpen = ref(false)
const previewImage = ref<ImageItem | null>(null)
const tagModalOpen = ref(false)
const tagModalImage = ref<ImageItem | null>(null)
const batchTagOpen = ref(false)
const showProgress = computed(
() =>
@@ -47,7 +54,7 @@ onMounted(async () => {
await checkSession()
}
if (isAuthenticated.value) {
await loadPage()
await Promise.all([loadPage(), fetchTags()])
}
})
@@ -66,6 +73,8 @@ function copyFormatLabel() {
return t('copy.url')
case 'html':
return t('copy.html')
case 'bbcode':
return t('copy.bbcode')
default:
return t('copy.markdown')
}
@@ -108,7 +117,9 @@ async function handleUpload(files: File[]) {
? first.url
: copyFormat.value === 'html'
? first.html
: first.markdown
: copyFormat.value === 'bbcode'
? buildImageBbcode(first.url)
: first.markdown
if (text) await copyUploadLink(text)
}
}
@@ -148,6 +159,50 @@ function requestPreviewDelete() {
if (!previewImage.value) return
void handleDelete(previewImage.value.key)
}
function openTagModal(image: ImageItem) {
tagModalImage.value = image
tagModalOpen.value = true
}
function updateSessionItemTags(key: string, tags: ImageItem['tags']) {
sessionItems.value = sessionItems.value.map(item =>
item.key === key ? { ...item, tags } : item
)
if (previewImage.value?.key === key) {
previewImage.value = { ...previewImage.value, tags }
}
if (tagModalImage.value?.key === key) {
tagModalImage.value = { ...tagModalImage.value, tags }
}
}
function handleTagSaved(tags: ImageTag[]) {
if (!tagModalImage.value) return
updateSessionItemTags(tagModalImage.value.key, tags)
}
async function confirmBatchTags(tagIds: number[]) {
const keys = sessionItems.value.map(item => item.key)
if (!keys.length || !tagIds.length) return
try {
await batchUpdateTags(keys, tagIds, 'add')
batchTagOpen.value = false
recordRecent(tagIds)
toast.add({ title: t('tags.batchSuccess'), color: 'success' })
await fetchTags(true)
const addedTags = tagItems.value.filter(tag => tagIds.includes(tag.id))
sessionItems.value = sessionItems.value.map((item) => {
const merged = new Map((item.tags ?? []).map(tag => [tag.id, tag]))
for (const tag of addedTags) merged.set(tag.id, tag)
return { ...item, tags: [...merged.values()] }
})
} catch (error: unknown) {
handleAuthError(error)
toast.add({ title: t('tags.batchFailed'), color: 'error' })
}
}
</script>
<template>
@@ -189,6 +244,14 @@ function requestPreviewDelete() {
{{ t('upload.sessionTitle') }}
<span class="ml-1 text-sm font-normal text-muted">({{ sessionItems.length }})</span>
</h2>
<UButton
icon="i-lucide-tags"
variant="outline"
color="primary"
size="sm"
:label="t('tags.batchTagAll')"
@click="() => { batchTagOpen = true }"
/>
</div>
<ImageGrid
@@ -196,9 +259,13 @@ function requestPreviewDelete() {
:selected-keys="emptySelection"
:selectable="false"
:show-key="false"
:allow-delete="true"
show-tag-action
:empty-text="t('upload.sessionEmpty')"
@update:selected-keys="() => {}"
@preview="openPreview"
@delete="(image) => handleDelete(image.key)"
@edit-tags="openTagModal"
/>
</section>
@@ -209,6 +276,21 @@ function requestPreviewDelete() {
:allow-delete="true"
@delete="requestPreviewDelete"
/>
<ImageTagModal
v-model:open="tagModalOpen"
:image="tagModalImage"
:tags="tagItems"
@saved="handleTagSaved"
/>
<BatchTagModal
v-model:open="batchTagOpen"
:tags="tagItems"
:selected-count="sessionItems.length"
add-only
@confirm="(tagIds) => confirmBatchTags(tagIds)"
/>
</AppShell>
</div>
</template>