1. 礼物特效

This commit is contained in:
李琦
2026-08-03 13:01:08 +08:00
parent 06c57a0754
commit abcb53b88e
3 changed files with 121 additions and 2 deletions

View File

@@ -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 || '',

View File

@@ -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') {

View File

@@ -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;