1. 礼物特效
This commit is contained in:
@@ -59,6 +59,8 @@ function baseFields(side) {
|
||||
showInviteButton: true,
|
||||
inviteButtonText: '打开请柬',
|
||||
invitePath: '/',
|
||||
/** 背景音乐 URL(从请柬 musicList 中选择;空=不播放) */
|
||||
musicUrl: '',
|
||||
// split 模板字段
|
||||
verticalSlogan: '吾家有喜',
|
||||
subSlogan1: '佳偶天成',
|
||||
@@ -263,6 +265,7 @@ export function normalizePosterConfig(raw, side = 1) {
|
||||
: base.enterEffect,
|
||||
invitePath: loaded.invitePath || '/',
|
||||
inviteButtonText: loaded.inviteButtonText || base.inviteButtonText,
|
||||
musicUrl: typeof loaded.musicUrl === 'string' ? loaded.musicUrl.trim() : '',
|
||||
pageTitle: loaded.pageTitle || base.pageTitle || '',
|
||||
pageDescription: loaded.pageDescription || base.pageDescription || '',
|
||||
qrCenterImg: loaded.qrCenterImg || '',
|
||||
|
||||
@@ -571,6 +571,22 @@
|
||||
<a-input v-model:value="posterCfg.invitePath" placeholder="/" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<a-form-item label="背景音乐">
|
||||
<a-select
|
||||
v-model:value="posterCfg.musicUrl"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="不播放(从已有曲目选择)"
|
||||
:options="posterMusicSelectOptions"
|
||||
:loading="posterMusicLoading"
|
||||
/>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">
|
||||
曲目来自「背景音乐」页;清空则该海报不播放。可到
|
||||
<a class="text-[#a88c6b] cursor-pointer" @click="activeTab = 'music'">背景音乐</a>
|
||||
上传管理。
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="页面标题(浏览器标签 / 分享)">
|
||||
<a-input
|
||||
v-model:value="posterCfg.pageTitle"
|
||||
@@ -1066,6 +1082,9 @@ const resizeProgress = ref('')
|
||||
const uploadConfigSaving = ref(false)
|
||||
const posterSaving = ref(false)
|
||||
const posterLoading = ref(false)
|
||||
const posterMusicLoading = ref(false)
|
||||
/** 海报选曲用:独立缓存请柬 musicList,避免依赖是否打开过音乐页 */
|
||||
const posterMusicOptions = ref([])
|
||||
const tabLoading = ref(false)
|
||||
const rsvpLoading = ref(false)
|
||||
const rsvpList = ref([])
|
||||
@@ -1128,6 +1147,20 @@ const uploadCfg = reactive(defaultUploadConfig())
|
||||
const posterSideTab = ref('1')
|
||||
const posterBundle = reactive(defaultPosterBundle())
|
||||
const posterCfg = computed(() => posterBundle[sideKey(posterSideTab.value)] || posterBundle['1'])
|
||||
const posterMusicSelectOptions = computed(() => {
|
||||
const list = Array.isArray(posterMusicOptions.value) ? posterMusicOptions.value : []
|
||||
const opts = list
|
||||
.filter((t) => t && t.url)
|
||||
.map((t, i) => ({
|
||||
value: t.url,
|
||||
label: t.name || `曲目 ${i + 1}`,
|
||||
}))
|
||||
const cur = posterCfg.value?.musicUrl
|
||||
if (cur && !opts.some((o) => o.value === cur)) {
|
||||
opts.unshift({ value: cur, label: `(原曲目)${cur.slice(0, 36)}${cur.length > 36 ? '…' : ''}` })
|
||||
}
|
||||
return opts
|
||||
})
|
||||
const currentPosterSide = computed(() => posterSideMeta(posterSideTab.value))
|
||||
const posterNameLabels = computed(() => {
|
||||
const s = sideKey(posterSideTab.value)
|
||||
@@ -1578,6 +1611,27 @@ async function onSaveUploadConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPosterMusicOptions() {
|
||||
posterMusicLoading.value = true
|
||||
try {
|
||||
const res = await getConfigSection('music')
|
||||
const list = res.data?.musicList
|
||||
if (Array.isArray(list) && list.length) {
|
||||
posterMusicOptions.value = list.filter((t) => t && t.url)
|
||||
} else if (Array.isArray(cfg.musicList) && cfg.musicList.length) {
|
||||
posterMusicOptions.value = cfg.musicList.filter((t) => t && t.url)
|
||||
} else {
|
||||
posterMusicOptions.value = []
|
||||
}
|
||||
} catch {
|
||||
posterMusicOptions.value = Array.isArray(cfg.musicList)
|
||||
? cfg.musicList.filter((t) => t && t.url)
|
||||
: []
|
||||
} finally {
|
||||
posterMusicLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPosterConfig() {
|
||||
posterLoading.value = true
|
||||
try {
|
||||
@@ -1884,7 +1938,7 @@ async function onTabActivate(tab) {
|
||||
return
|
||||
}
|
||||
if (tab === 'poster') {
|
||||
await loadPosterConfig()
|
||||
await Promise.all([loadPosterConfig(), loadPosterMusicOptions()])
|
||||
return
|
||||
}
|
||||
if (tab === 'rsvp') {
|
||||
|
||||
@@ -92,6 +92,17 @@
|
||||
<FestiveQrCard :url="pageUrl" :size="112" :center-img="cfg.qrCenterImg" compact />
|
||||
</aside>
|
||||
|
||||
<button
|
||||
v-if="hasMusic && phase !== 'loading'"
|
||||
type="button"
|
||||
class="hb-music-fab"
|
||||
:class="{ 'is-playing': audio.isPlaying.value }"
|
||||
:title="audio.isPlaying.value ? '暂停音乐' : '播放音乐'"
|
||||
@click="audio.toggle()"
|
||||
>
|
||||
<i class="fa-solid" :class="audio.isPlaying.value ? 'fa-pause' : 'fa-music'"></i>
|
||||
</button>
|
||||
|
||||
<div v-if="!cfg.enabled && phase === 'content'" class="hb-disabled">
|
||||
<p>海报暂未开放</p>
|
||||
<button type="button" class="hb-cta-btn" @click="goInvite">前往请柬</button>
|
||||
@@ -103,9 +114,10 @@
|
||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { getPosterConfig } from '@/api/wedding'
|
||||
import { getConfig, getPosterConfig } from '@/api/wedding'
|
||||
import { normalizePosterConfig, posterSideMeta, resolvePosterSide } from '@/composables/posterDefaults'
|
||||
import { renderFestiveQr } from '@/composables/renderFestiveQr'
|
||||
import { useAudio } from '@/composables/useAudio'
|
||||
import ClassicPoster from '@/views/hb/templates/ClassicPoster.vue'
|
||||
import SplitPoster from '@/views/hb/templates/SplitPoster.vue'
|
||||
import FestiveQrCard from '@/components/FestiveQrCard.vue'
|
||||
@@ -120,6 +132,8 @@ const contentVisible = ref(false)
|
||||
const savingImg = ref(false)
|
||||
const isPc = ref(false)
|
||||
const pageUrl = computed(() => (typeof window !== 'undefined' ? window.location.href : ''))
|
||||
const audio = useAudio()
|
||||
const hasMusic = computed(() => !!String(cfg.musicUrl || '').trim())
|
||||
let timers = []
|
||||
|
||||
function later(fn, ms) {
|
||||
@@ -242,6 +256,23 @@ async function savePosterImage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function setupPosterMusic() {
|
||||
const url = String(cfg.musicUrl || '').trim()
|
||||
if (!url) return
|
||||
let track = { url, name: '背景音乐' }
|
||||
try {
|
||||
const res = await getConfig()
|
||||
const list = Array.isArray(res.data?.musicList) ? res.data.musicList : []
|
||||
const hit = list.find((t) => t && t.url === url)
|
||||
if (hit) track = { url: hit.url, name: hit.name || '背景音乐', coverUrl: hit.coverUrl || '' }
|
||||
} catch {
|
||||
/* 仅用 poster 上的 url 亦可播放 */
|
||||
}
|
||||
audio.setTracks([track], url)
|
||||
const ok = await audio.attemptAutoplay()
|
||||
if (!ok) audio.armAutoplay()
|
||||
}
|
||||
|
||||
async function runSequence() {
|
||||
const loadMs = cfg.loadingEffect === 'fade' ? 700 : 1100
|
||||
await new Promise((r) => later(r, loadMs))
|
||||
@@ -276,6 +307,7 @@ onMounted(async () => {
|
||||
Object.assign(cfg, normalizePosterConfig(null, side))
|
||||
}
|
||||
applyPageMeta()
|
||||
setupPosterMusic()
|
||||
runSequence()
|
||||
})
|
||||
|
||||
@@ -337,6 +369,36 @@ onUnmounted(() => {
|
||||
pointer-events: none;
|
||||
}
|
||||
.hb-pc-tools > * { pointer-events: auto; }
|
||||
.hb-music-fab {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 30;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 999px;
|
||||
border: 1.5px solid rgba(212, 175, 55, 0.85);
|
||||
background: linear-gradient(160deg, rgba(212, 175, 55, 0.42), rgba(122, 0, 0, 0.82));
|
||||
color: #f0d78c;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.hb-music-fab.is-playing {
|
||||
animation: hb-music-spin 8s linear infinite;
|
||||
}
|
||||
.hb-music-fab.is-playing .fa-pause {
|
||||
animation: none;
|
||||
}
|
||||
@keyframes hb-music-spin {
|
||||
from { filter: brightness(1); }
|
||||
50% { filter: brightness(1.12); }
|
||||
to { filter: brightness(1); }
|
||||
}
|
||||
.hb-save-btn {
|
||||
appearance: none;
|
||||
border: 1.5px solid #d4af37;
|
||||
|
||||
Reference in New Issue
Block a user