Files
photo-live/apps/admin/src/views/albums/detail/index.vue
T
浅念 8e674a902e feat(albums): soft-delete albums (admin+owner, hidden & irreversible, both app entries)
契约 deleteAlbum(DELETE /albums/{albumId} → 204 幂等)+ status enum 'deleted':
- 服务端:softDelete(canView 404→canManage 403 防枚举;乐观锁 claim +
  outbox album.updated priority=high + audit album.delete);列表/详情/更新/
  成员/资产/分类/水印/照片/展示位绑定全部 deleted→404;公开端 cn
  (status!=='active')与 ov(isAlbumVisibleOv)天然屏蔽;ov 副本经
  album.updated 载荷 status 直投自动下线
- Admin:列表表格操作「删除」+ 详情页头「删除相册」(确认含不可恢复说明)
- App:操作面板「删除相册」(红字、二次确认、成功退回列表并刷新)
- 验证:API 56/56、App 397/397(analyze 0)、admin typecheck/lint/test/build 绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 10:13:08 +08:00

248 lines
7.9 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.
<!-- 相册详情(UI06 对齐示例 03:页头 + 摘要头 + 页签;banner 上传区待 B3c 收进资产页签)。
路由 /albums/detail/:albumId(isHide,不进菜单)。 -->
<template>
<div class="ui06-detail-page">
<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>
<template v-else>
<div v-loading="loading">
<!-- 页头:返回 + 标题 + 固定主操作(编辑 outline / 分享 primary / 交接次级) -->
<div class="page-heading">
<div class="heading-left">
<ElButton link data-test="album-detail-back" @click="router.back()">
<ElIcon><ArrowLeft /></ElIcon>
</ElButton>
<div>
<h1>{{ titleOf(album) }}</h1>
<p>{{ $t('albumsManage.detail.subtitle') }}</p>
</div>
</div>
<div class="actions">
<ElButton plain data-test="album-detail-transfer" @click="transferVisible = true">
{{ $t('albumsManage.transfer.entry') }}
</ElButton>
<ElButton data-test="album-detail-share-shortcut" @click="activeTab = 'share'">
{{ $t('albumsManage.detail.share') }}
</ElButton>
<ElButton
type="primary"
plain
data-test="album-detail-edit"
@click="router.push({ name: 'AlbumsFormEdit', params: { albumId: albumId } })"
>
{{ $t('albumsManage.detail.edit') }}
</ElButton>
<ElButton type="danger" plain data-test="album-detail-delete" @click="removeAlbum">
{{ $t('albumsManage.detail.delete') }}
</ElButton>
</div>
</div>
<!-- 摘要头:封面 + 紧凑资料 + 高级折叠(替换原 ElDescriptions 大区) -->
<AlbumSummaryHeader v-if="album" :album="album" />
</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" @transfer="transferVisible = true" />
</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>
<ElTabPane name="assets">
<template #label>
<span data-test="album-assets-tab">{{ $t('albumsManage.detail.tabAssets') }}</span>
</template>
<AlbumBannerUpload :album-id="albumId" />
</ElTabPane>
</ElTabs>
</template>
<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 } from '@element-plus/icons-vue'
import AlbumSummaryHeader from '@/components/business/albums/AlbumSummaryHeader.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 { ElMessage, ElMessageBox } from 'element-plus'
import { fetchAlbum, fetchDeleteAlbum } from '@/api/albums'
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 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
}
}
/** 交接成功:回包 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
}
/** 软删除相册(admin/owner):确认 → DELETE → 回列表 */
async function removeAlbum() {
try {
await ElMessageBox.confirm(
t('albumsManage.list.deleteConfirm', { title: titleOf(album.value) }),
t('common.tips'),
{
type: 'warning',
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel')
}
)
} catch {
return // 用户取消
}
await fetchDeleteAlbum(albumId.value)
ElMessage.success(t('albumsManage.list.deleted'))
void router.replace({ name: 'AlbumsManage' })
}
/** 列表「成员」入口带 #members 锚点:直达成员页签 */
watch(
() => route.hash,
(hash) => {
if (hash === '#members') activeTab.value = 'members'
},
{ immediate: true }
)
watch(albumId, () => {
if (route.name === 'AlbumsDetail') load()
})
onMounted(() => {
load()
})
</script>
<style lang="scss" scoped>
@reference '@styles/core/tailwind.css';
/* UI06:详情页工作区(示例 .workspace / .page-heading / .tabs) */
.ui06-detail-page {
max-width: 1660px;
padding: 28px 30px 44px;
margin: auto;
}
.page-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 22px;
.heading-left {
display: flex;
gap: 10px;
align-items: center;
min-width: 0;
h1 {
overflow: hidden;
font-size: 24px;
letter-spacing: -0.6px;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
p {
margin-top: 4px;
font-size: 12px;
color: #7b899f;
}
}
}
:deep(.el-tabs__nav-wrap::after) {
height: 1px;
background: #dfe7f2;
}
:deep(.el-tabs__item) {
font-size: 12px;
}
</style>