更新页面效果

This commit is contained in:
李琦
2026-07-31 13:09:49 +08:00
parent d060681920
commit e112b94ac9
36 changed files with 3670 additions and 64 deletions

View File

@@ -29,10 +29,10 @@
</header>
<div class="max-w-[960px] mx-auto px-4 py-6">
<a-tabs v-model:activeKey="activeTab" type="card" class="admin-tabs">
<a-tabs v-model:activeKey="activeTab" type="card" class="admin-tabs" :destroyInactiveTabPane="true">
<!-- 基础信息 -->
<a-tab-pane key="basic" tab="基础信息">
<a-card :bordered="false" class="!shadow-sm">
<a-card v-if="activeTab === 'basic'" :bordered="false" class="!shadow-sm">
<a-form layout="vertical">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<a-form-item label="新郎">
@@ -54,16 +54,23 @@
<a-textarea v-model:value="cfg.address" :rows="2" placeholder="可换行" />
</a-form-item>
</div>
<a-form-item label="封面 / 尾页主图">
<image-field v-model="cfg.heroImg" @pick="openUpload(u => cfg.heroImg = u)" :uploading="uploading" />
</a-form-item>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<a-form-item label="封面图片">
<image-field v-model="cfg.heroImg" label="封面" @pick="openUpload(u => cfg.heroImg = u)" :uploading="uploading" />
<div class="text-[11px] text-[#8A8680] mt-2">用于首页封面和入场揭幕背景</div>
</a-form-item>
<a-form-item label="尾页图片">
<image-field v-model="cfg.endImg" label="尾页" @pick="openUpload(u => cfg.endImg = u)" :uploading="uploading" />
<div class="text-[11px] text-[#8A8680] mt-2">用于最后的回执尾页可与封面分开设置</div>
</a-form-item>
</div>
</a-form>
</a-card>
</a-tab-pane>
<!-- 文案 -->
<a-tab-pane key="copy" tab="邀请文案">
<a-card :bordered="false" class="!shadow-sm">
<a-card v-if="activeTab === 'copy'" :bordered="false" class="!shadow-sm">
<a-form layout="vertical">
<a-form-item label="竖排文案 · 上联">
<a-input v-model:value="cfg.formalText1" placeholder="如 两姓联姻 一堂缔约" />
@@ -77,7 +84,7 @@
<!-- 视觉 / 动效 -->
<a-tab-pane key="visual" tab="视觉与动效">
<a-card :bordered="false" class="!shadow-sm">
<a-card v-if="activeTab === 'visual'" :bordered="false" class="!shadow-sm">
<a-form layout="vertical">
<a-form-item label="背景音乐">
<div class="text-[12px] text-[#8A8680] leading-relaxed">多首背景音乐的上传试听与设为播放请在 <span class="text-[#a88c6b]">背景音乐</span> 标签页管理</div>
@@ -122,7 +129,7 @@
<!-- 背景音乐 -->
<a-tab-pane key="music" tab="背景音乐">
<a-card :bordered="false" class="!shadow-sm">
<a-card v-if="activeTab === 'music'" :bordered="false" class="!shadow-sm">
<div class="text-[12px] text-[#8A8680] leading-relaxed mb-4">
上传多首背景音乐支持 mp3 等格式访客首次交互后自动播放当前曲目可在前台随时切换或暂停
</div>
@@ -147,14 +154,67 @@
</a-card>
</a-tab-pane>
<!-- 相册页 -->
<!-- 上传设置 -->
<a-tab-pane key="upload" tab="上传设置">
<a-card v-if="activeTab === 'upload'" :bordered="false" class="!shadow-sm">
<a-alert
type="info"
show-icon
class="mb-4"
message="上传设置仅后台可见OSS 密钥不会写入公开请柬配置。"
/>
<a-form layout="vertical">
<a-form-item label="上传方式">
<a-radio-group v-model:value="uploadCfg.provider">
<a-radio value="local">本地上传</a-radio>
<a-radio value="oss">阿里云 OSS</a-radio>
</a-radio-group>
</a-form-item>
<div v-if="uploadCfg.provider === 'oss'" class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<a-form-item label="AccessKeyId">
<a-input v-model:value="uploadCfg.accessKeyId" placeholder="请输入 AccessKeyId" />
</a-form-item>
<a-form-item label="AccessKeySecret">
<a-input-password v-model:value="uploadCfg.accessKeySecret" :placeholder="uploadCfg.hasSecret ? '已配置,留空则不修改' : '请输入 AccessKeySecret'" />
</a-form-item>
<a-form-item label="Bucket">
<a-input v-model:value="uploadCfg.bucket" placeholder="如 wedding-assets" />
</a-form-item>
<a-form-item label="文件夹">
<a-input v-model:value="uploadCfg.folder" placeholder="如 invitations" />
</a-form-item>
<a-form-item label="访问域名">
<a-input v-model:value="uploadCfg.domain" placeholder="如 https://cdn.example.com可留空" />
</a-form-item>
<a-form-item label="Region">
<a-input v-model:value="uploadCfg.region" placeholder="如 cn-hangzhou" />
</a-form-item>
<a-form-item label="Endpoint">
<a-input v-model:value="uploadCfg.endpoint" placeholder="如 https://oss-cn-hangzhou.aliyuncs.com" />
</a-form-item>
</div>
<div v-else class="text-[12px] text-[#8A8680] leading-relaxed mb-4">
本地上传会继续保存到后端 uploads 目录返回 http://localhost:8080/uploads/ 开头的地址。
</div>
<a-button type="primary" :loading="uploadConfigSaving" @click="onSaveUploadConfig"
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]">
<i class="fa-solid fa-floppy-disk mr-1"></i>保存上传设置
</a-button>
</a-form>
</a-card>
</a-tab-pane>
<a-tab-pane key="photos" tab="相册页管理">
<div class="flex justify-between items-center mb-3">
<span class="text-[12px] text-[#8A8680]"> {{ cfg.photoPages.length }} 个相册页</span>
<a-button size="small" @click="addPage"><i class="fa-solid fa-plus mr-1 text-[#a88c6b]"></i>新增相册页</a-button>
</div>
<a-empty v-if="!cfg.photoPages.length" description="暂无相册页" />
<a-card v-for="(page, idx) in cfg.photoPages" :key="idx" :bordered="false" class="!shadow-sm mb-4">
<a-tabs v-if="cfg.photoPages.length" v-model:activeKey="activePhotoTab" size="small" type="card" class="photo-page-tabs" :destroyInactiveTabPane="true">
<a-tab-pane v-for="(page, idx) in cfg.photoPages" :key="String(idx)" :tab="`第 ${idx + 1} 页`" />
</a-tabs>
<template v-for="(page, idx) in cfg.photoPages" :key="idx">
<a-card v-if="idx === currentPhotoIndex" :bordered="false" class="!shadow-sm mb-4">
<div class="flex justify-between items-center mb-3">
<span class="text-[#a88c6b] text-sm"> {{ idx + 1 }} </span>
<div class="flex gap-1">
@@ -257,6 +317,7 @@
</template>
</div>
</a-card>
</template>
</a-tab-pane>
<!-- 婚礼流程 -->
@@ -303,10 +364,10 @@
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { computed, reactive, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { getConfig, saveConfig, uploadImage, getRsvpList } from '@/api/wedding'
import { getConfig, saveConfig, uploadImage, getRsvpList, getUploadConfig, saveUploadConfig } from '@/api/wedding'
import { useAdminStore } from '@/stores/admin'
import ImageField from '@/components/ImageField.vue'
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
@@ -315,12 +376,16 @@ const router = useRouter()
const adminStore = useAdminStore()
const activeTab = ref('basic')
const activePhotoTab = ref('0')
const saving = ref(false)
const uploading = ref(false)
const uploadConfigSaving = ref(false)
const rsvpLoading = ref(false)
const rsvpList = ref([])
const cfg = reactive(defaultConfig())
const uploadCfg = reactive(defaultUploadConfig())
const currentPhotoIndex = computed(() => Math.min(Number(activePhotoTab.value) || 0, Math.max(cfg.photoPages.length - 1, 0)))
function defaultConfig() {
return {
@@ -328,6 +393,7 @@ function defaultConfig() {
hotel: '杭州星光万丽大酒店', address: '杭州市西湖区星光大道88号\n3楼大宴会厅',
formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称',
heroImg: 'https://images.unsplash.com/photo-1583939003579-730e3918a45a?auto=format&fit=crop&w=800&q=80',
endImg: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=800&q=80',
musicList: [{ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', name: '背景音乐 1' }],
activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
@@ -346,6 +412,20 @@ function defaultConfig() {
}
}
function defaultUploadConfig() {
return {
provider: 'local',
accessKeyId: '',
accessKeySecret: '',
hasSecret: false,
bucket: '',
folder: '',
domain: '',
region: '',
endpoint: '',
}
}
function fill(n) {
return Array.from({ length: n }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80')
}
@@ -411,9 +491,24 @@ function moveImage(page, i, dir) {
}
// 相册页操作
function addPage() { cfg.photoPages.push(ensurePageShape({ type: 'single', title: '新页面', subtitle: 'NEW', desc: '', img1: '' })) }
function removePage(i) { cfg.photoPages.splice(i, 1) }
function movePage(i, dir) { const j = i + dir; if (j < 0 || j >= cfg.photoPages.length) return; const t = cfg.photoPages[i]; cfg.photoPages[i] = cfg.photoPages[j]; cfg.photoPages[j] = t }
function setActivePhotoIndex(i) {
const max = Math.max(cfg.photoPages.length - 1, 0)
activePhotoTab.value = String(Math.min(Math.max(i, 0), max))
}
function addPage() {
cfg.photoPages.push(ensurePageShape({ type: 'single', title: '新页面', subtitle: 'NEW', desc: '', img1: '' }))
setActivePhotoIndex(cfg.photoPages.length - 1)
}
function removePage(i) {
cfg.photoPages.splice(i, 1)
setActivePhotoIndex(Math.min(i, cfg.photoPages.length - 1))
}
function movePage(i, dir) {
const j = i + dir
if (j < 0 || j >= cfg.photoPages.length) return
const t = cfg.photoPages[i]; cfg.photoPages[i] = cfg.photoPages[j]; cfg.photoPages[j] = t
setActivePhotoIndex(j)
}
// 流程操作
function addSchedule() { cfg.schedule.push({ time: '', event: '', desc: '' }) }
function removeSchedule(i) { cfg.schedule.splice(i, 1) }
@@ -473,6 +568,40 @@ function shortUrl(url) {
return parts[parts.length - 1] || url
}
async function loadUploadConfig() {
try {
const res = await getUploadConfig()
Object.assign(uploadCfg, defaultUploadConfig(), res.data || {})
uploadCfg.accessKeySecret = ''
} catch (e) {
// 上传设置仅后台使用,读取失败不影响内容编辑。
}
}
async function onSaveUploadConfig() {
uploadConfigSaving.value = true
try {
const payload = {
provider: uploadCfg.provider,
accessKeyId: uploadCfg.accessKeyId,
accessKeySecret: uploadCfg.accessKeySecret,
bucket: uploadCfg.bucket,
folder: uploadCfg.folder,
domain: uploadCfg.domain,
region: uploadCfg.region,
endpoint: uploadCfg.endpoint,
}
const res = await saveUploadConfig(payload)
Object.assign(uploadCfg, defaultUploadConfig(), res.data || {})
uploadCfg.accessKeySecret = ''
message.success('上传设置已保存')
} catch (e) {
message.error(e?.message || '上传设置保存失败')
} finally {
uploadConfigSaving.value = false
}
}
// 保存
async function onSave() {
saving.value = true
@@ -526,14 +655,19 @@ onMounted(async () => {
loaded.musicList = [{ url: loaded.musicUrl, name: '背景音乐' }]
loaded.activeMusicUrl = loaded.musicUrl
}
loaded.endImg = loaded.endImg || loaded.heroImg || cfg.endImg
Object.assign(cfg, loaded)
cfg.photoPages = (cfg.photoPages || []).map(ensurePageShape)
setActivePhotoIndex(currentPhotoIndex.value)
// 数值型参数兜底(兼容旧配置)
cfg.freeScrollInterval = Number(cfg.freeScrollInterval) || 50
cfg.pageSwitchDuration = Number(cfg.pageSwitchDuration) || 800
}
} catch (e) { /* 使用默认配置 */ }
if (adminStore.isAdmin) loadRsvp()
if (adminStore.isAdmin) {
loadRsvp()
loadUploadConfig()
}
})
</script>