feat(admin): batch ops, transfer, watermark, usage and qr share pages
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
<!-- 照片管理(listAlbumPhotos):全状态可见 + uploader 脱敏 + 可见性/分类过滤。
|
||||
单条操作:隐藏(published→hidden)/恢复发布(hidden→published)/软删除;
|
||||
批量隐藏属 P4a,此处仅单条。操作结果按条回显(行内状态即时更新 + 消息)。 -->
|
||||
单条操作:隐藏/恢复发布/软删除(行内回显);P4a Task 9 增量:
|
||||
· 多选(仅 published 可选——批量隐藏的状态机前置)+ 批量隐藏(逐项结果
|
||||
toast 汇总 成功x/失败y + 失败详情弹窗逐条错误码本地化;成功行原位
|
||||
置 hidden,失败行不动——A16 部分成功语义)
|
||||
· likeCount 副本列(P2 PhotoRecord 回传;相册级 statsSyncedAt 在详情
|
||||
信息卡展示,照片行无单行同步时间)。 -->
|
||||
<template>
|
||||
<div data-test="photos-panel">
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
@@ -32,13 +36,31 @@
|
||||
:label="categoryLabel(category)"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElButton
|
||||
type="warning"
|
||||
plain
|
||||
:disabled="selectedPublished.length === 0"
|
||||
:loading="batchHiding"
|
||||
data-test="photo-batch-hide-btn"
|
||||
@click="batchHide"
|
||||
>
|
||||
{{ t('albumsManage.photos.batchHide') }}
|
||||
<span v-if="selectedPublished.length" class="ml-1">({{ selectedPublished.length }})</span>
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElAlert v-if="error && !items.length" type="error" :closable="false">
|
||||
{{ error.message }}
|
||||
</ElAlert>
|
||||
|
||||
<ElTable v-else v-loading="loading" :data="items" data-test="photo-list-table">
|
||||
<ElTable
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="items"
|
||||
data-test="photo-list-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="44" :selectable="batchSelectable" />
|
||||
<ElTableColumn :label="t('albumsManage.photos.colState')" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="stateTagType(row.processingState)" data-test="photo-state-tag">
|
||||
@@ -70,6 +92,11 @@
|
||||
<span>{{ row.uploader.displayName || row.uploader.account }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="t('albumsManage.photos.colLikeCount')" width="90">
|
||||
<template #default="{ row }">
|
||||
<span class="text-gray-500" data-test="photo-like-count">{{ row.likeCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="t('albumsManage.photos.colUploadedAt')" width="160">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDateTime(row.uploadedAt) }}</span>
|
||||
@@ -116,16 +143,59 @@
|
||||
{{ $t('albumsManage.list.loadMore') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- 批量隐藏失败详情(逐条错误码本地化;成功行已原位回显) -->
|
||||
<ElDialog
|
||||
v-model="failuresVisible"
|
||||
:title="t('albumsManage.photos.batchFailuresTitle')"
|
||||
width="480px"
|
||||
data-test="photo-batch-failures-dialog"
|
||||
>
|
||||
<ElTable :data="batchSummary?.failures ?? []" size="small">
|
||||
<ElTableColumn :label="t('albumsManage.photos.batchColPhoto')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono text-xs break-all">{{ row.photoId }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="t('albumsManage.photos.batchColReason')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<ElTag type="danger" effect="plain" size="small">
|
||||
{{ localizeBatchHideCode(t, row.code) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<template #footer>
|
||||
<ElButton
|
||||
type="primary"
|
||||
data-test="photo-batch-failures-close"
|
||||
@click="failuresVisible = false"
|
||||
>
|
||||
{{ t('common.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { fetchAlbumPhotos, fetchDeletePhoto, fetchUpdatePhotoVisibility } from '@/api/albums'
|
||||
import {
|
||||
fetchAlbumPhotos,
|
||||
fetchDeletePhoto,
|
||||
fetchHidePhotosBatch,
|
||||
fetchUpdatePhotoVisibility
|
||||
} from '@/api/albums'
|
||||
import { useCursorList } from '@/views/albums/composables/useCursorList'
|
||||
import { formatBytes, formatDateTime } from '@/utils/albums/format'
|
||||
import {
|
||||
indexOfPhotoId,
|
||||
localizeBatchHideCode,
|
||||
summarizeBatchHideResults,
|
||||
type BatchHideSummary
|
||||
} from '@/utils/albums/batchHide'
|
||||
|
||||
const props = defineProps<{
|
||||
album: Api.Album.Detail
|
||||
@@ -245,4 +315,69 @@
|
||||
}
|
||||
ElMessage.success(t('albumsManage.photos.deleted'))
|
||||
}
|
||||
|
||||
// ── 批量隐藏(P4a A16:逐项结果部分成功) ──
|
||||
|
||||
/** 仅 published 可选(批量隐藏的目标态前置;其余状态行勾选禁用) */
|
||||
function batchSelectable(row: Api.Album.AdminPhoto): boolean {
|
||||
return row.visibility === 'published'
|
||||
}
|
||||
|
||||
const selection = ref<Api.Album.AdminPhoto[]>([])
|
||||
|
||||
function onSelectionChange(rows: Api.Album.AdminPhoto[]) {
|
||||
selection.value = rows
|
||||
}
|
||||
|
||||
/** selection 经 selectable 约束本就全为 published;过滤一层兜底 */
|
||||
const selectedPublished = computed(() =>
|
||||
selection.value.filter((row) => row.visibility === 'published')
|
||||
)
|
||||
|
||||
const batchHiding = ref(false)
|
||||
const failuresVisible = ref(false)
|
||||
const batchSummary = ref<BatchHideSummary | null>(null)
|
||||
|
||||
async function batchHide() {
|
||||
const targets = selectedPublished.value
|
||||
if (targets.length === 0) return
|
||||
if (!(await confirmBox(t('albumsManage.photos.batchHideConfirm', { count: targets.length })))) {
|
||||
return
|
||||
}
|
||||
batchHiding.value = true
|
||||
try {
|
||||
const result = await fetchHidePhotosBatch(
|
||||
props.album.id,
|
||||
targets.map((row) => row.photoId)
|
||||
)
|
||||
const summary = summarizeBatchHideResults(result.results)
|
||||
batchSummary.value = summary
|
||||
// 成功项原位置 hidden(结果与请求同序;失败行保持原样);过滤条件下
|
||||
// 与目标态不符则移除
|
||||
for (const item of result.results) {
|
||||
if (!item.ok) continue
|
||||
const index = indexOfPhotoId(items.value, item.photoId)
|
||||
if (index < 0) continue
|
||||
const row = items.value[index]!
|
||||
if (visibilityFilter.value !== '' && visibilityFilter.value !== 'hidden') {
|
||||
list.removeItem(index)
|
||||
} else {
|
||||
list.replaceItem(index, { ...row, visibility: 'hidden' as const })
|
||||
}
|
||||
}
|
||||
if (summary.failCount > 0) {
|
||||
failuresVisible.value = true
|
||||
ElMessage.warning(
|
||||
t('albumsManage.photos.batchResult', {
|
||||
ok: summary.okCount,
|
||||
fail: summary.failCount
|
||||
})
|
||||
)
|
||||
} else {
|
||||
ElMessage.success(t('albumsManage.photos.batchResultAllOk', { ok: summary.okCount }))
|
||||
}
|
||||
} finally {
|
||||
batchHiding.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user