diff --git a/data/config.json b/data/config.json index 8271f3a..47cdd25 100644 --- a/data/config.json +++ b/data/config.json @@ -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" } } \ No newline at end of file diff --git a/server.js b/server.js index f339137..3811a88 100644 --- a/server.js +++ b/server.js @@ -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'); }); // 监听服务器错误 diff --git a/src/router/index.js b/src/router/index.js index d0f8621..7c67c0d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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(); + } + } + + // 3. 进入其他页面:暂停音乐 + if (to.path !== '/' && !to.path.startsWith('/admin')) { musicPlayer.pause(); } -}); -// 全局前置守卫,设置页面标题 -router.beforeEach((to, from, next) => { - // 设置文档标题 + // 4. 设置页面标题(可选增强) if (to.meta.title) { document.title = to.meta.title; } + next(); }); diff --git a/src/services/configService.js b/src/services/configService.js index ca705e6..5ed5a0a 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -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; } }; \ No newline at end of file diff --git a/src/utils/musicPlayer.js b/src/utils/musicPlayer.js index e6f83b3..6ead222 100644 --- a/src/utils/musicPlayer.js +++ b/src/utils/musicPlayer.js @@ -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.isPlaying = false; + 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.isPlaying = true; + 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(); \ No newline at end of file diff --git a/src/views/AdminPanel.vue b/src/views/AdminPanel.vue index 205172b..5ebee33 100644 --- a/src/views/AdminPanel.vue +++ b/src/views/AdminPanel.vue @@ -62,6 +62,63 @@ + +
+

🎵 背景音乐配置

+ +
+

🎶 背景音乐设置

+
+ +
+ +
+ + +
+ + + {{ uploadMsg }} + +
+ + +
+ +
+ + +
+ +
+ + +

+ 仅支持MP3格式音频文件,建议文件大小不超过10MB,上传后立即生效 +

+

+ 开关关闭时,首页将停止播放背景音乐;管理员页面始终静音 +

+
+
+
+

⚙️ 显示配置管理

@@ -657,7 +714,7 @@
- +
@@ -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; +} \ No newline at end of file diff --git a/src/views/BattleRanking.vue b/src/views/BattleRanking.vue index 9f683f1..ed9a1fe 100644 --- a/src/views/BattleRanking.vue +++ b/src/views/BattleRanking.vue @@ -203,6 +203,7 @@