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 @@
-
+
-
-
+
@@ -216,6 +203,7 @@
@@ -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 {