1. 音乐播放模式

This commit is contained in:
李琦
2026-08-04 12:54:33 +08:00
parent ef77bb4870
commit 33cea285c1
5 changed files with 252 additions and 31 deletions

View File

@@ -523,11 +523,12 @@
<div class="flex flex-wrap items-start justify-between gap-3 mb-5">
<div>
<div class="text-sm text-[#2c2c2c] tracking-wide">曲目列表</div>
<p class="text-[11px] text-[#8A8680] mt-1 leading-relaxed max-w-md">
访客首次交互后自动播放当前曲目开启随机后24 小时内本地缓存同一首
<p class="text-[11px] text-[#8A8680] mt-1 leading-relaxed max-w-xl">
开启随机后按<strong>权重</strong>抽曲播完自动再随机下一首不单曲循环
可开启记忆曲目让同一访客在设定时长内首次打开仍播同一首
</p>
</div>
<div class="flex items-center gap-3">
<div class="flex flex-wrap items-center gap-3 justify-end">
<label class="inline-flex items-center gap-2 text-[12px] text-[#5c5348] cursor-pointer select-none">
<a-switch v-model:checked="cfg.musicRandomEnabled" size="small" />
随机播放
@@ -540,6 +541,34 @@
</div>
</div>
<div
v-if="cfg.musicRandomEnabled"
class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2 rounded-xl border border-[#e7e3db] bg-[#faf8f5] px-3 py-2.5"
>
<label class="inline-flex items-center gap-2 text-[12px] text-[#5c5348] cursor-pointer select-none">
<a-switch v-model:checked="cfg.musicCacheEnabled" size="small" />
记忆曲目
</label>
<div class="inline-flex items-center gap-2 text-[12px] text-[#5c5348]">
<span :class="{ 'opacity-40': !cfg.musicCacheEnabled }">记忆时长</span>
<a-input-number
v-model:value="cfg.musicCacheHours"
:min="0.5"
:max="720"
:step="0.5"
size="small"
class="!w-[96px]"
:disabled="!cfg.musicCacheEnabled"
/>
<span :class="{ 'opacity-40': !cfg.musicCacheEnabled }" class="text-[#8A8680]">小时</span>
</div>
<span class="text-[10px] text-[#8A8680]">
{{ cfg.musicCacheEnabled
? `同一设备 ${cfg.musicCacheHours || 24} 小时内首次打开沿用上次抽中的歌;播完仍会换下一首。`
: '关闭后每次打开页面都会重新按权重抽曲。' }}
</span>
</div>
<div v-if="!cfg.musicList.length" class="admin-empty">
<div class="admin-empty-icon"><i class="fa-solid fa-music"></i></div>
<div class="text-[13px] text-[#5c5348]">尚未上传音乐</div>
@@ -585,6 +614,26 @@
>正在播放</span>
</div>
<div class="music-chips">
<label
class="music-chip music-chip--weight"
:class="{ 'opacity-40': !cfg.musicRandomEnabled }"
:title="cfg.musicRandomEnabled ? '随机权重,越大越容易抽到' : '开启随机播放后生效'"
>
<i class="fa-solid fa-scale-balanced"></i>
<span>权重</span>
<a-input-number
v-model:value="t.weight"
:min="1"
:max="1000"
:step="1"
size="small"
class="music-weight-input"
:disabled="!cfg.musicRandomEnabled"
/>
<span v-if="cfg.musicRandomEnabled" class="music-weight-pct">
{{ musicWeightPercent(t) }}%
</span>
</label>
<button type="button" class="music-chip" @click="openLrcUpload(t)">
<i class="fa-solid fa-file-lines"></i>
{{ t.lrcUrl ? '已有歌词' : '上传歌词' }}
@@ -1537,6 +1586,7 @@ import MediaPickerModal from '@/components/MediaPickerModal.vue'
import HotelWelcomePosterAdmin from '@/views/admin/HotelWelcomePosterAdmin.vue'
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
import { resolveDanmakuSwatch } from '@/utils/danmakuColors'
import { normalizeMusicWeight, normalizeMusicCacheHours } from '@/utils/musicPick'
import { GIFT_TYPES, GIFT_LABEL_MAP, emptyGiftCosts, DEFAULT_GIFT_COSTS } from '@/utils/giftTypes'
import {
getSlotResizeSpecs,
@@ -1626,7 +1676,7 @@ const SECTION_KEYS = {
'introExitEffect', 'scrollMode', 'freeScrollInterval', 'pageSwitchDuration', 'firstScreenDuration',
'petalEnabled', 'bubbleEnabled', 'introEnabled', 'danmakuEnabled', 'danmakuShowTime', 'nineGridAnimate',
],
music: ['musicList', 'activeMusicUrl', 'musicRandomEnabled'],
music: ['musicList', 'activeMusicUrl', 'musicRandomEnabled', 'musicCacheEnabled', 'musicCacheHours'],
photos: ['photoPages'],
schedule: ['schedule'],
}
@@ -1934,9 +1984,11 @@ function defaultConfig() {
endImg: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=800&q=80',
endImgResized: '',
endImgResizedMeta: '',
musicList: [{ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', name: '背景音乐 1' }],
musicList: [{ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', name: '背景音乐 1', weight: 1 }],
activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
musicRandomEnabled: false,
musicCacheEnabled: true,
musicCacheHours: 24,
danmakuEnabled: true,
danmakuShowTime: true,
nineGridAnimate: false,
@@ -2423,7 +2475,7 @@ function openMusicUpload() {
const name = file.name.replace(/\.[^.]+$/, '') || '背景音乐'
const url = res.url
if (!cfg.musicList) cfg.musicList = []
cfg.musicList.push({ url, name, coverUrl: '', lrcUrl: '' })
cfg.musicList.push({ url, name, coverUrl: '', lrcUrl: '', weight: 1 })
if (!cfg.activeMusicUrl) cfg.activeMusicUrl = url
message.success('音乐上传成功')
} catch (err) {
@@ -2464,6 +2516,13 @@ function removeMusic(i) {
cfg.activeMusicUrl = cfg.musicList[0]?.url || ''
}
}
function musicWeightPercent(track) {
const list = (cfg.musicList || []).filter((t) => t && t.url)
const total = list.reduce((s, t) => s + normalizeMusicWeight(t.weight), 0)
if (!total) return 0
return Math.round((normalizeMusicWeight(track?.weight) / total) * 100)
}
function shortUrl(url) {
if (!url) return ''
const parts = url.split('/')
@@ -2702,7 +2761,16 @@ function pickSectionData(section) {
const keys = SECTION_KEYS[section] || []
const data = {}
for (const k of keys) data[k] = cfg[k]
return JSON.parse(JSON.stringify(data))
const cloned = JSON.parse(JSON.stringify(data))
if (section === 'music' && Array.isArray(cloned.musicList)) {
cloned.musicList = cloned.musicList.map((t) => ({
...t,
weight: normalizeMusicWeight(t?.weight),
}))
cloned.musicCacheEnabled = cloned.musicCacheEnabled !== false
cloned.musicCacheHours = normalizeMusicCacheHours(cloned.musicCacheHours)
}
return cloned
}
function applySectionData(section, loaded) {
@@ -2713,9 +2781,12 @@ function applySectionData(section, loaded) {
...t,
coverUrl: t.coverUrl || '',
lrcUrl: t.lrcUrl || '',
weight: normalizeMusicWeight(t.weight),
}))
}
if (typeof loaded.musicRandomEnabled !== 'boolean') loaded.musicRandomEnabled = false
if (typeof loaded.musicCacheEnabled !== 'boolean') loaded.musicCacheEnabled = true
loaded.musicCacheHours = normalizeMusicCacheHours(loaded.musicCacheHours)
}
if (section === 'photos') {
loaded.photoPages = (loaded.photoPages || []).map(ensurePageShape)
@@ -3904,6 +3975,28 @@ onUnmounted(() => {
border-color: transparent;
background: transparent;
}
.music-chip--weight {
cursor: default;
gap: 6px;
}
.music-chip--weight:hover {
border-color: rgba(168, 140, 107, 0.22);
color: #7a6550;
}
.music-weight-input {
width: 72px !important;
}
.music-weight-input :deep(.ant-input-number-input) {
height: 22px;
font-size: 11px;
padding: 0 4px;
text-align: center;
}
.music-weight-pct {
font-size: 10px;
color: #a88c6b;
min-width: 2.2em;
}
.music-audio {
width: 100%;
max-width: 420px;