1. 音乐播放模式
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user