From abcb53b88eb5f6b4b7473e12fbe3fc06e39ad3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Mon, 3 Aug 2026 13:01:08 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E7=A4=BC=E7=89=A9=E7=89=B9=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/composables/posterDefaults.js | 3 + .../src/views/admin/AdminEditor.vue | 56 +++++++++++++++- vite-tailwindcss/src/views/hb/index.vue | 64 ++++++++++++++++++- 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/vite-tailwindcss/src/composables/posterDefaults.js b/vite-tailwindcss/src/composables/posterDefaults.js index a6548ee..0e0a3e4 100644 --- a/vite-tailwindcss/src/composables/posterDefaults.js +++ b/vite-tailwindcss/src/composables/posterDefaults.js @@ -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 || '', diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index c3cdaf9..5bd65c7 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -571,6 +571,22 @@ + + +
+ 曲目来自「背景音乐」页;清空则该海报不播放。可到 + 背景音乐 + 上传管理。 +
+
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') { diff --git a/vite-tailwindcss/src/views/hb/index.vue b/vite-tailwindcss/src/views/hb/index.vue index 885a288..b7fa775 100644 --- a/vite-tailwindcss/src/views/hb/index.vue +++ b/vite-tailwindcss/src/views/hb/index.vue @@ -92,6 +92,17 @@ + +

海报暂未开放

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