初始化
This commit is contained in:
@@ -79,9 +79,8 @@
|
||||
<a-tab-pane key="visual" tab="视觉与动效">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="背景音乐链接 (URL)">
|
||||
<a-input v-model:value="cfg.musicUrl" placeholder="留空则不播放音乐" />
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">支持 mp3 直链,访客首次交互后自动播放,可随时暂停。</div>
|
||||
<a-form-item label="背景音乐">
|
||||
<div class="text-[12px] text-[#8A8680] leading-relaxed">多首背景音乐的上传、试听与「设为播放」请在 <span class="text-[#a88c6b]">背景音乐</span> 标签页管理。</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="滚动方式">
|
||||
<a-radio-group v-model:value="cfg.scrollMode">
|
||||
@@ -105,6 +104,33 @@
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 背景音乐 -->
|
||||
<a-tab-pane key="music" tab="背景音乐">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<div class="text-[12px] text-[#8A8680] leading-relaxed mb-4">
|
||||
上传多首背景音乐(支持 mp3 等格式),访客首次交互后自动播放当前曲目,可在前台随时切换或暂停。
|
||||
</div>
|
||||
<a-empty v-if="!cfg.musicList.length" description="尚未上传音乐" />
|
||||
<a-card v-for="(t, i) in cfg.musicList" :key="i" :bordered="false"
|
||||
class="!shadow-sm mb-3 !border" :class="t.url === cfg.activeMusicUrl ? '!border-[#a88c6b]' : '!border-[#a88c6b]/15'">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<a-input v-model:value="t.name" placeholder="曲目名称" class="flex-1 min-w-[140px]" />
|
||||
<span class="text-[10px] text-[#8A8680] flex-1 min-w-[140px] truncate">{{ shortUrl(t.url) }}</span>
|
||||
<a-button size="small" :type="t.url === cfg.activeMusicUrl ? 'primary' : 'default'"
|
||||
:class="t.url === cfg.activeMusicUrl ? '!bg-[#a88c6b] !border-[#a88c6b]' : ''"
|
||||
@click="setActiveMusic(t.url)">
|
||||
{{ t.url === cfg.activeMusicUrl ? '播放中' : '设为播放' }}
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="removeMusic(i)"><i class="fa-solid fa-trash"></i></a-button>
|
||||
</div>
|
||||
<audio v-if="t.url" :src="t.url" controls class="w-full mt-3" style="height: 36px;"></audio>
|
||||
</a-card>
|
||||
<a-button class="mt-1" @click="openMusicUpload">
|
||||
<i class="fa-solid fa-upload mr-1 text-[#a88c6b]"></i>上传音乐
|
||||
</a-button>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 相册页 -->
|
||||
<a-tab-pane key="photos" tab="相册页管理">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
@@ -128,8 +154,23 @@
|
||||
<a-select-option value="overlap">错落双图</a-select-option>
|
||||
<a-select-option value="one-large-five-small">一大五小</a-select-option>
|
||||
<a-select-option value="nine-grid">九宫格</a-select-option>
|
||||
<a-select-option value="masonry">瀑布流</a-select-option>
|
||||
<a-select-option value="carousel">轮播滑动</a-select-option>
|
||||
<a-select-option value="polaroid">拍立得拼贴</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="页面高度">
|
||||
<div class="flex gap-2 items-center">
|
||||
<a-select :value="heightSelectValue(page)" @change="(v) => onHeightModeChange(page, v)" class="flex-1">
|
||||
<a-select-option v-for="opt in HEIGHT_PRESETS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number v-if="heightSelectValue(page) === '__custom__'"
|
||||
:value="customVh(page)" @change="(v) => onCustomVh(page, v)"
|
||||
:min="20" :max="200" addon-after="vh" class="w-[150px]" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="小标题 (英文)">
|
||||
<a-input v-model:value="page.subtitle" />
|
||||
</a-form-item>
|
||||
@@ -150,11 +191,18 @@
|
||||
<image-field v-model="page.img2" label="图二" @pick="openUpload(u => page.img2 = u)" :uploading="uploading" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<image-field v-for="(img, iidx) in page.images" :key="iidx"
|
||||
v-model="page.images[iidx]"
|
||||
:label="`图${iidx+1}`"
|
||||
@pick="openUpload(u => page.images[iidx] = u)"
|
||||
:uploading="uploading" />
|
||||
<div v-for="(img, iidx) in page.images" :key="iidx" class="relative">
|
||||
<image-field v-model="page.images[iidx]"
|
||||
:label="`图${iidx+1}`"
|
||||
@pick="openUpload(u => page.images[iidx] = u)"
|
||||
:uploading="uploading" />
|
||||
<button v-if="isFlexible(page.type) && page.images.length > 1"
|
||||
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-[#a88c6b] text-white text-[10px] leading-none flex items-center justify-center shadow"
|
||||
@click="removeImage(page, iidx)" title="移除该图">✕</button>
|
||||
</div>
|
||||
<button v-if="isFlexible(page.type)"
|
||||
class="w-[96px] h-[96px] rounded-xl border border-dashed border-[#a88c6b]/40 text-[#a88c6b] text-[11px] flex items-center justify-center hover:border-[#a88c6b] transition-colors"
|
||||
@click="addImage(page)">+ 添加图片</button>
|
||||
</template>
|
||||
</div>
|
||||
</a-card>
|
||||
@@ -228,13 +276,14 @@ function defaultConfig() {
|
||||
hotel: '杭州星光万丽大酒店', address: '杭州市西湖区星光大道88号\n3楼大宴会厅',
|
||||
formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称',
|
||||
heroImg: 'https://images.unsplash.com/photo-1583939003579-730e3918a45a?auto=format&fit=crop&w=800&q=80',
|
||||
musicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
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,
|
||||
photoPages: [
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: fill(6) },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: fill(9) },
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', height: '100vh' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', height: '100vh' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: fill(6), height: '100vh' },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: fill(9), height: '100vh' },
|
||||
],
|
||||
schedule: [
|
||||
{ time: '16:00', event: '签到迎宾', desc: 'Welcome' },
|
||||
@@ -248,20 +297,56 @@ function fill(n) {
|
||||
return Array.from({ length: n }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80')
|
||||
}
|
||||
|
||||
// 页面高度预设
|
||||
const HEIGHT_PRESETS = [
|
||||
{ label: '满屏(整屏)', value: '100vh' },
|
||||
{ label: '大屏(80%)', value: '80vh' },
|
||||
{ label: '半屏(60%)', value: '60vh' },
|
||||
{ label: '自适应(内容高度)', value: 'auto' },
|
||||
{ label: '自定义', value: '__custom__' },
|
||||
]
|
||||
function heightSelectValue(page) {
|
||||
const match = HEIGHT_PRESETS.find((p) => p.value === page.height)
|
||||
return match ? match.value : '__custom__'
|
||||
}
|
||||
function onHeightModeChange(page, val) {
|
||||
if (val !== '__custom__') page.height = val
|
||||
}
|
||||
function customVh(page) {
|
||||
const m = /^([\d.]+)vh$/.exec(page.height || '')
|
||||
return m ? Number(m[1]) : 100
|
||||
}
|
||||
function onCustomVh(page, val) {
|
||||
page.height = (val || 100) + 'vh'
|
||||
}
|
||||
|
||||
function ensurePageShape(p) {
|
||||
if (p.type === 'single') { p.img1 = p.img1 || ''; delete p.img2; delete p.images }
|
||||
else if (p.type === 'overlap') { p.img1 = p.img1 || ''; p.img2 = p.img2 || ''; delete p.images }
|
||||
else {
|
||||
const need = p.type === 'one-large-five-small' ? 6 : 9
|
||||
// 固定数量版式:一大五小(6) / 九宫格(9);柔性版式:瀑布流/轮播/拍立得,仅首次补齐
|
||||
const fixed = { 'one-large-five-small': 6, 'nine-grid': 9, 'masonry': 6, 'carousel': 5, 'polaroid': 6 }
|
||||
const need = fixed[p.type] || 6
|
||||
p.images = Array.isArray(p.images) ? p.images : []
|
||||
while (p.images.length < need) p.images.push('')
|
||||
if (p.images.length > need) p.images.length = need
|
||||
if (p.images.length === 0) while (p.images.length < need) p.images.push('')
|
||||
else if (['one-large-five-small', 'nine-grid'].includes(p.type)) {
|
||||
while (p.images.length < need) p.images.push('')
|
||||
if (p.images.length > need) p.images.length = need
|
||||
}
|
||||
delete p.img1; delete p.img2
|
||||
}
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.height = p.height || '100vh'
|
||||
return p
|
||||
}
|
||||
|
||||
// 柔性版式(图片数量可增删)
|
||||
function isFlexible(type) {
|
||||
return ['masonry', 'carousel', 'polaroid'].includes(type)
|
||||
}
|
||||
function addImage(page) { if (!Array.isArray(page.images)) page.images = []; page.images.push('') }
|
||||
function removeImage(page, i) { if (page.images.length > 1) page.images.splice(i, 1) }
|
||||
|
||||
// 相册页操作
|
||||
function addPage() { cfg.photoPages.push(ensurePageShape({ type: 'single', title: '新页面', subtitle: 'NEW', desc: '', img1: '' })) }
|
||||
function removePage(i) { cfg.photoPages.splice(i, 1) }
|
||||
@@ -270,7 +355,7 @@ function movePage(i, dir) { const j = i + dir; if (j < 0 || j >= cfg.photoPages.
|
||||
function addSchedule() { cfg.schedule.push({ time: '', event: '', desc: '' }) }
|
||||
function removeSchedule(i) { cfg.schedule.splice(i, 1) }
|
||||
|
||||
// 上传
|
||||
// 上传(图片)
|
||||
function openUpload(onUrl) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'; input.accept = 'image/*'
|
||||
@@ -289,6 +374,42 @@ function openUpload(onUrl) {
|
||||
input.click()
|
||||
}
|
||||
|
||||
// 上传(背景音乐,多首)
|
||||
function openMusicUpload() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'; input.accept = 'audio/*'
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0]; if (!file) return
|
||||
uploading.value = true
|
||||
try {
|
||||
const res = await uploadImage(file) // 后端 /upload 同处理任意文件
|
||||
const name = file.name.replace(/\.[^.]+$/, '') || '背景音乐'
|
||||
const url = res.url
|
||||
if (!cfg.musicList) cfg.musicList = []
|
||||
cfg.musicList.push({ url, name })
|
||||
if (!cfg.activeMusicUrl) cfg.activeMusicUrl = url
|
||||
message.success('音乐上传成功')
|
||||
} catch (err) {
|
||||
message.error('上传失败')
|
||||
} finally {
|
||||
uploading.value = false; e.target.value = ''
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
function setActiveMusic(url) { cfg.activeMusicUrl = url }
|
||||
function removeMusic(i) {
|
||||
cfg.musicList.splice(i, 1)
|
||||
if (cfg.activeMusicUrl === cfg.musicList[i]?.url) {
|
||||
cfg.activeMusicUrl = cfg.musicList[0]?.url || ''
|
||||
}
|
||||
}
|
||||
function shortUrl(url) {
|
||||
if (!url) return ''
|
||||
const parts = url.split('/')
|
||||
return parts[parts.length - 1] || url
|
||||
}
|
||||
|
||||
// 保存
|
||||
async function onSave() {
|
||||
saving.value = true
|
||||
@@ -337,6 +458,11 @@ onMounted(async () => {
|
||||
const res = await getConfig()
|
||||
if (res.data && res.data !== '{}') {
|
||||
const loaded = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
|
||||
// 兼容旧版单链接 musicUrl
|
||||
if (loaded.musicUrl && !(loaded.musicList && loaded.musicList.length)) {
|
||||
loaded.musicList = [{ url: loaded.musicUrl, name: '背景音乐' }]
|
||||
loaded.activeMusicUrl = loaded.musicUrl
|
||||
}
|
||||
Object.assign(cfg, loaded)
|
||||
cfg.photoPages = (cfg.photoPages || []).map(ensurePageShape)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user