批量打标签
This commit is contained in:
1 parent
92aaf6fbb8
commit
5c1f640a2b
3 files changed
+59
-11
No files matched your search
@@ -180,6 +180,11 @@ const presetLabel = computed(() => {
|
|||||||
</UBadge>
|
</UBadge>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<TagBadgeList
|
||||||
|
:tags="item.tags"
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
|
||||||
<UProgress
|
<UProgress
|
||||||
v-if="running"
|
v-if="running"
|
||||||
:model-value="item.transcodeProgress ?? 0"
|
:model-value="item.transcodeProgress ?? 0"
|
||||||
|
|||||||
@@ -21,11 +21,15 @@ const {
|
|||||||
loadActiveTranscodes,
|
loadActiveTranscodes,
|
||||||
uploadFiles
|
uploadFiles
|
||||||
} = useMediaUpload(props.kind)
|
} = useMediaUpload(props.kind)
|
||||||
|
const { items: tagItems, fetchTags, batchUpdateTags } = useTags()
|
||||||
|
const { recordRecent } = useRecentTags()
|
||||||
|
const batchTagOpen = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
loadActiveTranscodes(),
|
loadActiveTranscodes(),
|
||||||
loadUploadPolicy().catch(() => {})
|
loadUploadPolicy().catch(() => {}),
|
||||||
|
fetchTags()
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -89,6 +93,27 @@ async function removeItem(item: FileItem) {
|
|||||||
toast.add({ title: t('files.deleteFailed'), color: 'error' })
|
toast.add({ title: t('files.deleteFailed'), color: 'error' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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()] }
|
||||||
|
})
|
||||||
|
toast.add({ title: t('tags.batchSuccess'), color: 'success' })
|
||||||
|
} catch {
|
||||||
|
toast.add({ title: t('tags.batchFailed'), color: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -112,10 +137,20 @@ async function removeItem(item: FileItem) {
|
|||||||
v-if="sessionItems.length"
|
v-if="sessionItems.length"
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-medium">
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
{{ t('upload.sessionTitle') }}
|
<h2 class="text-lg font-medium">
|
||||||
<span class="ml-1 text-sm font-normal text-muted">({{ sessionItems.length }})</span>
|
{{ t('upload.sessionTitle') }}
|
||||||
</h2>
|
<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>
|
||||||
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
<MediaSessionCard
|
<MediaSessionCard
|
||||||
@@ -129,5 +164,13 @@ async function removeItem(item: FileItem) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<BatchTagModal
|
||||||
|
v-model:open="batchTagOpen"
|
||||||
|
:tags="tagItems"
|
||||||
|
:selected-count="sessionItems.length"
|
||||||
|
add-only
|
||||||
|
@confirm="confirmBatchTags"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,7 +46,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const action = body?.action === 'remove' ? 'remove' : 'add'
|
const action = body?.action === 'remove' ? 'remove' : 'add'
|
||||||
|
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '请提供图片列表', 400)
|
createApiError(event, 'INVALID_REQUEST', '请提供媒体列表', 400)
|
||||||
}
|
}
|
||||||
if (!tagIds.length) {
|
if (!tagIds.length) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '请提供标签列表', 400)
|
createApiError(event, 'INVALID_REQUEST', '请提供标签列表', 400)
|
||||||
@@ -55,7 +55,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
createApiError(
|
createApiError(
|
||||||
event,
|
event,
|
||||||
'INVALID_REQUEST',
|
'INVALID_REQUEST',
|
||||||
`每次最多处理 ${MAX_DELETE_BATCH} 张图片`,
|
`每次最多处理 ${MAX_DELETE_BATCH} 个媒体`,
|
||||||
400
|
400
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
validateKeys(keys)
|
validateKeys(keys)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message.startsWith('INVALID_KEY:')) {
|
if (error instanceof Error && error.message.startsWith('INVALID_KEY:')) {
|
||||||
createApiError(event, 'INVALID_IMAGE_KEY', `无效的图片路径: ${error.message.slice(12)}`, 400)
|
createApiError(event, 'INVALID_IMAGE_KEY', `无效的媒体路径: ${error.message.slice(12)}`, 400)
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -87,10 +87,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
logActivity(event, {
|
logActivity(event, {
|
||||||
action: 'tags',
|
action: 'tags',
|
||||||
key: 'images/batch-tags',
|
key: 'media/batch-tags',
|
||||||
originalName: action === 'add'
|
originalName: action === 'add'
|
||||||
? `批量打标(${keys.length} 张)`
|
? `批量打标(${keys.length} 个媒体)`
|
||||||
: `批量移除标签(${keys.length} 张)`,
|
: `批量移除标签(${keys.length} 个媒体)`,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user