feat(ui06): B3c form batch — full-page create/edit form + assets tab

- 新增 /albums/form/create 与 /albums/form/:albumId 整页表单路由(隐藏路由,父菜单高亮)
- 编辑模式资料/访问页签(访问=开关行卡:标题+说明+开关);创建模式契约约束下载开关+解释提示
- 校验/提交逻辑与原对话框一致(契约镜像校验;translations 全量替换;创建仅 download 开关)
- 保存成功跳相册详情;manage/detail 两处入口改路由跳转,删除 AlbumFormDialog
- banner 上传收进详情「品牌资产」独立页签(15)
- e2e 适配:整页表单流程(form-page/goback/访问页签)、详情锚点改 summary、banner 页签
- typecheck/lint/vitest/build 全绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 06:58:01 +08:00
parent d6d012b992
commit 352d7405c2
30 changed files with 565 additions and 337 deletions
+7 -13
View File
@@ -35,7 +35,7 @@
type="primary"
plain
data-test="album-detail-edit"
@click="editVisible = true"
@click="router.push({ name: 'AlbumsFormEdit', params: { albumId: albumId } })"
>
{{ $t('albumsManage.detail.edit') }}
</ElButton>
@@ -44,11 +44,6 @@
<!-- 摘要头:封面 + 紧凑资料 + 高级折叠(替换原 ElDescriptions 大区) -->
<AlbumSummaryHeader v-if="album" :album="album" />
<!-- 品牌资产(banner 上传;B3c 收进独立页签,本批保持原位) -->
<div v-if="album" class="mt-5">
<AlbumBannerUpload :album-id="albumId" />
</div>
</div>
<ElTabs v-if="album" v-model="activeTab" class="mt-4" data-test="album-detail-tabs">
@@ -88,10 +83,15 @@
</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>
<AlbumFormDialog v-if="album" v-model="editVisible" :album="album" @saved="onAlbumSaved" />
<AlbumTransferDialog
v-if="album"
v-model="transferVisible"
@@ -105,7 +105,6 @@
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import AlbumFormDialog from '@/views/albums/manage/AlbumFormDialog.vue'
import AlbumSummaryHeader from '@/components/business/albums/AlbumSummaryHeader.vue'
import AlbumBannerUpload from './AlbumBannerUpload.vue'
import AlbumMembers from './AlbumMembers.vue'
@@ -126,7 +125,6 @@
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')
@@ -143,10 +141,6 @@
}
}
function onAlbumSaved(saved: Api.Album.Detail) {
album.value = saved
}
/** 交接成功:回包 ownerId 已变更,原位替换相册详情 */
function onTransferred(transferred: Api.Album.Detail) {
album.value = transferred
+466
View File
@@ -0,0 +1,466 @@
<!-- 相册整页表单(UI06 对齐示例 08/09:对话框改整页路由)。
create:/albums/form/create;edit:/albums/form/:albumId。
校验/提交逻辑与原 AlbumFormDialog 一致(契约镜像校验 utils/albums/albumValidation);
编辑 = PATCH 全量可编辑字段(translations 全量替换);创建仅 download 两开关(契约)。 -->
<template>
<div class="ui06-form-page" data-test="album-form-page">
<!-- 页头:返回 + 标题 + 主操作 -->
<div class="page-heading">
<div class="heading-left">
<ElButton link data-test="album-form-back" @click="goBack">
<ElIcon><ArrowLeft /></ElIcon>
</ElButton>
<div>
<h1>{{
isEdit ? t('albumsManage.form.editTitle') : t('albumsManage.form.createTitle')
}}</h1>
<p>{{ t('albumsManage.form.pageSubtitle') }}</p>
</div>
</div>
</div>
<!-- 编辑模式:资料 / 访问 页签(示例 09);创建模式仅资料 -->
<ElTabs v-if="isEdit" v-model="formTab" class="form-tabs">
<ElTabPane :label="t('albumsManage.form.tabDetails')" name="details" />
<ElTabPane :label="t('albumsManage.form.tabAccess')" name="access" />
</ElTabs>
<template v-if="!isEdit || formTab === 'details'">
<!-- 基础资料卡(示例 08 基础卡:翻译 + 日期 + 时区) -->
<ElCard class="ui06-card" shadow="never">
<template #header>
<div class="card-head"
><h2>{{ t('albumsManage.form.basicCard') }}</h2></div
>
</template>
<ElForm :model="form" label-width="130px">
<ElFormItem :label="t('albumsManage.form.fieldDefaultLocale')">
<ElRadioGroup v-model="form.defaultLocale" data-test="album-form-default-locale">
<ElRadioButton value="zh-CN">zh-CN</ElRadioButton>
<ElRadioButton value="en">en</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<ElTabs v-model="activeTab">
<ElTabPane :label="t('albumsManage.form.tabZh')" name="zh-CN">
<TranslationFields v-model="drafts['zh-CN']" :locale="'zh-CN'" />
</ElTabPane>
<ElTabPane :label="t('albumsManage.form.tabEn')" name="en">
<TranslationFields v-model="drafts['en']" :locale="'en'" />
</ElTabPane>
</ElTabs>
<ElFormItem
:label="t('albumsManage.form.fieldStartAt')"
required
:error="fieldErrors.startAt"
>
<!-- ElDatePicker 的透传 attr 被 ElTooltip 吞掉,data-test 挂不到 DOM:
外层普通 div 承载选择器,测试经 div 内 input 定位 -->
<div data-test="album-form-start-at">
<ElDatePicker
v-model="form.startAt"
type="datetime"
:placeholder="t('albumsManage.form.fieldStartAt')"
/>
</div>
</ElFormItem>
<ElFormItem :label="t('albumsManage.form.fieldEndAt')" :error="fieldErrors.endAt">
<div data-test="album-form-end-at">
<ElDatePicker
v-model="form.endAt"
type="datetime"
clearable
:placeholder="t('albumsManage.form.fieldEndAt')"
/>
</div>
</ElFormItem>
<ElFormItem :label="t('albumsManage.form.fieldTimezone')">
<ElInput
v-model="form.timezone"
data-test="album-form-timezone"
placeholder="Asia/Shanghai"
/>
</ElFormItem>
</ElForm>
</ElCard>
<!-- 创建模式:下载开关卡(契约仅 download 两项;上传/公开创建后经编辑调整) -->
<ElCard v-if="!isEdit" class="ui06-card" shadow="never">
<template #header>
<div class="card-head"
><h2>{{ t('albumsManage.form.switchesCard') }}</h2></div
>
</template>
<div data-test="album-form-switches" class="switch-list">
<ElSwitch
v-model="form.downloadEnabled"
:active-text="t('albumsManage.form.switchDownload')"
data-test="album-form-switch-download"
/>
<ElSwitch
v-model="form.originalDownloadEnabled"
:active-text="t('albumsManage.form.switchOriginal')"
data-test="album-form-switch-original"
/>
</div>
<ElAlert
type="info"
:closable="false"
:title="t('albumsManage.form.createSwitchesHint')"
class="mt-3"
/>
</ElCard>
<ElAlert
v-if="isEdit"
type="warning"
:closable="false"
:title="t('albumsManage.form.translationsReplaceHint')"
data-test="album-form-replace-hint"
class="mt-4"
/>
</template>
<!-- 编辑模式访问页签(示例 09 开关行卡:标题+说明+开关) -->
<template v-if="isEdit && formTab === 'access'">
<ElCard class="ui06-card" shadow="never">
<template #header>
<div class="card-head"
><h2>{{ t('albumsManage.form.accessCard') }}</h2></div
>
</template>
<div data-test="album-form-switches" class="switch-rows">
<div class="switch-row">
<div class="row-text">
<strong>{{ t('albumsManage.form.switchUpload') }}</strong>
<p>{{ t('albumsManage.form.switchUploadDesc') }}</p>
</div>
<ElSwitch v-model="form.uploadEnabled" data-test="album-form-switch-upload" />
</div>
<div class="switch-row">
<div class="row-text">
<strong>{{ t('albumsManage.form.switchPublic') }}</strong>
<p>{{ t('albumsManage.form.switchPublicDesc') }}</p>
</div>
<ElSwitch v-model="form.publicEnabled" data-test="album-form-switch-public" />
</div>
<div class="switch-row">
<div class="row-text">
<strong>{{ t('albumsManage.form.switchDownload') }}</strong>
<p>{{ t('albumsManage.form.switchDownloadDesc') }}</p>
</div>
<ElSwitch v-model="form.downloadEnabled" data-test="album-form-switch-download" />
</div>
<div class="switch-row">
<div class="row-text">
<strong>{{ t('albumsManage.form.switchOriginal') }}</strong>
<p>{{ t('albumsManage.form.switchOriginalDesc') }}</p>
</div>
<ElSwitch
v-model="form.originalDownloadEnabled"
data-test="album-form-switch-original"
/>
</div>
</div>
</ElCard>
</template>
<!-- 底部操作条(示例 09 Save 固定底栏语义) -->
<div class="form-footer">
<ElButton data-test="album-form-cancel" @click="goBack">
{{ t('albumsManage.form.cancel') }}
</ElButton>
<ElButton type="primary" :loading="submitting" data-test="album-form-submit" @click="submit">
{{ t('albumsManage.form.submit') }}
</ElButton>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import TranslationFields from '@/views/albums/manage/TranslationFields.vue'
import { fetchAlbum, fetchCreateAlbum, fetchUpdateAlbum } from '@/api/albums'
import {
toTranslationPayload,
validateTimeRange,
validateTranslations,
type TranslationDraft
} from '@/utils/albums/albumValidation'
defineOptions({ name: 'AlbumsForm' })
/** 翻译草稿形状(两语言固定键,模板按键取值) */
type DraftMap = Record<'zh-CN' | 'en', TranslationDraft>
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
/** 路由参数决定模式:/albums/form/create | /albums/form/:albumId */
const albumId = computed(() => {
const raw = String(route.params.albumId ?? '')
return raw === '' || raw === 'create' ? null : raw
})
const isEdit = computed(() => albumId.value !== null)
/** 编辑目标摘要(fetchAlbum 装载) */
const album = ref<Api.Album.Detail | null>(null)
const loading = ref(false)
const loadError = ref<Error | null>(null)
const formTab = ref<'details' | 'access'>('details')
const activeTab = ref<'zh-CN' | 'en'>('zh-CN')
const emptyDraft = (locale: 'zh-CN' | 'en'): TranslationDraft => ({
locale,
title: '',
description: '',
venue: '',
address: ''
})
const drafts = reactive<DraftMap>({
'zh-CN': emptyDraft('zh-CN'),
en: emptyDraft('en')
})
const form = reactive({
defaultLocale: 'zh-CN' as Api.Album.Locale,
startAt: null as Date | null,
endAt: null as Date | null,
timezone: 'Asia/Shanghai',
uploadEnabled: true,
publicEnabled: true,
downloadEnabled: true,
originalDownloadEnabled: false
})
const fieldErrors = reactive<{ startAt: string; endAt: string }>({ startAt: '', endAt: '' })
const submitting = ref(false)
async function load(): Promise<void> {
if (!albumId.value) return
loading.value = true
loadError.value = null
try {
const target = await fetchAlbum(albumId.value)
album.value = target
form.defaultLocale = target.defaultLocale
form.startAt = new Date(target.startAt)
form.endAt = target.endAt ? new Date(target.endAt) : null
form.timezone = target.timezone
form.uploadEnabled = target.uploadEnabled
form.publicEnabled = target.publicEnabled
form.downloadEnabled = target.downloadEnabled
form.originalDownloadEnabled = target.originalDownloadEnabled
for (const locale of ['zh-CN', 'en'] as const) {
const translation = target.translations.find((item) => item.locale === locale)
drafts[locale] = {
...emptyDraft(locale),
title: translation?.title ?? '',
description: translation?.description ?? '',
venue: translation?.venue ?? '',
address: translation?.address ?? ''
}
}
} catch (e) {
loadError.value = e as Error
} finally {
loading.value = false
}
}
/** 提交前校验(镜像契约:默认语言覆盖 + endAt>startAt) */
function validate(): boolean {
fieldErrors.startAt = ''
fieldErrors.endAt = ''
if (!form.startAt) {
fieldErrors.startAt = t('albumsManage.form.errors.startAtRequired')
return false
}
const translationErrors = validateTranslations([drafts['zh-CN'], drafts.en], form.defaultLocale)
if (translationErrors.length > 0) {
const code = translationErrors[0]
// 定位到出错的页签并切换,便于用户直接修补
const other =
code === 'titleRequiredWithFields' &&
!drafts.en.title.trim() &&
drafts['zh-CN'].title.trim()
? 'en'
: null
activeTab.value = other ?? form.defaultLocale
formTab.value = 'details'
ElMessage.error(
code === 'defaultLocaleMissing'
? t('albumsManage.form.errors.defaultLocaleMissing', { locale: form.defaultLocale })
: t(`albumsManage.form.errors.${code}`)
)
return false
}
const startIso = form.startAt.toISOString()
const endIso = form.endAt ? form.endAt.toISOString() : null
if (!validateTimeRange(startIso, endIso)) {
fieldErrors.endAt = t('albumsManage.form.errors.endAtNotAfterStartAt')
return false
}
return true
}
/** 保存后返回来源(列表或详情) */
function goBack(): void {
if (window.history.length > 1) {
router.back()
} else {
void router.push({ name: 'AlbumsManage' })
}
}
async function submit() {
if (!validate()) return
submitting.value = true
try {
const translations = toTranslationPayload([
drafts['zh-CN'],
drafts.en
]) as Api.Album.Translation[]
const endAt = form.endAt ? form.endAt.toISOString() : null
let saved: Api.Album.Detail
if (albumId.value) {
// 编辑:PATCH 全量可编辑字段(开关独立修改;translations 全量替换)
saved = await fetchUpdateAlbum(albumId.value, {
translations,
defaultLocale: form.defaultLocale,
startAt: form.startAt!.toISOString(),
endAt,
timezone: form.timezone.trim() || 'Asia/Shanghai',
uploadEnabled: form.uploadEnabled,
publicEnabled: form.publicEnabled,
downloadEnabled: form.downloadEnabled,
originalDownloadEnabled: form.originalDownloadEnabled
})
} else {
// 创建:契约仅 download 两开关(upload/public 服务端固定初始 true)
saved = await fetchCreateAlbum({
translations,
defaultLocale: form.defaultLocale,
startAt: form.startAt!.toISOString(),
endAt,
timezone: form.timezone.trim() || 'Asia/Shanghai',
downloadEnabled: form.downloadEnabled,
originalDownloadEnabled: form.originalDownloadEnabled
})
}
ElMessage.success(
isEdit.value ? t('albumsManage.form.updateSuccess') : t('albumsManage.form.createSuccess')
)
void router.replace({ name: 'AlbumsDetail', params: { albumId: saved.id } })
} finally {
submitting.value = false
}
}
onMounted(() => {
void load()
})
</script>
<style lang="scss" scoped>
@reference '@styles/core/tailwind.css';
.ui06-form-page {
max-width: 900px;
padding: 28px 30px 90px;
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;
h1 {
font-size: 24px;
letter-spacing: -0.6px;
line-height: 1.35;
}
p {
margin-top: 4px;
font-size: 12px;
color: #7b899f;
}
}
}
.card-head h2 {
font-size: 17px;
line-height: 1.4;
}
.switch-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 示例 .switch-row:标题+说明+右开关 */
.switch-rows .switch-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 17px 0;
border-bottom: 1px solid #e5eaf1;
&:last-child {
border-bottom: 0;
}
.row-text {
strong {
font-size: 13px;
font-weight: 550;
}
p {
margin-top: 4px;
font-size: 12px;
color: #8a97ac;
}
}
}
/* 底部操作条 */
.form-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 22px;
.el-button {
min-width: 120px;
}
}
@media (max-width: 640px) {
.ui06-form-page {
padding: 20px 14px 80px;
}
}
</style>
@@ -1,294 +0,0 @@
<!-- 创建/编辑相册对话框:zh-CN/en 翻译分页签 + 契约镜像校验(utils/albums/albumValidation)。
编辑为 PATCH 语义;translations 全量替换(提示 i18n)。 -->
<template>
<ElDialog
v-model="visible"
:title="isEdit ? t('albumsManage.form.editTitle') : t('albumsManage.form.createTitle')"
width="720px"
destroy-on-close
data-test="album-form-dialog"
@closed="resetForm"
>
<ElForm :model="form" label-width="130px">
<ElFormItem :label="t('albumsManage.form.fieldDefaultLocale')">
<ElRadioGroup v-model="form.defaultLocale" data-test="album-form-default-locale">
<ElRadioButton value="zh-CN">zh-CN</ElRadioButton>
<ElRadioButton value="en">en</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<ElTabs v-model="activeTab">
<ElTabPane :label="t('albumsManage.form.tabZh')" name="zh-CN">
<TranslationFields v-model="drafts['zh-CN']" :locale="'zh-CN'" />
</ElTabPane>
<ElTabPane :label="t('albumsManage.form.tabEn')" name="en">
<TranslationFields v-model="drafts['en']" :locale="'en'" />
</ElTabPane>
</ElTabs>
<ElFormItem
:label="t('albumsManage.form.fieldStartAt')"
required
:error="fieldErrors.startAt"
>
<!-- ElDatePicker 的透传 attr 被 ElTooltip 吞掉,data-test 挂不到 DOM:
外层普通 div 承载选择器,测试经 div 内 input 定位 -->
<div data-test="album-form-start-at">
<ElDatePicker
v-model="form.startAt"
type="datetime"
:placeholder="t('albumsManage.form.fieldStartAt')"
/>
</div>
</ElFormItem>
<ElFormItem :label="t('albumsManage.form.fieldEndAt')" :error="fieldErrors.endAt">
<div data-test="album-form-end-at">
<ElDatePicker
v-model="form.endAt"
type="datetime"
clearable
:placeholder="t('albumsManage.form.fieldEndAt')"
/>
</div>
</ElFormItem>
<ElFormItem :label="t('albumsManage.form.fieldTimezone')">
<ElInput
v-model="form.timezone"
data-test="album-form-timezone"
placeholder="Asia/Shanghai"
/>
</ElFormItem>
<ElFormItem :label="t('albumsManage.form.fieldSwitches')">
<div class="flex flex-col gap-2" data-test="album-form-switches">
<template v-if="isEdit">
<ElSwitch
v-model="form.uploadEnabled"
:active-text="t('albumsManage.form.switchUpload')"
data-test="album-form-switch-upload"
/>
<ElSwitch
v-model="form.publicEnabled"
:active-text="t('albumsManage.form.switchPublic')"
data-test="album-form-switch-public"
/>
</template>
<ElSwitch
v-model="form.downloadEnabled"
:active-text="t('albumsManage.form.switchDownload')"
data-test="album-form-switch-download"
/>
<ElSwitch
v-model="form.originalDownloadEnabled"
:active-text="t('albumsManage.form.switchOriginal')"
data-test="album-form-switch-original"
/>
</div>
</ElFormItem>
<ElAlert
v-if="isEdit"
type="warning"
:closable="false"
:title="t('albumsManage.form.translationsReplaceHint')"
data-test="album-form-replace-hint"
/>
</ElForm>
<template #footer>
<ElButton data-test="album-form-cancel" @click="visible = false">
{{ t('albumsManage.form.cancel') }}
</ElButton>
<ElButton type="primary" :loading="submitting" data-test="album-form-submit" @click="submit">
{{ t('albumsManage.form.submit') }}
</ElButton>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import TranslationFields from './TranslationFields.vue'
import { fetchCreateAlbum, fetchUpdateAlbum } from '@/api/albums'
import {
toTranslationPayload,
validateTimeRange,
validateTranslations,
type TranslationDraft
} from '@/utils/albums/albumValidation'
/** 翻译草稿形状(两语言固定键,模板按键取值) */
type DraftMap = Record<'zh-CN' | 'en', TranslationDraft>
const props = defineProps<{
/** 编辑目标(null = 创建) */
album: Api.Album.Summary | null
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved', album: Api.Album.Detail): void
}>()
const visible = defineModel<boolean>({ default: false })
const { t } = useI18n()
const isEdit = computed(() => props.album !== null)
const emptyDraft = (locale: 'zh-CN' | 'en'): TranslationDraft => ({
locale,
title: '',
description: '',
venue: '',
address: ''
})
const activeTab = ref<'zh-CN' | 'en'>('zh-CN')
const drafts = reactive<DraftMap>({
'zh-CN': emptyDraft('zh-CN'),
en: emptyDraft('en')
})
const form = reactive({
defaultLocale: 'zh-CN' as Api.Album.Locale,
startAt: null as Date | null,
endAt: null as Date | null,
timezone: 'Asia/Shanghai',
uploadEnabled: true,
publicEnabled: true,
downloadEnabled: true,
originalDownloadEnabled: false
})
const fieldErrors = reactive<{ startAt: string; endAt: string }>({ startAt: '', endAt: '' })
const submitting = ref(false)
/** 打开时装载编辑目标(或重置为创建默认值) */
watch(visible, (open) => {
if (!open) return
resetForm()
const album = props.album
if (!album) return
form.defaultLocale = album.defaultLocale
form.startAt = new Date(album.startAt)
form.endAt = album.endAt ? new Date(album.endAt) : null
form.timezone = album.timezone
form.uploadEnabled = album.uploadEnabled
form.publicEnabled = album.publicEnabled
form.downloadEnabled = album.downloadEnabled
form.originalDownloadEnabled = album.originalDownloadEnabled
for (const locale of ['zh-CN', 'en'] as const) {
const translation = album.translations.find((item) => item.locale === locale)
drafts[locale] = {
...emptyDraft(locale),
title: translation?.title ?? '',
description: translation?.description ?? '',
venue: translation?.venue ?? '',
address: translation?.address ?? ''
}
}
})
function resetForm() {
activeTab.value = 'zh-CN'
drafts['zh-CN'] = emptyDraft('zh-CN')
drafts.en = emptyDraft('en')
Object.assign(form, {
defaultLocale: 'zh-CN',
startAt: null,
endAt: null,
timezone: 'Asia/Shanghai',
uploadEnabled: true,
publicEnabled: true,
downloadEnabled: true,
originalDownloadEnabled: false
})
fieldErrors.startAt = ''
fieldErrors.endAt = ''
}
/** 提交前校验(镜像契约:默认语言覆盖 + endAt>startAt) */
function validate(): boolean {
fieldErrors.startAt = ''
fieldErrors.endAt = ''
if (!form.startAt) {
fieldErrors.startAt = t('albumsManage.form.errors.startAtRequired')
return false
}
const translationErrors = validateTranslations([drafts['zh-CN'], drafts.en], form.defaultLocale)
if (translationErrors.length > 0) {
const code = translationErrors[0]
// 定位到出错的页签并切换,便于用户直接修补
const other =
code === 'titleRequiredWithFields' &&
!drafts.en.title.trim() &&
drafts['zh-CN'].title.trim()
? 'en'
: null
activeTab.value = other ?? form.defaultLocale
ElMessage.error(
code === 'defaultLocaleMissing'
? t('albumsManage.form.errors.defaultLocaleMissing', { locale: form.defaultLocale })
: t(`albumsManage.form.errors.${code}`)
)
return false
}
const startIso = form.startAt.toISOString()
const endIso = form.endAt ? form.endAt.toISOString() : null
if (!validateTimeRange(startIso, endIso)) {
fieldErrors.endAt = t('albumsManage.form.errors.endAtNotAfterStartAt')
return false
}
return true
}
async function submit() {
if (!validate()) return
submitting.value = true
try {
const translations = toTranslationPayload([
drafts['zh-CN'],
drafts.en
]) as Api.Album.Translation[]
const endAt = form.endAt ? form.endAt.toISOString() : null
let saved: Api.Album.Detail
if (props.album) {
// 编辑:PATCH 全量可编辑字段(开关独立修改;translations 全量替换)
saved = await fetchUpdateAlbum(props.album.id, {
translations,
defaultLocale: form.defaultLocale,
startAt: form.startAt!.toISOString(),
endAt,
timezone: form.timezone.trim() || 'Asia/Shanghai',
uploadEnabled: form.uploadEnabled,
publicEnabled: form.publicEnabled,
downloadEnabled: form.downloadEnabled,
originalDownloadEnabled: form.originalDownloadEnabled
})
} else {
// 创建:契约仅 download 两开关(upload/public 服务端固定初始 true)
saved = await fetchCreateAlbum({
translations,
defaultLocale: form.defaultLocale,
startAt: form.startAt!.toISOString(),
endAt,
timezone: form.timezone.trim() || 'Asia/Shanghai',
downloadEnabled: form.downloadEnabled,
originalDownloadEnabled: form.originalDownloadEnabled
})
}
ElMessage.success(
isEdit.value ? t('albumsManage.form.updateSuccess') : t('albumsManage.form.createSuccess')
)
emit('saved', saved)
visible.value = false
} finally {
submitting.value = false
}
}
</script>
+3 -15
View File
@@ -170,8 +170,6 @@
</ElButton>
<span v-else>{{ $t('albumsManage.list.noMore') }}</span>
</div>
<AlbumFormDialog v-model="formVisible" :album="editing" @saved="onSaved" />
</div>
</template>
@@ -180,7 +178,6 @@
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Search, Menu, Grid } from '@element-plus/icons-vue'
import AlbumFormDialog from './AlbumFormDialog.vue'
import AlbumCoverCard from '@/components/business/albums/AlbumCoverCard.vue'
import { fetchAlbumList } from '@/api/albums'
import { useCursorList } from '@/views/albums/composables/useCursorList'
@@ -243,22 +240,13 @@
]
}
// ── 创建 / 编辑 ──
const formVisible = ref(false)
const editing = ref<Api.Album.Summary | null>(null)
// ── 创建 / 编辑(UI06:整页表单路由) ──
function openCreate() {
editing.value = null
formVisible.value = true
router.push({ name: 'AlbumsFormCreate' })
}
function openEdit(album: Api.Album.Summary) {
editing.value = album
formVisible.value = true
}
function onSaved() {
loadFirst()
router.push({ name: 'AlbumsFormEdit', params: { albumId: album.id } })
}
// ── 跳转 ──