diff --git a/hunliji-api/configsection/section.go b/hunliji-api/configsection/section.go index 7bdce4e..3de70b5 100644 --- a/hunliji-api/configsection/section.go +++ b/hunliji-api/configsection/section.go @@ -35,6 +35,7 @@ var configSectionKeys = map[string][]string{ }, "music": { "musicList", "activeMusicUrl", "musicRandomEnabled", + "musicCacheEnabled", "musicCacheHours", }, "photos": { "photoPages", diff --git a/vite-tailwindcss/src/composables/useAudio.js b/vite-tailwindcss/src/composables/useAudio.js index a957a87..48bb146 100644 --- a/vite-tailwindcss/src/composables/useAudio.js +++ b/vite-tailwindcss/src/composables/useAudio.js @@ -2,7 +2,7 @@ import { ref, computed, onUnmounted } from 'vue' /** * 背景音乐控制 composable(支持多曲目) - * - tracks: [{ url, name, coverUrl?, lrcUrl? }] + * - tracks: [{ url, name, coverUrl?, lrcUrl?, weight? }] * - activeUrl: 当前选中的曲目地址 * - 处理浏览器自动播放限制:首次用户交互后再尝试播放 */ @@ -14,8 +14,10 @@ export function useAudio() { const currentTime = ref(0) const duration = ref(0) const volume = ref(0.5) + const loop = ref(true) let audio = null let unlockHandler = null + let endedHandler = null const currentTrack = computed(() => tracks.value.find((t) => t.url === activeUrl.value) || null) const currentName = computed(() => currentTrack.value?.name || '背景音乐') @@ -32,7 +34,7 @@ export function useAudio() { const ensureAudio = () => { if (audio) return audio audio = new Audio() - audio.loop = true + audio.loop = loop.value audio.volume = volume.value audio.preload = 'auto' audio.addEventListener('canplay', () => { @@ -43,16 +45,27 @@ export function useAudio() { audio.addEventListener('timeupdate', syncMeta) audio.addEventListener('play', () => { isPlaying.value = true }) audio.addEventListener('pause', () => { isPlaying.value = false }) + audio.addEventListener('ended', () => { + if (audio?.loop) return + if (typeof endedHandler === 'function') endedHandler() + }) return audio } const playUrl = async (url) => { if (!url) return false const a = ensureAudio() - if (a.src !== url) { + const abs = (() => { + try { return new URL(url, window.location.href).href } catch { return url } + })() + if (a.src !== abs && a.src !== url) { a.src = url currentTime.value = 0 duration.value = 0 + } else { + // 同曲重播(播完后再次抽中)需归零 + try { a.currentTime = 0 } catch { /* ignore */ } + currentTime.value = 0 } activeUrl.value = url try { @@ -80,7 +93,10 @@ export function useAudio() { else { activeUrl.value = url const a = ensureAudio() - if (a.src !== url) a.src = url + const abs = (() => { + try { return new URL(url, window.location.href).href } catch { return url } + })() + if (a.src !== abs && a.src !== url) a.src = url } } @@ -126,16 +142,29 @@ export function useAudio() { const a = ensureAudio() if (!Number.isFinite(seconds)) return const d = Number.isFinite(a.duration) ? a.duration : duration.value - const next = Math.max(0, Math.min(d || seconds, seconds)) - a.currentTime = next - currentTime.value = next + const nextSec = Math.max(0, Math.min(d || seconds, seconds)) + a.currentTime = nextSec + currentTime.value = nextSec } const setVolume = (v) => { - const next = Math.max(0, Math.min(1, Number(v) || 0)) - volume.value = next + const nextVol = Math.max(0, Math.min(1, Number(v) || 0)) + volume.value = nextVol const a = ensureAudio() - a.volume = next + a.volume = nextVol + } + + const setLoop = (v) => { + loop.value = !!v + if (audio) audio.loop = loop.value + } + + /** 注册播完回调(仅 loop=false 时触发);返回取消函数 */ + const onEnded = (fn) => { + endedHandler = typeof fn === 'function' ? fn : null + return () => { + if (endedHandler === fn) endedHandler = null + } } const armAutoplay = () => { @@ -158,6 +187,7 @@ export function useAudio() { onUnmounted(() => { pause() removeUnlock() + endedHandler = null if (audio) { audio.src = '' audio = null @@ -176,6 +206,7 @@ export function useAudio() { currentTime, duration, volume, + loop, currentIndex, setTracks, setActive, @@ -186,6 +217,9 @@ export function useAudio() { toggle, seek, setVolume, + setLoop, + onEnded, + playUrl, attemptAutoplay, armAutoplay, } diff --git a/vite-tailwindcss/src/utils/musicPick.js b/vite-tailwindcss/src/utils/musicPick.js index 2aeaa08..2f4f8f2 100644 --- a/vite-tailwindcss/src/utils/musicPick.js +++ b/vite-tailwindcss/src/utils/musicPick.js @@ -1,5 +1,19 @@ const STORAGE_KEY = 'wedding_music_pick_v1' -const DAY_MS = 24 * 60 * 60 * 1000 +const HOUR_MS = 60 * 60 * 1000 + +/** 随机权重:默认 1,最小 1,最大 1000 */ +export function normalizeMusicWeight(w) { + const n = Number(w) + if (!Number.isFinite(n) || n <= 0) return 1 + return Math.min(1000, Math.round(n)) +} + +/** 记忆时长(小时):默认 24,范围 0.5~720 */ +export function normalizeMusicCacheHours(h) { + const n = Number(h) + if (!Number.isFinite(n) || n <= 0) return 24 + return Math.min(720, Math.max(0.5, Math.round(n * 10) / 10)) +} function readCache() { try { @@ -20,11 +34,46 @@ export function rememberMusicPick(url) { } catch { /* ignore quota */ } } +export function clearMusicPickCache() { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { /* ignore */ } +} + /** - * Resolve which track to play. - * When random is enabled, reuse local cache within 24h if still in list; otherwise pick randomly. + * 按 weight 加权随机选一首 + * @param {{ excludeUrl?: string }} [opts] excludeUrl:尽量避开当前曲(仅一首时仍可播它) */ -export function pickMusicUrl(musicList, activeMusicUrl, musicRandomEnabled) { +export function pickWeightedMusicUrl(musicList, opts = {}) { + let list = Array.isArray(musicList) ? musicList.filter((t) => t && t.url) : [] + if (!list.length) return '' + const excludeUrl = opts.excludeUrl || '' + if (excludeUrl && list.length > 1) { + const filtered = list.filter((t) => t.url !== excludeUrl) + if (filtered.length) list = filtered + } + let total = 0 + const weights = list.map((t) => { + const w = normalizeMusicWeight(t.weight) + total += w + return w + }) + if (total <= 0) return list[0].url + let r = Math.random() * total + for (let i = 0; i < list.length; i += 1) { + r -= weights[i] + if (r <= 0) return list[i].url + } + return list[list.length - 1].url +} + +/** + * 解析首播曲目。 + * options: + * - cacheEnabled: 是否启用本地记忆 + * - cacheHours: 记忆时长(小时) + */ +export function pickMusicUrl(musicList, activeMusicUrl, musicRandomEnabled, options = {}) { const list = Array.isArray(musicList) ? musicList.filter((t) => t && t.url) : [] if (!list.length) return '' @@ -33,12 +82,18 @@ export function pickMusicUrl(musicList, activeMusicUrl, musicRandomEnabled) { return list[0].url } - const cache = readCache() - if (cache && Date.now() - cache.ts < DAY_MS && list.some((t) => t.url === cache.url)) { - return cache.url + const cacheEnabled = options.cacheEnabled !== false && options.cacheEnabled !== 0 + const cacheHours = normalizeMusicCacheHours(options.cacheHours) + const cacheMs = cacheHours * HOUR_MS + + if (cacheEnabled) { + const cache = readCache() + if (cache && Date.now() - cache.ts < cacheMs && list.some((t) => t.url === cache.url)) { + return cache.url + } } - const picked = list[Math.floor(Math.random() * list.length)].url - rememberMusicPick(picked) + const picked = pickWeightedMusicUrl(list, { excludeUrl: options.excludeUrl }) + if (cacheEnabled) rememberMusicPick(picked) return picked } diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index 84921c6..e904061 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -523,11 +523,12 @@
曲目列表
-

- 访客首次交互后自动播放当前曲目。开启随机后,24 小时内本地缓存同一首。 +

+ 开启随机后按权重抽曲,播完自动再随机下一首(不单曲循环)。 + 可开启「记忆曲目」让同一访客在设定时长内首次打开仍播同一首。

-
+
+
+ +
+ 记忆时长 + + 小时 +
+ + {{ cfg.musicCacheEnabled + ? `同一设备 ${cfg.musicCacheHours || 24} 小时内首次打开沿用上次抽中的歌;播完仍会换下一首。` + : '关闭后每次打开页面都会重新按权重抽曲。' }} + +
+
尚未上传音乐
@@ -585,6 +614,26 @@ >正在播放
+