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 @@
>正在播放
+