From 5fad2f97f83782a65a75e0721d28dc33f6914cdd Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 15 Dec 2025 17:44:10 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=94=AF=E6=8C=81=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E9=9F=B3=E4=B9=90=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/config.json | 35 +- server.js | 86 ++++ src/data/mockData.js | 37 +- src/router/index.js | 48 ++- src/services/configService.js | 133 +++++-- src/utils/musicPlayer.js | 137 +++++++ src/views/AdminPanel.vue | 718 ++++++++++++++++++++++------------ src/views/BattleRanking.vue | 403 ++----------------- 8 files changed, 890 insertions(+), 707 deletions(-) create mode 100644 src/utils/musicPlayer.js diff --git a/data/config.json b/data/config.json index 9da7f4f..a1132d2 100644 --- a/data/config.json +++ b/data/config.json @@ -864,6 +864,10 @@ "role": "manager" } ], + "music": { + "enabled": true, + "filePath": "/uploads/1765791477402.mp3" + }, "displayConfig": { "showBonusModule": true, "individual": { @@ -950,36 +954,5 @@ "battleEndTime": { "date": "2026-02-08", "time": "00:00:00" - }, - "drumConfig": { - "sound": { - "volume": 1, - "frequency1": 150, - "frequency2": 100, - "attackTime": 0.01, - "decayTime": 0.3, - "type1": "sine", - "type2": "triangle", - "enabled": false - }, - "animation": { - "beatInterval": 200, - "beatScale": 1.3, - "beatTranslateY": -15, - "beatRotate": 5, - "idlePulseDuration": 2, - "beatDuration": 100, - "enabled": true - }, - "pattern": { - "strongBeats": [ - 1, - 4 - ], - "totalBeats": 4, - "accentMultiplier": 1.5, - "accentFrequencyOffset": 10, - "accentAnimation": 50 - } } } \ No newline at end of file diff --git a/server.js b/server.js index 20b569b..43bf0fa 100644 --- a/server.js +++ b/server.js @@ -32,6 +32,7 @@ const storage = multer.diskStorage({ } }); +// 文件上传 const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制 @@ -47,6 +48,23 @@ const upload = multer({ } }); +// 音乐上传 +const musicUpload = multer({ + storage, + 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()); @@ -77,6 +95,7 @@ app.post('/api/config', (req, res) => { } }); + // API: 上传图片 app.post('/api/upload', upload.single('image'), (req, res) => { try { @@ -115,6 +134,73 @@ app.delete('/api/upload/:filename', (req, res) => { } }); +// API: 上传音乐 +app.post('/api/music', musicUpload.single('music'), (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: '没有文件上传' }); + } + + // 返回文件的相对路径和完整信息 + const relativePath = `/uploads/${req.file.filename}`; + res.json({ + success: true, + filePath: relativePath, + filename: req.file.filename, + originalName: req.file.originalname, + size: req.file.size + }); + } catch (error) { + console.error('音乐上传失败:', error); + res.status(500).json({ error: error.message || '音乐上传失败' }); + } +}); + +// API: 删除音乐 +app.delete('/api/music/:filename', (req, res) => { + try { + const filename = req.params.filename; + const filePath = path.join(uploadDir, filename); + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + res.json({ success: true }); + } else { + res.status(404).json({ error: '音乐文件不存在' }); + } + } catch (error) { + console.error('音乐删除失败:', error); + res.status(500).json({ error: '音乐删除失败' }); + } +}); + +// API: 获取音乐文件列表 +app.get('/api/music', (req, res) => { + try { + const files = fs.readdirSync(uploadDir) + .filter(file => { + const ext = path.extname(file).toLowerCase(); + return ['.mp3', '.wav', '.ogg', '.m4a'].includes(ext); + }) + .map(file => { + const filePath = path.join(uploadDir, file); + const stats = fs.statSync(filePath); + return { + filename: file, + filePath: `/uploads/${file}`, + size: stats.size, + createdAt: stats.birthtime.toISOString(), + modifiedAt: stats.mtime.toISOString() + }; + }); + + res.json({ success: true, files }); + } catch (error) { + console.error('获取音乐列表失败:', error); + res.status(500).json({ error: '获取音乐列表失败' }); + } +}); + // 处理Vue Router历史模式 - 使用正则表达式代替通配符 app.get(/^((?!\/api).)*$/, (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); diff --git a/src/data/mockData.js b/src/data/mockData.js index a9416f7..c3e488b 100644 --- a/src/data/mockData.js +++ b/src/data/mockData.js @@ -12,10 +12,10 @@ import { saveDisplayConfig as saveDisplayConfigToConfig, getBattleEndTime, saveBattleEndTime as saveBattleEndTimeToConfig, - getDrumConfig, - saveDrumConfig as saveDrumConfigToConfig, getBonusRules, - saveBonusRules as saveBonusRulesToConfig + saveBonusRules as saveBonusRulesToConfig, + getMusicConfig, + saveMusicConfig as saveMusicConfigToConfig } from '../services/configService'; // 初始化空数据占位符,将在initializeData中正确加载 @@ -29,7 +29,8 @@ export let bonusRules = [ export let systemUsers = []; export let displayConfig = null; export let battleEndTime = { date: new Date().toISOString().split('T')[0], time: '00:00:00' }; -export let drumConfig = {}; +export let musicConfig = { enabled: false, filePath: '' }; + // 保存结束时间 export const saveBattleEndTime = async (endTime) => { @@ -59,30 +60,7 @@ export const saveDisplayConfig = async (config) => { return await saveDisplayConfigToConfig(config); }; -// 保存战鼓配置 -export const saveDrumConfig = async (config) => { - console.log('保存战鼓配置:', config); - - // 深度合并配置,确保嵌套对象(如sound、animation、pattern)的属性不会丢失 - drumConfig = { - ...drumConfig, - ...config, - sound: { - ...drumConfig.sound, - ...config.sound - }, - animation: { - ...drumConfig.animation, - ...config.animation - }, - pattern: { - ...drumConfig.pattern, - ...config.pattern - } - }; - - return await saveDrumConfigToConfig(drumConfig); -}; + // 保存奖金规则 export const saveBonusRules = async (rules) => { @@ -118,7 +96,8 @@ export const refreshData = async () => { systemUsers = await getSystemUsers(); displayConfig = await getDisplayConfig(); battleEndTime = await getBattleEndTime(); - drumConfig = await getDrumConfig(); + musicConfig = await getMusicConfig(); + return true; } catch (error) { console.error('刷新数据失败:', error); diff --git a/src/router/index.js b/src/router/index.js index 1036b3d..3cdcb6b 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,7 +1,10 @@ import { createRouter, createWebHistory } from 'vue-router'; -import BattleRanking from '../views/BattleRanking.vue'; -import AdminPanel from '../views/AdminPanel.vue'; +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: '/', @@ -16,23 +19,56 @@ const routes = [ meta: { title: '管理员面板' } }, { - // 捕获所有未匹配的路由,重定向到首页 + // 404路由:未匹配路径重定向到首页 path: '/:pathMatch(.*)*', redirect: '/' } ]; +// 创建路由实例 const router = createRouter({ history: createWebHistory(), routes }); -// 全局前置守卫,设置页面标题 -router.beforeEach((to, from, next) => { - // 设置文档标题 +// 路由守卫:页面切换时控制音乐状态 +router.beforeEach(async (to, from, next) => { + // 1. 进入管理员页面:强制暂停+静音 + if (to.path.startsWith('/admin')) { + musicPlayer.pause(); // 暂停音乐 + musicPlayer.setMuted(true); // 强制静音(管理员页面始终无声音) + next(); + return; + } + + // 2. 进入首页:按配置播放/暂停 + if (to.path === '/') { + 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(); + } + + // 4. 设置页面标题(可选增强) if (to.meta.title) { document.title = to.meta.title; } + next(); }); diff --git a/src/services/configService.js b/src/services/configService.js index f114b56..34ea306 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -102,25 +102,15 @@ const getDefaultConfig = () => ({ } } }, + // ========== 音乐配置默认值(和displayConfig同级) ========== + music: { + enabled: false, + filePath: '' + }, battleEndTime: { date: new Date().toISOString().split('T')[0], time: '00:00:00' }, - drumConfig: { - showDrum: false, // 控制战鼓的显示,默认不显示 - sound: { - volume: 1.0, - enabled: false, // 控制声音播放,默认不播放 - soundSrc: '' // 战鼓声音来源文件路径 - }, - animation: { - enabled: false - }, - pattern: { - strongBeats: [1], - totalBeats: 4 - } - }, backgroundConfig: { useBackgroundImage: true, backgroundImage: '/battle-background.jpg', // 默认战旗背景图片 @@ -332,26 +322,6 @@ export const saveBattleEndTime = async (endTime) => { return await writeConfig(config); }; -/** - * 获取战鼓配置 - * @returns {Object} 战鼓配置 - */ -export const getDrumConfig = async () => { - const config = await readConfig(); - return config.drumConfig || getDefaultConfig().drumConfig; -}; - -/** - * 保存战鼓配置 - * @param {Object} drumConfig 战鼓配置 - * @returns {boolean} 是否保存成功 - */ -export const saveDrumConfig = async (drumConfig) => { - const config = await readConfig(); - config.drumConfig = drumConfig; - return await writeConfig(config); -}; - /** * 获取背景配置 * @returns {Object} 背景配置 @@ -370,4 +340,97 @@ export const saveBackgroundConfig = async (backgroundConfig) => { const config = await readConfig(); config.backgroundConfig = backgroundConfig; return await writeConfig(config); +}; + +/** + * 获取音乐配置 + * @returns {Object} 音乐配置 + */ +export const getMusicConfig = async () => { + const config = await readConfig(); + return config.music || getDefaultConfig().music; +}; + +/** + * 保存音乐配置 + * @param {Object} musicConfig 音乐配置 + * @returns {boolean} 是否保存成功 + */ +export const saveMusicConfig = async (musicConfig) => { + const config = await readConfig(); + config.music = musicConfig; + return await writeConfig(config); +}; + +/** + * 上传音乐 + * @param {File} file 音乐文件 + * @returns {Object} 上传结果 { success: boolean, filePath?: string, filename?: string, originalName?: string, size?: number, error?: string } + */ +export const uploadMusic = async (file) => { + try { + const formData = new FormData(); + formData.append('music', file); + + const response = await fetch('/api/music', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (response.ok) { + return result; + } else { + throw new Error(result.error || '音乐上传失败'); + } + } catch (error) { + console.error('上传音乐失败:', error); + return { success: false, error: error.message }; + } +}; + +/** + * 删除音乐 + * @param {string} filename 文件名 + * @returns {Object} 删除结果 { success: boolean, error?: string } + */ +export const deleteMusic = async (filename) => { + try { + const response = await fetch(`/api/music/${filename}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (response.ok) { + return result; + } else { + throw new Error(result.error || '音乐删除失败'); + } + } catch (error) { + console.error('删除音乐失败:', error); + return { success: false, error: error.message }; + } +}; + +/** + * 获取音乐文件列表 + * @returns {Object} 列表结果 { success: boolean, files?: Array, error?: string } + */ +export const getMusicList = async () => { + try { + const response = await fetch('/api/music'); + + const result = await response.json(); + + if (response.ok) { + return result; + } else { + throw new Error(result.error || '获取音乐列表失败'); + } + } catch (error) { + console.error('获取音乐列表失败:', error); + return { success: false, error: error.message }; + } }; \ No newline at end of file diff --git a/src/utils/musicPlayer.js b/src/utils/musicPlayer.js new file mode 100644 index 0000000..6ead222 --- /dev/null +++ b/src/utils/musicPlayer.js @@ -0,0 +1,137 @@ +// src/utils/musicPlayer.js +class MusicPlayer { + constructor() { + 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() + .then(() => { + this.isPlaying = true; + console.log("音乐播放成功,当前路径:", this.getCurrentPath()); + }) + .catch(err => { + console.error("音乐播放失败(浏览器自动播放限制/路径错误):", err); + this.isPlaying = false; + }); + } + } + + /** + * 暂停音乐(保留原有逻辑) + */ + pause() { + 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 8db4ade..32f1c37 100644 --- a/src/views/AdminPanel.vue +++ b/src/views/AdminPanel.vue @@ -62,6 +62,116 @@ + +
+

🎵 背景音乐配置

+ +
+

🎶 背景音乐设置

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

🎵 已上传音乐列表

+
+
+ {{ music.filename }} +
+ ✅ 当前使用 + +
+
+
+

暂无已上传的音乐文件

+
+ + +
+ +
+ + +
+ +
+ {{ testPlayMsg }} +
+
+ + +
+ +
+ + +

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

+

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

+
+
+
+

⚙️ 显示配置管理

@@ -655,174 +765,9 @@
- -
-

🥁 战鼓配置管理

- -
-

🔊 音效配置

-
-
- -
-
- -
-
- -
-
- -
-

🎵 第一个音调

-
- -
-
- -
-

🎵 第二个音调

-
- -
-
- -
-
-
- - -
-

🎬 动画配置

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- - -
-

🎵 节拍模式配置

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- +
@@ -954,14 +899,26 @@ import { saveDisplayConfig, battleEndTime, saveBattleEndTime, - drumConfig, - saveDrumConfig, refreshData, initializeData } from '../data/mockData.js'; -const router = useRouter(); +import { + getMusicConfig, + saveMusicConfig, + uploadMusic, + deleteMusic, + getMusicList +} from '../services/configService.js'; +import { musicPlayer } from '../utils/musicPlayer.js'; +const router = useRouter(); +// 新增音乐配置变量 +const selectedMusicFile = ref(null); // 选中的MP3文件 +const uploadMsg = ref(''); // 上传提示信息 +const musicEnabled = ref(false); // 首页播放开关状态 +const currentMusicPath = ref(''); // 当前音乐路径 +const musicList = ref([]); // 新增:已上传音乐列表 // 返回首页 const goToHome = () => { router.push('/'); @@ -984,7 +941,7 @@ const tabs = [ { key: 'config', label: '显示配置' }, { key: 'champion', label: '冠军Logo配置' }, { key: 'endTime', label: '结束时间设置' }, - { key: 'drum', label: '战鼓配置' } + { key: 'music', label: '背景音乐设置' } ]; // 冠军Logo配置 @@ -994,6 +951,221 @@ const championLogos = ref({ teamChampionSize: 60, // 默认60px individualChampionSize: 60 // 默认60px }); +// ========== 增强版:音乐相关核心方法 ========== +// 1. 初始化音乐配置 +const initMusicConfig = async () => { + try { + // 获取音乐配置 + const musicConfig = await getMusicConfig(); + musicEnabled.value = musicConfig.enabled || false; + currentMusicPath.value = musicConfig.filePath || ''; + + // 获取音乐文件列表 + const musicListResult = await getMusicList(); + if (musicListResult.success) { + musicList.value = musicListResult.files; + } + } catch (error) { + console.error('初始化音乐配置失败:', error); + } +}; + +// 2. 选择音乐文件(校验格式+友好提示) +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 = ''; + } +}; + +// 3. 上传音乐文件 +const handleMusicUpload = async () => { + if (!selectedMusicFile.value) { + uploadMsg.value = '❌ 请先选择音乐文件!'; + return; + } + + try { + uploadMsg.value = '📤 正在上传...'; + + // 上传音乐文件 + const uploadResult = await uploadMusic(selectedMusicFile.value); + + if (uploadResult.success) { + uploadMsg.value = `✅ 上传成功:${uploadResult.filename}`; + + // 更新当前音乐路径 + currentMusicPath.value = uploadResult.filePath; + + // 刷新音乐列表 + const musicListResult = await getMusicList(); + if (musicListResult.success) { + musicList.value = musicListResult.files; + } + + // 自动启用音乐 + musicEnabled.value = true; + await saveMusicConfig({ + enabled: true, + filePath: uploadResult.filePath + }); + + // 清空选择 + selectedMusicFile.value = null; + + // 清空文件输入 + const fileInput = document.querySelector('input[type="file"][accept=".mp3"]'); + if (fileInput) { + fileInput.value = ''; + } + + setTimeout(() => { + uploadMsg.value = ''; + }, 3000); + } else { + uploadMsg.value = `❌ 上传失败:${uploadResult.error}`; + } + } catch (error) { + console.error('音乐上传失败:', error); + uploadMsg.value = '❌ 上传失败,请重试'; + } +}; + +// 4. 切换音乐文件 +const switchToMusic = async (filePath) => { + try { + currentMusicPath.value = filePath; + await saveMusicConfig({ + enabled: musicEnabled.value, + filePath: filePath + }); + + uploadMsg.value = '✅ 已切换音乐文件'; + setTimeout(() => { + uploadMsg.value = ''; + }, 2000); + } catch (error) { + console.error('切换音乐失败:', error); + uploadMsg.value = '❌ 切换失败,请重试'; + } +}; + +// 5. 删除音乐文件 +const deleteMusicFile = async (filename, event) => { + event.stopPropagation(); // 阻止触发点击切换音乐 + + if (!confirm('确定要删除这个音乐文件吗?')) { + return; + } + + try { + const deleteResult = await deleteMusic(filename); + + if (deleteResult.success) { + // 如果删除的是当前使用的音乐,清空配置 + const deletedFilePath = `/uploads/${filename}`; + if (currentMusicPath.value === deletedFilePath) { + currentMusicPath.value = ''; + musicEnabled.value = false; + await saveMusicConfig({ + enabled: false, + filePath: '' + }); + } + + // 刷新音乐列表 + const musicListResult = await getMusicList(); + if (musicListResult.success) { + musicList.value = musicListResult.files; + } + + uploadMsg.value = '✅ 音乐文件已删除'; + setTimeout(() => { + uploadMsg.value = ''; + }, 2000); + } else { + uploadMsg.value = `❌ 删除失败:${deleteResult.error}`; + } + } catch (error) { + console.error('删除音乐失败:', error); + uploadMsg.value = '❌ 删除失败,请重试'; + } +}; + +// 6. 处理音乐开关切换 +const handleMusicSwitchChange = async () => { + try { + await saveMusicConfig({ + enabled: musicEnabled.value, + filePath: currentMusicPath.value + }); + + uploadMsg.value = musicEnabled.value ? '✅ 背景音乐已开启' : '⏸️ 背景音乐已关闭'; + setTimeout(() => { + uploadMsg.value = ''; + }, 2000); + } catch (error) { + console.error('保存音乐配置失败:', error); + uploadMsg.value = '❌ 操作失败,请重试'; + // 回滚开关状态 + musicEnabled.value = !musicEnabled.value; + } +}; + +// 7. 测试播放音乐 +const testPlayMsg = ref(''); + +const testMusicPlay = () => { + if (!currentMusicPath.value) { + testPlayMsg.value = '❌ 请先选择或上传音乐文件'; + return; + } + + try { + // 初始化音乐播放器(管理员页面强制启用播放) + musicPlayer.initMusicConfig(currentMusicPath.value, true); + musicPlayer.play(); + testPlayMsg.value = '🎵 开始播放音乐(管理员页面测试)'; + + setTimeout(() => { + testPlayMsg.value = ''; + }, 3000); + } catch (error) { + console.error('测试播放失败:', error); + testPlayMsg.value = '❌ 播放失败,请检查音乐文件'; + } +}; + +const testMusicPause = () => { + try { + musicPlayer.pause(); + testPlayMsg.value = '⏸️ 已停止播放'; + + setTimeout(() => { + testPlayMsg.value = ''; + }, 2000); + } catch (error) { + console.error('停止播放失败:', error); + testPlayMsg.value = '❌ 停止失败'; + } +}; // 组件挂载时初始化冠军Logo配置 onMounted(async () => { @@ -1004,30 +1176,26 @@ onMounted(async () => { 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 }; - localDrumConfig.value = { ...drumConfig }; // 初始化冠军Logo配置 if (displayConfig.championLogos) { championLogos.value = { ...displayConfig.championLogos }; } - // 重新处理强拍位置 - if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) { - localDrumConfig.value.pattern.strongBeatsStr = - localDrumConfig.value.pattern.strongBeats.join(',') || '1,4'; - } + // 新增:初始化音乐配置 + await initMusicConfig(); } catch (error) { console.error('初始化数据失败:', error); } }); - // 处理冠军Logo上传 const handleChampionLogoUpload = async (event, type) => { const file = event.target.files[0]; @@ -1138,13 +1306,6 @@ const handleRefreshData = () => { localBonusRules.value = [...bonusRules]; localDisplayConfig.value = { ...displayConfig }; localBattleEndTime.value = { ...battleEndTime }; - localDrumConfig.value = { ...drumConfig }; - - // 重新处理强拍位置 - if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) { - localDrumConfig.value.pattern.strongBeatsStr = - localDrumConfig.value.pattern.strongBeats.join(',') || '1,4'; - } if (success) { alert('数据刷新成功!'); @@ -1164,57 +1325,7 @@ const localTeamRankings = ref([...teamRankings]); const localBonusRules = ref([...bonusRules]); const localDisplayConfig = ref({ ...displayConfig }); const localBattleEndTime = ref({ ...battleEndTime }); -// 初始化本地战鼓配置副本 -const localDrumConfig = ref({ ...drumConfig }); -// 添加强拍位置的字符串表示,用于输入框 -if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) { - localDrumConfig.value.pattern.strongBeatsStr = localDrumConfig.value.pattern.strongBeats.join(',') || '1,4'; -} else { - localDrumConfig.value.pattern = localDrumConfig.value.pattern || {}; - localDrumConfig.value.pattern.strongBeats = [1, 4]; - localDrumConfig.value.pattern.strongBeatsStr = '1,4'; -} -// 组件挂载时初始化数据 -onMounted(async () => { - try { - await initializeData(); - // 重新加载本地数据副本 - localIndividualRankings.value = [...individualRankings]; - localTeamRankings.value = [...teamRankings]; - localBonusRules.value = [...bonusRules]; - localDisplayConfig.value = { ...displayConfig }; - localBattleEndTime.value = { ...battleEndTime }; - localDrumConfig.value = { ...drumConfig }; - - // 重新处理强拍位置 - if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) { - localDrumConfig.value.pattern.strongBeatsStr = - localDrumConfig.value.pattern.strongBeats.join(',') || '1,4'; - } - } catch (error) { - console.error('初始化数据失败:', error); - } -}); - -// 更新强拍位置数组 -const updateStrongBeats = () => { - try { - const beatsStr = localDrumConfig.value.pattern.strongBeatsStr; - if (!beatsStr) { - localDrumConfig.value.pattern.strongBeats = []; - return; - } - const beats = beatsStr.split(',') - .map(beat => parseInt(beat.trim())) - .filter(beat => !isNaN(beat) && beat > 0 && beat <= 8); - localDrumConfig.value.pattern.strongBeats = beats; - } catch (error) { - console.error('更新强拍位置失败:', error); - localDrumConfig.value.pattern.strongBeats = [1, 4]; - localDrumConfig.value.pattern.strongBeatsStr = '1,4'; - } -}; // 对话框状态 const showAddIndividual = ref(false); @@ -1326,12 +1437,6 @@ const saveData = async () => { localIndividualRankings.value.sort((a, b) => b.score - a.score); localTeamRankings.value.sort((a, b) => b.totalScore - a.totalScore); - // 保存战鼓配置前,确保强拍位置数组是最新的 - updateStrongBeats(); - // 移除临时的字符串表示,避免保存到配置中 - const configToSave = { ...localDrumConfig.value }; - delete configToSave.pattern.strongBeatsStr; - // 导入必要的配置服务函数 const { readConfig, writeConfig } = await import('../services/configService'); @@ -1346,7 +1451,11 @@ const saveData = async () => { // 保存冠军Logo配置 currentConfig.displayConfig.championLogos = championLogos.value; currentConfig.battleEndTime = localBattleEndTime.value; - currentConfig.drumConfig = configToSave; + // 保存音乐配置 + currentConfig.music = { + enabled: musicEnabled.value, + filePath: currentMusicPath.value + }; // 一次性保存所有配置 const result = await writeConfig(currentConfig); @@ -2204,4 +2313,129 @@ 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; +} + +/* 新增:已上传音乐列表样式 */ +.music-list-section { + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; +} +.music-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 10px; +} +.music-item { + padding: 10px 15px; + background: white; + border-radius: 4px; + border: 1px solid #eee; + cursor: pointer; + transition: all 0.2s; + display: flex; + justify-content: space-between; + align-items: center; +} +.music-item:hover { + background: #f0f7ff; + border-color: #667eea; +} +.music-item.active { + background: #e6f0ff; + border-color: #667eea; + font-weight: 500; +} + +.music-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: 10px; +} + +.music-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.current-indicator { + font-size: 0.9rem; + color: #667eea; +} + +.btn-delete-music { + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: background-color 0.2s; + font-size: 1rem; +} + +.btn-delete-music:hover { + background-color: #ffebee; +} + \ No newline at end of file diff --git a/src/views/BattleRanking.vue b/src/views/BattleRanking.vue index df79522..3d4bc36 100644 --- a/src/views/BattleRanking.vue +++ b/src/views/BattleRanking.vue @@ -1,5 +1,5 @@ @@ -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 {