feat: 新增音乐控制按钮并完善音乐相关逻辑

This commit is contained in:
Zhukj
2025-12-15 10:43:08 +08:00
parent bc8b63d231
commit 0d23216c74
7 changed files with 663 additions and 81 deletions

View File

@@ -865,7 +865,8 @@
}
],
"musicConfig": {
"enabled": true
"enabled": true,
"filePath": "/assets/music/background.mp3"
},
"displayConfig": {
"showBonusModule": true,
@@ -953,5 +954,9 @@
"battleEndTime": {
"date": "2026-02-08",
"time": "00:00:00"
},
"music": {
"enabled": true,
"filePath": "/assets/music/background.mp3"
}
}

151
server.js
View File

@@ -13,14 +13,15 @@ const app = express();
const PORT = 3000;
const CONFIG_FILE_PATH = path.join(__dirname, 'data', 'config.json');
// 创建上传目录
// ===================== 原有图片上传配置(保留) =====================
// 创建图片上传目录
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 配置multer
const storage = multer.diskStorage({
// 图片上传multer配置
const imageStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
@@ -32,8 +33,8 @@ const storage = multer.diskStorage({
}
});
const upload = multer({
storage,
const imageUpload = multer({
storage: imageStorage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
@@ -47,15 +48,55 @@ const upload = multer({
}
});
// 中间件
// ===================== 新增:音乐上传配置 =====================
// 创建音乐上传目录(对应前端访问路径)
const musicUploadDir = path.join(__dirname, 'public', 'assets', 'music');
// 确保目录存在(不存在则创建)
if (!fs.existsSync(musicUploadDir)) {
fs.mkdirSync(musicUploadDir, { recursive: true });
}
// 音乐上传multer配置
const musicStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, musicUploadDir); // 保存到public/assets/music
},
filename: (req, file, cb) => {
// 保留原文件名+时间戳,避免重复
const timestamp = Date.now();
const ext = path.extname(file.originalname);
const filename = `${timestamp}${ext}`;
cb(null, filename);
}
});
const musicUpload = multer({
storage: musicStorage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB音乐文件限制
fileFilter: (req, file, cb) => {
// 仅允许MP3格式
const allowedTypes = /mp3/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype) || file.mimetype === 'audio/mpeg';
if (extname && mimetype) {
return cb(null, true);
} else {
cb(new Error('只允许上传MP3格式的音频文件'));
}
}
});
// ===================== 中间件(保留+优化) =====================
app.use(cors());
app.use(express.json());
// 静态文件服务Vue应用上传的图片)
// 静态文件服务Vue应用上传的图片、音乐文件
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/uploads', express.static(uploadDir));
app.use('/assets/music', express.static(musicUploadDir)); // 新增:音乐文件静态访问
// API: 获取配置数据
// ===================== 原有API保留 =====================
// API: 获取整体配置数据
app.get('/api/config', (req, res) => {
try {
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
@@ -66,7 +107,7 @@ app.get('/api/config', (req, res) => {
}
});
// API: 保存配置数据
// API: 保存整体配置数据
app.post('/api/config', (req, res) => {
try {
fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(req.body, null, 2), 'utf8');
@@ -76,19 +117,92 @@ app.post('/api/config', (req, res) => {
res.status(500).json({ error: '保存配置文件失败' });
}
});
// 第78行新增位置
// API: 获取音乐配置
// ===================== 新增音乐专属API =====================
// API: 获取音乐配置单独返回musicConfig
app.get('/api/musicConfig', (req, res) => {
try {
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
const config = JSON.parse(configData);
res.json(config.musicConfig || { enabled: false });
// 兜底如果没有musicConfig返回默认值
res.json(config.musicConfig || {
enabled: false,
filePath: '/assets/music/background.mp3'
});
} catch (error) {
res.status(500).json({ enabled: false });
console.error('读取音乐配置失败:', error);
res.status(500).json({
enabled: false,
filePath: '/assets/music/background.mp3'
});
}
});
// API: 更新音乐配置仅更新musicConfig节点不影响其他配置
app.post('/api/musicConfig', (req, res) => {
try {
// 1. 读取原有配置
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
const config = JSON.parse(configData);
// 2. 校验参数
const { enabled, filePath } = req.body;
if (typeof enabled !== 'boolean' || !filePath) {
return res.status(400).json({
success: false,
error: '参数错误enabled必须为布尔值filePath不能为空'
});
}
// 3. 更新musicConfig节点保留其他配置不变
config.musicConfig = { enabled, filePath };
// 4. 写入配置文件
fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf8');
res.json({
success: true,
data: config.musicConfig
});
} catch (error) {
console.error('更新音乐配置失败:', error);
res.status(500).json({
success: false,
error: '更新音乐配置失败'
});
}
});
// API: 上传音乐文件
app.post('/api/upload/music', musicUpload.single('musicFile'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: '没有选择要上传的音乐文件'
});
}
// 返回前端可访问的音乐路径对应public/assets/music
const relativePath = `/assets/music/${req.file.filename}`;
res.json({
success: true,
filePath: relativePath, // 音乐访问路径
filename: req.file.filename,
originalName: req.file.originalname
});
} catch (error) {
console.error('音乐文件上传失败:', error);
res.status(500).json({
success: false,
error: error.message || '音乐文件上传失败'
});
}
});
// ===================== 原有图片上传/删除API保留 =====================
// API: 上传图片
app.post('/api/upload', upload.single('image'), (req, res) => {
app.post('/api/upload', imageUpload.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: '没有文件上传' });
@@ -125,16 +239,23 @@ app.delete('/api/upload/:filename', (req, res) => {
}
});
// ===================== 前端路由兼容(保留) =====================
// 处理Vue Router历史模式 - 使用正则表达式代替通配符
app.get(/^((?!\/api).)*$/, (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// ===================== 服务器启动(保留+优化) =====================
// 启动服务器并监听错误
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
console.log('服务器已成功启动,可以访问 http://localhost:3000');
console.log('API端点: GET/POST /api/config');
console.log('API端点:');
console.log(' - 整体配置: GET/POST /api/config');
console.log(' - 音乐配置: GET/POST /api/musicConfig');
console.log(' - 图片上传: POST /api/upload');
console.log(' - 音乐上传: POST /api/upload/music');
console.log(' - 图片删除: DELETE /api/upload/:filename');
});
// 监听服务器错误

View File

@@ -1,9 +1,10 @@
import { createRouter, createWebHistory } from 'vue-router';
import BattleRanking from '../views/BattleRanking.vue';
import AdminPanel from '../views/AdminPanel.vue';
// 新增引入音乐播放器写在原有import之后路由数组之前
import { musicPlayer } from '@/utils/musicPlayer';
import BattleRanking from '../views/BattleRanking.vue'; // 首页组件
import AdminPanel from '../views/AdminPanel.vue'; // 管理员面板组件
import { musicPlayer } from '@/utils/musicPlayer'; // 音乐播放器实例
import { getMusicConfig } from '@/services/configService'; // 音乐配置读取服务
// 路由配置
const routes = [
{
path: '/',
@@ -18,32 +19,56 @@ const routes = [
meta: { title: '管理员面板' }
},
{
// 捕获所有未匹配的路由,重定向到首页
// 404路由未匹配路径重定向到首页
path: '/:pathMatch(.*)*',
redirect: '/'
}
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes
});
// 新增路由守卫写在router创建完成后export router之前
router.afterEach((to) => {
// 主页面(/)播放,管理员页面(/admin开头暂停
// 路由守卫:页面切换时控制音乐状态
router.beforeEach(async (to, from, next) => {
// 1. 进入管理员页面:强制暂停+静音
if (to.path.startsWith('/admin')) {
musicPlayer.pause(); // 暂停音乐
musicPlayer.setMuted(true); // 强制静音(管理员页面始终无声音)
next();
return;
}
// 2. 进入首页:按配置播放/暂停
if (to.path === '/') {
musicPlayer.play();
} else if (to.path.startsWith('/admin')) {
try {
const musicConfig = await getMusicConfig(); // 读取音乐配置
if (musicConfig.enabled) {
// 初始化音乐路径+开关状态
musicPlayer.initMusicConfig(musicConfig.filePath, musicConfig.enabled);
musicPlayer.setMuted(false); // 首页取消静音
musicPlayer.play(); // 播放音乐
} else {
musicPlayer.pause(); // 开关关闭则暂停
}
} catch (error) {
console.error('首页音乐配置读取失败:', error);
musicPlayer.pause();
}
});
}
// 全局前置守卫,设置页面标题
router.beforeEach((to, from, next) => {
// 设置文档标题
// 3. 进入其他页面:暂停音乐
if (to.path !== '/' && !to.path.startsWith('/admin')) {
musicPlayer.pause();
}
// 4. 设置页面标题(可选增强)
if (to.meta.title) {
document.title = to.meta.title;
}
next();
});

View File

@@ -1,6 +1,9 @@
// 配置文件API路径
// 修复后绝对路径直接请求后端3000端口
const CONFIG_API_URL = 'http://localhost:3000/api/config';
// 新增音乐相关API地址和后端接口对应
const MUSIC_API_URL = 'http://localhost:3000/api/musicConfig';
const MUSIC_UPLOAD_API_URL = 'http://localhost:3000/api/upload/music';
/**
* 读取配置文件
@@ -103,11 +106,16 @@ const getDefaultConfig = () => ({
}
}
},
// ========== 音乐配置默认值和displayConfig同级 ==========
music: {
enabled: false,
filePath: '/assets/music/background.mp3'
},
// ==========================================================
battleEndTime: {
date: new Date().toISOString().split('T')[0],
time: '00:00:00'
},
backgroundConfig: {
useBackgroundImage: true,
backgroundImage: '/battle-background.jpg', // 默认战旗背景图片
@@ -319,8 +327,6 @@ export const saveBattleEndTime = async (endTime) => {
return await writeConfig(config);
};
/**
* 获取背景配置
* @returns {Object} 背景配置
@@ -340,14 +346,104 @@ export const saveBackgroundConfig = async (backgroundConfig) => {
config.backgroundConfig = backgroundConfig;
return await writeConfig(config);
};
// 新增:获取音乐开关配置
/**
* 获取音乐配置(优先读后端接口,失败则兜底本地配置)
* @returns {Object} 音乐配置 { enabled, filePath }
*/
export const getMusicConfig = async () => {
try {
// 调用后端新增的/api/musicConfig接口
const response = await fetch('http://localhost:3000/api/musicConfig');
return await response.json();
// 优先调用后端音乐配置接口
const response = await fetch(MUSIC_API_URL);
if (response.ok) {
const musicConfig = await response.json();
// 基础格式校验
if (typeof musicConfig.enabled !== 'boolean' || !musicConfig.filePath) {
throw new Error('后端音乐配置返回格式异常');
}
return musicConfig;
}
throw new Error(`获取音乐配置失败: ${response.status}`);
} catch (error) {
// 报错时返回默认关闭状态
return { enabled: false };
console.error('读取音乐配置接口失败,兜底读取本地配置:', error);
// 兜底逻辑读取本地config中的music字段
const localConfig = await readConfig();
return localConfig.music || { enabled: false, filePath: '/assets/music/background.mp3' };
}
};
/**
* 上传音乐文件(带返回值格式校验)
* @param {File} file 音乐文件MP3
* @returns {Object} { success: boolean, filePath?: string, error?: string }
*/
export const uploadMusicFile = async (file) => {
try {
// 前置校验:文件类型
if (!file.type.includes('audio/mpeg') && !file.name.endsWith('.mp3')) {
throw new Error('仅支持MP3格式的音乐文件');
}
// 构建FormData适配后端multer.single('musicFile')
const formData = new FormData();
formData.append('musicFile', file);
const response = await fetch(MUSIC_UPLOAD_API_URL, {
method: 'POST',
body: formData // FormData格式无需设置Content-Type浏览器自动处理
});
if (response.ok) {
const result = await response.json();
// 严格校验后端返回格式
if (typeof result.success !== 'boolean') {
throw new Error('音乐上传接口返回格式异常缺失success字段');
}
if (result.success && !result.filePath) {
throw new Error('音乐上传成功但未返回文件路径');
}
return result;
}
throw new Error(`音乐上传失败: ${response.status}`);
} catch (error) {
console.error('上传音乐文件失败:', error);
return { success: false, error: error.message };
}
};
/**
* 更新音乐配置(同步更新后端+本地配置)
* @param {boolean} enabled 是否开启播放
* @param {string} filePath 音乐文件路径
* @returns {boolean} 是否更新成功
*/
export const updateMusicConfig = async (enabled, filePath) => {
try {
// 1. 调用后端接口更新音乐配置
const response = await fetch(MUSIC_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled, filePath })
});
if (response.ok) {
const result = await response.json();
if (result.success) {
// 2. 同步更新本地config中的music字段
const localConfig = await readConfig();
localConfig.music = { enabled, filePath };
await writeConfig(localConfig);
console.log('音乐配置已同步更新到本地');
}
return result.success;
}
throw new Error(`保存音乐配置失败: ${response.status}`);
} catch (error) {
console.error('更新音乐配置失败:', error);
return false;
}
};

View File

@@ -1,30 +1,137 @@
// 单例模式:全局唯一的音乐播放器
// src/utils/musicPlayer.js
class MusicPlayer {
constructor() {
// 创建音频实例指定背景音乐文件路径需提前把音乐文件放public/assets/music下
this.audio = new Audio('/assets/music/background.mp3');
// 默认设置循环播放(满足“循环播放”需求)
this.audio.loop = true;
// 初始状态:未播放
this.audio = null;
this.isPlaying = false;
this.defaultPath = "/assets/music/background.mp3";
this.enabled = false;
}
// 播放音乐(仅当配置开启时执行)
/**
* 适配组件调用的 init 方法(核心:复用 initMusicConfig 逻辑)
* @param {string} path 音乐文件路径(组件中传入的 musicPath
*/
init(path) {
// 组件调用 init 时,复用已有的 initMusicConfig开关状态先传 this.enabled后续组件会通过配置更新
this.initMusicConfig(path, this.enabled);
}
/**
* 原有初始化音乐配置方法(保留,适配动态配置)
* @param {string} filePath 音乐路径
* @param {boolean} enabled 播放开关
*/
initMusicConfig(filePath, enabled) {
this.enabled = enabled;
let validPath = this.defaultPath;
if (filePath && filePath.endsWith('.mp3')) {
validPath = filePath;
} else if (filePath) {
console.warn(`音乐路径无效非MP3格式${filePath},使用兜底路径`);
}
if (this.audio) {
this.audio.pause();
this.audio = null;
}
this.audio = new Audio(validPath);
this.audio.loop = true;
}
/**
* 播放音乐(保留原有逻辑,适配开关)
*/
play() {
if (!this.enabled) {
console.log("首页播放开关未开启,跳过音乐播放");
return;
}
if (!this.audio) {
this.initMusicConfig(this.defaultPath, false);
console.warn("未初始化音乐配置,使用兜底路径且关闭播放开关");
return;
}
if (!this.isPlaying) {
this.audio.play();
this.audio.play()
.then(() => {
this.isPlaying = true;
console.log("音乐播放成功,当前路径:", this.getCurrentPath());
})
.catch(err => {
console.error("音乐播放失败(浏览器自动播放限制/路径错误):", err);
this.isPlaying = false;
});
}
}
// 暂停音乐(切到管理员页面时执行)
/**
* 暂停音乐(保留原有逻辑)
*/
pause() {
if (this.isPlaying) {
if (this.audio && this.isPlaying) {
this.audio.pause();
this.isPlaying = false;
console.log("音乐已暂停");
}
}
/**
* 新增:设置静音/取消静音(适配管理员/首页场景)
* @param {boolean} muted 是否静音
*/
setMuted(muted) {
if (this.audio) {
this.audio.muted = muted;
console.log(muted ? "音乐已静音" : "音乐已取消静音");
}
}
/**
* 新增stop 方法(组件 onUnmounted 调用,暂停+重置进度)
*/
stop() {
if (this.audio) {
this.audio.pause();
this.audio.currentTime = 0; // 重置播放进度到开头
this.isPlaying = false;
console.log("音乐已停止(重置进度)");
}
}
/**
* 新增destroy 方法(组件 onUnmounted 调用,销毁实例释放内存)
*/
destroy() {
this.stop(); // 先停止播放
if (this.audio) {
this.audio = null; // 清空音频实例,释放内存
console.log("音乐实例已销毁,释放内存");
}
}
/**
* 获取当前音乐路径(保留原有逻辑)
* @returns {string} 相对路径
*/
getCurrentPath() {
if (!this.audio) return this.defaultPath;
return this.audio.src.split(window.location.origin)[1] || this.defaultPath;
}
/**
* 更新音乐路径(保留原有逻辑)
* @param {string} newPath 新路径
*/
updateMusicPath(newPath) {
if (!newPath || !newPath.endsWith('.mp3')) {
console.error("更新的音乐路径无效非MP3格式", newPath);
return;
}
this.initMusicConfig(newPath, this.enabled);
console.log("音乐路径已更新为:", newPath);
if (this.enabled && this.isPlaying) {
this.pause();
this.play();
}
}
}
// 导出单例,全局使用同一个播放器
// 导出全局唯一实例
export const musicPlayer = new MusicPlayer();

View File

@@ -62,6 +62,63 @@
</div>
</div>
<!-- 背景音乐配置 -->
<div v-if="currentTab === 'music'" class="music-config-content">
<h2 class="game-subtitle">🎵 背景音乐配置</h2>
<div class="config-section">
<h3 class="text-gold">🎶 背景音乐设置</h3>
<div class="logo-upload-section">
<!-- 第1行背景音乐上传复用Logo上传样式 -->
<div class="config-item">
<label class="checkbox-label">
<span>背景音乐文件</span>
<input type="file" accept=".mp3" @change="handleMusicFileChange" class="logo-input">
</label>
</div>
<!-- 上传按钮 + 状态提示对齐Logo上传控件 -->
<div class="upload-controls" style="margin: 10px 0;">
<button
@click="handleMusicUpload"
:disabled="!selectedMusicFile"
class="btn-clear"
style="margin-right: 10px; background: #667eea;"
>
🎶 上传并应用
</button>
<span v-if="uploadMsg" class="upload-hint" :style="uploadMsg.includes('失败') ? 'color: red;' : 'color: #667eea;'">
{{ uploadMsg }}
</span>
</div>
<!-- 当前音乐路径回显对齐Logo大小配置 -->
<div class="config-item" style="margin: 10px 0;">
<label class="checkbox-label">
<span class="text-gold">当前音乐路径</span>
<input type="text" v-model="currentMusicPath" readonly class="text-input" placeholder="未配置音乐文件">
</label>
</div>
<!-- 第2行首页播放开关对齐Logo配置的复选框样式 -->
<div class="config-item" style="margin: 20px 0;">
<label class="checkbox-label">
<input type="checkbox" v-model="musicEnabled" @change="handleMusicSwitchChange">
<span class="text-gold">开启首页背景音乐播放</span>
</label>
</div>
<!-- 提示文本复用Logo上传的hint样式 -->
<p class="upload-hint">
仅支持MP3格式音频文件建议文件大小不超过10MB上传后立即生效
</p>
<p class="upload-hint">
开关关闭时首页将停止播放背景音乐管理员页面始终静音
</p>
</div>
</div>
</div>
<!-- 显示配置管理 -->
<div v-if="currentTab === 'config'" class="config-content">
<h2 class="game-subtitle"> 显示配置管理</h2>
@@ -792,9 +849,15 @@ import {
refreshData,
initializeData
} from '../data/mockData.js';
// 新增音乐相关依赖引入
import { uploadMusicFile, updateMusicConfig, getMusicConfig } from '../services/configService.js';
import { musicPlayer } from '@/utils/musicPlayer';
const router = useRouter();
// 新增音乐配置变量
const selectedMusicFile = ref(null); // 选中的MP3文件
const uploadMsg = ref(''); // 上传提示信息
const musicEnabled = ref(false); // 首页播放开关状态
const currentMusicPath = ref(''); // 当前音乐路径
// 返回首页
const goToHome = () => {
router.push('/');
@@ -816,7 +879,8 @@ const tabs = [
{ key: 'bonus', label: '奖金设置' },
{ key: 'config', label: '显示配置' },
{ key: 'champion', label: '冠军Logo配置' },
{ key: 'endTime', label: '结束时间设置' }
{ key: 'endTime', label: '结束时间设置' },
{ key: 'music', label: '背景音乐设置' }
];
// 冠军Logo配置
@@ -826,33 +890,140 @@ const championLogos = ref({
teamChampionSize: 60, // 默认60px
individualChampionSize: 60 // 默认60px
});
// ========== 增强版:音乐相关核心方法 ==========
// 1. 选择音乐文件(校验格式+友好提示)
const handleMusicFileChange = (e) => {
const file = e.target.files[0];
if (file) {
// 严格校验MP3格式兼容不同浏览器的MIME类型
const isMp3 = file.type === 'audio/mpeg' || file.type === 'audio/mp3' || file.name.endsWith('.mp3');
if (!isMp3) {
uploadMsg.value = '❌ 仅支持MP3格式音频文件';
selectedMusicFile.value = null;
return;
}
// 校验文件大小10MB限制
if (file.size > 10 * 1024 * 1024) {
uploadMsg.value = '❌ 文件大小超过10MB请选择更小的文件';
selectedMusicFile.value = null;
return;
}
selectedMusicFile.value = file;
uploadMsg.value = `✅ 已选择文件:${file.name}`;
} else {
selectedMusicFile.value = null;
uploadMsg.value = '';
}
};
// 2. 上传并切换音乐文件更新config.json+实时生效)
const handleMusicUpload = async () => {
if (!selectedMusicFile.value) {
uploadMsg.value = '❌ 请先选择MP3文件';
return;
}
uploadMsg.value = '⏳ 正在上传音乐文件...';
try {
// 构建FormData适配后端上传接口
const formData = new FormData();
formData.append('music', selectedMusicFile.value);
// 调用上传接口(替换为你的实际接口地址)
const response = await fetch('/api/upload-music', {
method: 'POST',
body: formData
});
const uploadResult = await response.json();
if (uploadResult.success) {
// 更新config.json的filePath保留开关状态
await updateMusicConfig(musicEnabled.value, uploadResult.filePath);
// 回显新路径
currentMusicPath.value = uploadResult.filePath;
// 实时切换播放器音乐
musicPlayer.updateMusicPath(uploadResult.filePath);
// 若开关开启,立即播放
if (musicEnabled.value) {
musicPlayer.play();
}
uploadMsg.value = '✅ 音乐上传成功!已自动应用到首页';
selectedMusicFile.value = null; // 清空选中文件
} else {
uploadMsg.value = `❌ 上传失败:${uploadResult.error}`;
}
} catch (err) {
uploadMsg.value = `❌ 上传异常:${err.message}`;
}
};
// 3. 切换播放开关更新config.json+实时控制)
const handleMusicSwitchChange = async () => {
try {
// 更新config.json的enabled状态
const updateResult = await updateMusicConfig(musicEnabled.value, currentMusicPath.value);
if (updateResult) {
uploadMsg.value = `✅ 开关已${musicEnabled.value ? '开启' : '关闭'}`;
// 实时控制播放器
if (musicEnabled.value) {
musicPlayer.play();
} else {
musicPlayer.pause();
}
} else {
// 失败时回滚开关状态
musicEnabled.value = !musicEnabled.value;
uploadMsg.value = '❌ 开关更新失败!';
}
} catch (err) {
musicEnabled.value = !musicEnabled.value;
uploadMsg.value = `❌ 开关更新异常:${err.message}`;
}
};
// 4. 初始化音乐配置(页面加载时回显状态)
const initMusicConfig = async () => {
try {
const config = await getMusicConfig();
musicEnabled.value = config.enabled ?? false; // 兼容默认值
currentMusicPath.value = config.filePath || '/assets/music/background.mp3'; // 默认路径
} catch (err) {
uploadMsg.value = `⚠️ 音乐配置初始化失败:${err.message}`;
// 初始化默认值
musicEnabled.value = false;
currentMusicPath.value = '/assets/music/background.mp3';
}
};
// ========== 音乐方法结束 ==========
// 组件挂载时初始化冠军Logo配置
onMounted(async () => {
try {
await initializeData();
// 重新加载本地数据副本
// 重新加载本地数据副本合并第二个onMounted的逻辑
localIndividualRankings.value = [...individualRankings];
localTeamRankings.value = [...teamRankings];
localBonusRules.value = [...bonusRules];
localDisplayConfig.value = { ...displayConfig };
localBattleEndTime.value = { ...battleEndTime };
// 确保皇冠位置配置存在
if (!localDisplayConfig.value.crownPosition) {
localDisplayConfig.value.crownPosition = { top: '-100px' };
} else if (!localDisplayConfig.value.crownPosition.top) {
localDisplayConfig.value.crownPosition.top = '-100px';
}
localBattleEndTime.value = { ...battleEndTime };
// 初始化冠军Logo配置
if (displayConfig.championLogos) {
championLogos.value = { ...displayConfig.championLogos };
}
// 新增:初始化音乐配置
await initMusicConfig();
} catch (error) {
console.error('初始化数据失败:', error);
}
});
// 处理冠军Logo上传
const handleChampionLogoUpload = async (event, type) => {
const file = event.target.files[0];
@@ -983,22 +1154,6 @@ const localBonusRules = ref([...bonusRules]);
const localDisplayConfig = ref({ ...displayConfig });
const localBattleEndTime = ref({ ...battleEndTime });
// 组件挂载时初始化数据
onMounted(async () => {
try {
await initializeData();
// 重新加载本地数据副本
localIndividualRankings.value = [...individualRankings];
localTeamRankings.value = [...teamRankings];
localBonusRules.value = [...bonusRules];
localDisplayConfig.value = { ...displayConfig };
localBattleEndTime.value = { ...battleEndTime };
} catch (error) {
console.error('初始化数据失败:', error);
}
});
// 对话框状态
const showAddIndividual = ref(false);
@@ -1981,4 +2136,61 @@ const deleteBonusRule = (index) => {
margin: 20px;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.top-nav {
flex-direction: column;
gap: 15px;
}
.table-header,
.table-row {
font-size: 0.8rem;
}
.action-col {
flex-direction: column;
}
.modal {
min-width: auto;
margin: 20px;
}
}
// ========== 新增:音乐配置样式适配 ==========
.text-input[readonly] {
background-color: #f8f9fa;
color: #667eea;
cursor: not-allowed;
}
.btn-clear[disabled] {
background-color: #ccc !important;
cursor: not-allowed;
}
.logo-input[type="file"] {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
// ========== 音乐样式结束 ==========
/* 背景音乐配置页面整体样式(补充) */
.music-config-content {
padding: 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 15px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
/* 音乐配置标题样式(补充) */
.music-config-content .game-subtitle {
color: #667eea;
margin-bottom: 25px;
font-size: 1.5rem;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
</style>

View File

@@ -203,6 +203,7 @@
</template>
<script setup>
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed, reactive, proxyRefs } from 'vue';
import { useRouter } from 'vue-router';
import {
@@ -213,10 +214,12 @@ import {
battleEndTime,
initializeData
} from '../data/mockData.js';
import { readConfig } from '../services/configService.js';
import { getMusicConfig } from '../services/configService.js';
import { readConfig, getMusicConfig } from '../services/configService.js';
import { musicPlayer } from '@/utils/musicPlayer';
// 新增:选项卡激活状态(控制音乐面板显示/隐藏)
const activeTab = ref('');
// 创建默认显示配置的函数
function createDefaultDisplayConfig() {
return {
@@ -933,14 +936,27 @@ const handleDisplayConfigChange = () => {
// 在实际项目中可能需要通过WebSocket或轮询来更新配置
onUnmounted(() => {
// 1. 强制暂停+静音(双重保险)
musicPlayer.pause();
musicPlayer.setMuted(true);
// 2. 销毁实例(防止内存泄漏)
if (isMusicInitiated.value) {
musicPlayer.stop();
musicPlayer.destroy();
isMusicInitiated.value = false; // 重置初始化状态
}
// 3. 重置响应式变量
isMusicEnabled.value = false;
musicPath.value = '';
document.removeEventListener('click', unlockMusicPlay);
document.removeEventListener('touchstart', unlockMusicPlay);
if (countdownInterval) clearInterval(countdownInterval);
window.removeEventListener('resize', handleResize);
// 移除拖放相关的事件监听
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', endDrag);
// 移除触摸事件监听
document.removeEventListener('touchmove', touchMove);
document.removeEventListener('touchend', endTouch);
document.removeEventListener('touchcancel', endTouch);