Files
photo-live/apps/admin/src/views/albums/detail/index.vue
T
2026-09-20 21:10:30 +08:00

258 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 相册详情:基础信息 + banner 上传 + 照片/成员/分类管理页签(P1b Task 10)。
路由 /albums/detail/:albumId(isHide,不进菜单)。 -->
<template>
<div class="p-5">
<ElAlert v-if="error" type="error" :closable="false" data-test="album-detail-error">
<div class="flex items-center gap-2">
<span>{{ $t('albumsManage.detail.loadFailed') }}:{{ error.message }}</span>
<ElButton size="small" data-test="album-detail-retry" @click="load">
{{ $t('albumsManage.detail.retry') }}
</ElButton>
</div>
</ElAlert>
<ElCard v-else v-loading="loading" :data-test="'album-detail-card'">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<ElButton link @click="router.back()" data-test="album-detail-back">
<ElIcon><ArrowLeft /></ElIcon>
</ElButton>
<span class="font-medium">{{ titleOf(album) }}</span>
<ElTag size="small" type="info">{{ album?.defaultLocale }}</ElTag>
</div>
<div class="flex items-center gap-2">
<ElButton plain data-test="album-detail-transfer" @click="transferVisible = true">
{{ $t('albumsManage.transfer.entry') }}
</ElButton>
<ElButton
type="primary"
plain
data-test="album-detail-edit"
@click="editVisible = true"
>
{{ $t('albumsManage.detail.edit') }}
</ElButton>
</div>
</div>
</template>
<ElDescriptions
v-if="album"
:column="3"
border
:title="$t('albumsManage.detail.basicInfo')"
data-test="album-detail-desc"
>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldTitle')">
{{ titleOf(album) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldToken')">
<span class="flex items-center gap-1">
<span data-test="album-detail-token">{{ album.publicToken }}</span>
<ElButton link type="primary" size="small" @click="copyToken(album.publicToken)">
<ElIcon><CopyDocument /></ElIcon>
</ElButton>
</span>
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldStatus')">
<ElTag :type="album.status === 'active' ? 'success' : 'info'">
{{
album.status === 'active'
? $t('albumsManage.list.statusActive')
: $t('albumsManage.list.statusOffline')
}}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.list.colSwitches')">
<span class="flex flex-wrap gap-1">
<ElTag
v-for="sw in switchTags(album)"
:key="sw.key"
size="small"
:type="sw.on ? 'success' : 'info'"
:effect="sw.on ? 'light' : 'plain'"
>
{{ sw.label }} {{ sw.on ? $t('albumsManage.list.on') : $t('albumsManage.list.off') }}
</ElTag>
</span>
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldTimezone')">
{{ album.timezone }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldStartAt')">
{{ formatDateTime(album.startAt) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldEndAt')">
{{ album.endAt ? formatDateTime(album.endAt) : $t('albumsManage.list.endAtUndecided') }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldCreatedAt')">
{{ formatDateTime(album.createdAt) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldUpdatedAt')">
{{ formatDateTime(album.updatedAt) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldStatsSyncedAt')">
<span data-test="album-detail-stats-synced-at">
{{
album.statsSyncedAt
? formatDateTime(album.statsSyncedAt)
: $t('albumsManage.detail.statsNotSynced')
}}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('albumsManage.detail.fieldOwnerId')">
<span data-test="album-detail-owner-id">{{ album.ownerId }}</span>
</ElDescriptionsItem>
</ElDescriptions>
<div v-if="album" class="mt-5">
<AlbumBannerUpload :album-id="albumId" />
</div>
<ElTabs v-if="album" v-model="activeTab" class="mt-4" data-test="album-detail-tabs">
<!-- 页签头由 ElTabs 自动生成(pane 上的 attr 只落在隐藏面板上):
data-test 经 #label 插槽挂到头部文案,供 e2e 点击 -->
<ElTabPane name="photos">
<template #label>
<span data-test="album-photos-tab">{{ $t('albumsManage.detail.tabPhotos') }}</span>
</template>
<AlbumPhotos :album="album" />
</ElTabPane>
<ElTabPane name="members">
<template #label>
<span data-test="album-members-tab">{{ $t('albumsManage.detail.tabMembers') }}</span>
</template>
<AlbumMembers :album="album" />
</ElTabPane>
<ElTabPane name="categories">
<template #label>
<span data-test="album-categories-tab">
{{ $t('albumsManage.detail.tabCategories') }}
</span>
</template>
<AlbumCategories :album="album" @changed="load" />
</ElTabPane>
<ElTabPane name="watermark">
<template #label>
<span data-test="album-watermark-tab">
{{ $t('albumsManage.detail.tabWatermark') }}
</span>
</template>
<AlbumWatermark :album="album" />
</ElTabPane>
<ElTabPane name="share">
<template #label>
<span data-test="album-share-tab">{{ $t('albumsManage.detail.tabShare') }}</span>
</template>
<AlbumShareCard :album="album" />
</ElTabPane>
</ElTabs>
</ElCard>
<AlbumFormDialog v-if="album" v-model="editVisible" :album="album" @saved="onAlbumSaved" />
<AlbumTransferDialog
v-if="album"
v-model="transferVisible"
:album="album"
@saved="onTransferred"
/>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ArrowLeft, CopyDocument } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import AlbumFormDialog from '@/views/albums/manage/AlbumFormDialog.vue'
import AlbumBannerUpload from './AlbumBannerUpload.vue'
import AlbumMembers from './AlbumMembers.vue'
import AlbumCategories from './AlbumCategories.vue'
import AlbumPhotos from './AlbumPhotos.vue'
import AlbumWatermark from './AlbumWatermark.vue'
import AlbumShareCard from './AlbumShareCard.vue'
import AlbumTransferDialog from './AlbumTransferDialog.vue'
import { fetchAlbum } from '@/api/albums'
import { formatDateTime } from '@/utils/albums/format'
defineOptions({ name: 'AlbumsDetail' })
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const albumId = computed(() => String(route.params.albumId ?? ''))
const album = ref<Api.Album.Detail | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const editVisible = ref(false)
const transferVisible = ref(false)
const activeTab = ref('photos')
async function load() {
if (!albumId.value) return
loading.value = true
error.value = null
try {
album.value = await fetchAlbum(albumId.value)
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
function onAlbumSaved(saved: Api.Album.Detail) {
album.value = saved
}
/** 交接成功:回包 ownerId 已变更,原位替换相册详情 */
function onTransferred(transferred: Api.Album.Detail) {
album.value = transferred
}
function titleOf(target: Api.Album.Detail | null): string {
if (!target) return ''
const preferred = target.translations.find((item) => item.locale === target.defaultLocale)
return preferred?.title ?? target.translations[0]?.title ?? target.id
}
function switchTags(target: Api.Album.Detail) {
return [
{ key: 'upload', on: target.uploadEnabled, label: t('albumsManage.list.switchUpload') },
{ key: 'public', on: target.publicEnabled, label: t('albumsManage.list.switchPublic') },
{ key: 'download', on: target.downloadEnabled, label: t('albumsManage.list.switchDownload') },
{
key: 'original',
on: target.originalDownloadEnabled,
label: t('albumsManage.list.switchOriginal')
}
]
}
async function copyToken(token: string) {
await navigator.clipboard.writeText(token)
ElMessage.success(t('albumsManage.list.copied'))
}
/** 列表「成员」入口带 #members 锚点:直达成员页签 */
watch(
() => route.hash,
(hash) => {
if (hash === '#members') activeTab.value = 'members'
},
{ immediate: true }
)
watch(albumId, () => {
if (route.name === 'AlbumsDetail') load()
})
onMounted(() => {
load()
})
</script>