chore: 支持背景音乐设置

This commit is contained in:
2025-12-15 17:44:10 +08:00
parent 4a829b7dfc
commit 5fad2f97f8
8 changed files with 890 additions and 707 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div @click.once="handleFirstInteraction">
<!-- 第一部分百日大战主题 - 使用banner0.png图片 -->
<section class="theme-section card-game">
<div class="theme-container">
@@ -7,20 +7,7 @@
</div>
</section>
<!-- 第二部分战鼓动画浮动并支持拖放 -->
<section v-if="localDisplayConfig.showDrum" class="drums-section card-game" @mousedown="startDrag"
@click="handleDrumClick" :style="{ left: drumsPosition.x + 'px', top: drumsPosition.y + 'px' }">
<div class="drums-container">
<!-- 战鼓动画在上面 -->
<div class="drums-animation">
<div class="drum glow-border" :class="{ beating: isBeating }">🥁</div>
<div class="drum" :class="{ beating: isBeating }">🥁</div>
<div class="trophy" style="font-size: 2.5rem; filter: drop-shadow(0 0 10px var(--gold-primary));">🏆</div>
<div class="drum" :class="{ beating: isBeating }">🥁</div>
<div class="drum" :class="{ beating: isBeating }">🥁</div>
</div>
</div>
</section>
<!-- 任务设置模块 -->
<section class="task-settings-section card-game">
@@ -216,6 +203,7 @@
</template>
<script setup>
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed, reactive, proxyRefs } from 'vue';
import { useRouter } from 'vue-router';
import {
@@ -224,10 +212,11 @@ import {
bonusRules,
displayConfig,
battleEndTime,
drumConfig,
initializeData
initializeData,
musicConfig as importedMusicConfig
} from '../data/mockData.js';
import { readConfig } from '../services/configService.js';
import { musicPlayer } from '../utils/musicPlayer';
// 创建默认显示配置的函数
function createDefaultDisplayConfig() {
@@ -414,6 +403,19 @@ const taskSettings = ref({
subtitle: '时间: 2025-11-12 - 2026-02-08'
});
const localMusicConfig = ref({
enabled: true,
filePath: ''
});
// 添加首次交互处理函数
const handleFirstInteraction = () => {
if (localMusicConfig.value.enabled) {
musicPlayer.play();
}
};
// 加载任务设置和初始化所有数据
onBeforeMount(async () => {
try {
@@ -442,6 +444,12 @@ onBeforeMount(async () => {
const configCopy = JSON.parse(JSON.stringify(config.displayConfig));
localDisplayConfig.value = mergeConfig(defaultDisplayConfig, configCopy);
}
if (config.music) {
localMusicConfig.value = config.music;
}
}
} catch (error) {
console.error('加载数据失败:', error);
@@ -692,17 +700,7 @@ const hours = ref(0);
const minutes = ref(0);
const seconds = ref(0);
// 战鼓动画状态
const isBeating = ref(false);
let beatInterval = null;
let countdownInterval = null;
// 音频上下文和战鼓音效
let audioContext = null;
const isPlayingSound = ref(false);
// 战鼓位置状态
const drumsPosition = ref({ x: 20, y: 20 });
// 倒计时位置状态已移除,直接在模板中使用固定位置
// 奖金设置模块位置状态 - 使用reactive存储实际定位值
const bonusPosition = reactive({ x: 'auto', y: 'auto' });
@@ -726,13 +724,7 @@ function throttle(func, limit) {
};
}
// 开始拖动战鼓
const startDrag = (e) => {
isDragging = true;
dragOffset.x = e.clientX - drumsPosition.value.x;
dragOffset.y = e.clientY - drumsPosition.value.y;
e.preventDefault();
};
// 开始拖动奖金模块(鼠标事件)
const startBonusDrag = (e) => {
@@ -881,237 +873,11 @@ const calculateCountdown = () => {
}
};
// 初始化音频上下文
const initAudioContext = () => {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
};
// 音频缓存用于存储加载的MP3文件
const audioBufferCache = ref({});
// 加载MP3文件到音频缓冲区
const loadAudioFile = async (filePath) => {
try {
// 检查是否已缓存
if (audioBufferCache.value[filePath]) {
return audioBufferCache.value[filePath];
}
// 加载音频文件
const response = await fetch(filePath);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// 缓存音频缓冲区
audioBufferCache.value[filePath] = audioBuffer;
return audioBuffer;
} catch (error) {
console.error('加载音频文件失败:', error);
return null;
}
};
// 播放战鼓音效
const playDrumSound = async (isStrongBeat = false) => {
// 检查是否启用声音播放
if (!audioContext || isPlayingSound.value || drumConfig?.sound?.enabled === false) return;
isPlayingSound.value = true;
try {
// 使用配置的音效参数
const soundConfig = drumConfig?.sound || {};
const patternConfig = drumConfig?.pattern || {};
// 检查是否配置了MP3文件路径
if (soundConfig.soundSrc && soundConfig.soundSrc.trim() !== '') {
// 使用MP3文件播放
const audioBuffer = await loadAudioFile(soundConfig.soundSrc);
if (audioBuffer) {
// 创建音频源节点
const source = audioContext.createBufferSource();
const gainNode = audioContext.createGain();
// 连接节点
source.buffer = audioBuffer;
source.connect(gainNode);
gainNode.connect(audioContext.destination);
// 设置音量,支持强拍音量增强
const baseVolume = soundConfig.volume || 1.0;
const accentMultiplier = patternConfig.accentMultiplier || 1.2;
const volume = isStrongBeat ? baseVolume * accentMultiplier : baseVolume;
gainNode.gain.value = volume;
// 播放声音
source.start(0);
// 设置完成后重置播放状态
setTimeout(() => {
isPlayingSound.value = false;
}, audioBuffer.duration * 1000 + 100);
return;
}
}
// 如果没有配置MP3文件或加载失败回退到合成音效
// 创建振荡器节点
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
// 连接节点
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// 设置战鼓音效参数,支持强拍
const baseVolume = soundConfig.volume || 1.0;
// 使用pattern配置中的强拍音量倍数
const accentMultiplier = patternConfig.accentMultiplier || 1.2;
const volume = isStrongBeat ? baseVolume * accentMultiplier : baseVolume;
// 使用soundConfig中的type1
oscillator.type = soundConfig.type1 || 'sine';
// 基础频率,支持强拍频率偏移
const baseFrequency = soundConfig.frequency1 || 150;
const frequencyOffset = isStrongBeat ? (patternConfig.accentFrequencyOffset || 0) / 100 : 0;
const actualFrequency = baseFrequency * (1 + frequencyOffset);
oscillator.frequency.setValueAtTime(actualFrequency, audioContext.currentTime);
// 频率渐变
oscillator.frequency.exponentialRampToValueAtTime(actualFrequency * 0.5, audioContext.currentTime + 0.1);
// 设置音量包络
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + (soundConfig.attackTime || 0.01));
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + (soundConfig.decayTime || 0.3));
// 播放声音
oscillator.start();
oscillator.stop(audioContext.currentTime + (soundConfig.decayTime || 0.3));
// 双音调效果 - 始终使用
const oscillator2 = audioContext.createOscillator();
const gainNode2 = audioContext.createGain();
oscillator2.connect(gainNode2);
gainNode2.connect(audioContext.destination);
oscillator2.type = soundConfig.type2 || 'triangle';
oscillator2.frequency.setValueAtTime(soundConfig.frequency2 || 100, audioContext.currentTime);
gainNode2.gain.setValueAtTime(0, audioContext.currentTime);
gainNode2.gain.linearRampToValueAtTime(volume * 0.8, audioContext.currentTime + (soundConfig.attackTime || 0.01));
gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + (soundConfig.decayTime || 0.3) + 0.2);
oscillator2.start();
oscillator2.stop(audioContext.currentTime + (soundConfig.decayTime || 0.3) + 0.2);
// 设置完成后重置播放状态
setTimeout(() => {
isPlayingSound.value = false;
}, (soundConfig.decayTime || 0.3) * 1000 + 150);
} catch (error) {
console.error('播放战鼓音效出错:', error);
isPlayingSound.value = false;
}
};
// 修改相关函数以支持异步
const handleDrumClick = async () => {
// 如果音频上下文未初始化,初始化它
if (!audioContext) {
initAudioContext();
}
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
audioContext.resume();
}
// 触发战鼓动画和音效,使用配置的点击效果
const animationConfig = drumConfig?.animation || {};
isBeating.value = true;
await playDrumSound(true); // 点击总是强拍
setTimeout(() => {
isBeating.value = false;
}, animationConfig.clickBeatDuration || 250);
};
// 战鼓动画效果
const startDrumAnimation = () => {
// 检查是否显示战鼓
if (drumConfig?.showDrum === false) return;
// 使用配置的动画和节拍参数
const animationConfig = drumConfig?.animation || {};
const patternConfig = drumConfig?.pattern || {};
// 检查是否启用动画
if (animationConfig.enabled === false) return;
let beatCount = 0;
// 使用配置的节拍间隔
const interval = animationConfig.beatInterval || 200;
beatInterval = setInterval(() => {
beatCount++;
// 使用配置的节拍模式和总拍数
const totalBeats = patternConfig.totalBeats || 4;
const currentBeat = ((beatCount - 1) % totalBeats) + 1;
// 根据节拍模式确定是否是强拍
const strongBeats = patternConfig.strongBeats || [1, 4];
const isStrongBeat = strongBeats.includes(currentBeat);
// 执行动画和音效
isBeating.value = true;
// 根据是否是强拍播放音效
playDrumSound(isStrongBeat);
// 设置CSS变量支持强拍动画增强
const drums = document.querySelectorAll('.drum');
drums.forEach(drum => {
// 使用配置的动画参数
drum.style.setProperty('--drum-scale', isStrongBeat ?
(animationConfig.beatScale || 1.3) * (1 + (patternConfig.accentAnimation || 0) / 100) :
(animationConfig.beatScale || 1.3));
drum.style.setProperty('--drum-translate-y', isStrongBeat ?
`${(animationConfig.beatTranslateY || -15) * (1 + (patternConfig.accentAnimation || 0) / 100)}px` :
`${animationConfig.beatTranslateY || -15}px`);
drum.style.setProperty('--drum-rotate', `${animationConfig.beatRotate || 5}deg`);
drum.style.setProperty('--drum-brightness', isStrongBeat ? '1.4' : '1.3');
drum.style.setProperty('--drum-saturation', isStrongBeat ? '1.3' : '1.2');
});
// 根据节拍类型设置持续时间
const beatDuration = isStrongBeat
? (animationConfig.beatDuration || 150)
: (animationConfig.beatDuration || 100);
setTimeout(() => {
isBeating.value = false;
}, beatDuration);
}, interval);
};
// 已移至文件中异步版本的handleDrumClick函数
// 跳转到管理员页面
const goToAdmin = () => {
router.push('/admin');
};
// 监听窗口点击事件,用于用户交互后初始化音频上下文
document.addEventListener('click', initAudioContext, { once: true });
document.addEventListener('touchstart', initAudioContext, { once: true });
const handleResize = () => {
// 计算并设置排名明细区域的最小高度,使其底部与视口对齐
const rankingsSection = document.querySelector('.rankings-section');
@@ -1129,7 +895,14 @@ onMounted(async () => {
try {
// 异步初始化数据
await initializeData();
if (localMusicConfig.value.enabled) {
musicPlayer.initMusicConfig(localMusicConfig.value.filePath, localMusicConfig.value.enabled);
// 注意由于浏览器自动播放策略限制这里不直接调用play()
// 而是等待用户的第一次交互(点击)后再播放
console.log("音乐已准备就绪,等待用户首次交互后播放...");
} else {
musicPlayer.pause();
}
// 更新本地显示配置确保columnAlignments属性存在
if (displayConfig) {
const configCopy = JSON.parse(JSON.stringify(displayConfig));
@@ -1156,7 +929,6 @@ onMounted(async () => {
calculateCountdown();
countdownInterval = setInterval(calculateCountdown, 10); // 改为10ms更新一次以显示毫秒
startDrumAnimation();
// 监听窗口大小变化,确保排名明细与底部对齐
window.addEventListener('resize', handleResize);
handleResize(); // 初始调整
@@ -1186,8 +958,11 @@ const handleDisplayConfigChange = () => {
// 在实际项目中可能需要通过WebSocket或轮询来更新配置
onUnmounted(() => {
// 强制暂停音乐
musicPlayer.pause();
musicPlayer.setMuted(true);
if (countdownInterval) clearInterval(countdownInterval);
if (beatInterval) clearInterval(beatInterval);
window.removeEventListener('resize', handleResize);
// 移除拖放相关的事件监听
@@ -1198,12 +973,6 @@ onUnmounted(() => {
document.removeEventListener('touchmove', touchMove);
document.removeEventListener('touchend', endTouch);
document.removeEventListener('touchcancel', endTouch);
// 清理音频资源
if (audioContext) {
audioContext.close();
audioContext = null;
}
});
</script>
@@ -1725,97 +1494,7 @@ onUnmounted(() => {
}
}
/* 战鼓部分 - 浮动并支持拖放 */
.drums-section {
position: fixed;
left: 20px;
top: 20px;
padding: 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
cursor: move;
z-index: 1000;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transition: box-shadow 0.3s ease;
}
.drums-section:hover {
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.25);
}
.drums-section:active {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.drums-container {
display: flex;
justify-content: center;
}
.drums-animation {
display: flex;
align-items: center;
gap: 20px;
font-size: 3rem;
}
.drum {
transition: transform 0.1s ease, filter 0.1s ease;
animation: idlePulse 2s infinite alternate;
}
.drum.beating {
/* 使用CSS变量方便动态调整 */
--drum-scale: 1.3;
--drum-translate-y: -15px;
--drum-rotate: 5deg;
--drum-brightness: 1.3;
--drum-saturation: 1.2;
transform: scale(var(--drum-scale)) translateY(var(--drum-translate-y)) rotate(var(--drum-rotate));
filter: brightness(var(--drum-brightness)) saturate(var(--drum-saturation));
animation: drumBeat 0.1s ease-in-out;
}
/* 战鼓闲置时的轻微脉动动画 */
@keyframes idlePulse {
0% {
transform: scale(1);
}
100% {
transform: scale(1.05);
}
}
/* 增强跳动效果的关键帧动画 */
@keyframes drumBeat {
0% {
transform: scale(1);
}
50% {
transform: scale(var(--drum-scale, 1.3)) translateY(var(--drum-translate-y, -15px)) rotate(var(--drum-rotate, 5deg));
}
100% {
transform: scale(1);
}
}
.trophy {
animation: bounce 1s infinite alternate;
}
@keyframes bounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-10px);
}
}
/* 按钮样式 */
.btn-game-secondary {
@@ -2320,11 +1999,7 @@ onUnmounted(() => {
height: auto;
}
/* 战鼓部分调整 */
.drums-section {
transform: scale(0.8);
/* 缩小战鼓元素 */
}
/* 2. 倒计时模块调整 - 移至冠军战区上方,缩小时间显示为一行 */
.timer-float {