feat: 新增音乐控制按钮并完善音乐相关逻辑
This commit is contained in:
@@ -865,7 +865,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"musicConfig": {
|
"musicConfig": {
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"filePath": "/assets/music/background.mp3"
|
||||||
},
|
},
|
||||||
"displayConfig": {
|
"displayConfig": {
|
||||||
"showBonusModule": true,
|
"showBonusModule": true,
|
||||||
@@ -953,5 +954,9 @@
|
|||||||
"battleEndTime": {
|
"battleEndTime": {
|
||||||
"date": "2026-02-08",
|
"date": "2026-02-08",
|
||||||
"time": "00:00:00"
|
"time": "00:00:00"
|
||||||
|
},
|
||||||
|
"music": {
|
||||||
|
"enabled": true,
|
||||||
|
"filePath": "/assets/music/background.mp3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
151
server.js
151
server.js
@@ -13,14 +13,15 @@ const app = express();
|
|||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const CONFIG_FILE_PATH = path.join(__dirname, 'data', 'config.json');
|
const CONFIG_FILE_PATH = path.join(__dirname, 'data', 'config.json');
|
||||||
|
|
||||||
// 创建上传目录
|
// ===================== 原有图片上传配置(保留) =====================
|
||||||
|
// 创建图片上传目录
|
||||||
const uploadDir = path.join(__dirname, 'uploads');
|
const uploadDir = path.join(__dirname, 'uploads');
|
||||||
if (!fs.existsSync(uploadDir)) {
|
if (!fs.existsSync(uploadDir)) {
|
||||||
fs.mkdirSync(uploadDir, { recursive: true });
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置multer
|
// 图片上传multer配置
|
||||||
const storage = multer.diskStorage({
|
const imageStorage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
cb(null, uploadDir);
|
cb(null, uploadDir);
|
||||||
},
|
},
|
||||||
@@ -32,8 +33,8 @@ const storage = multer.diskStorage({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const upload = multer({
|
const imageUpload = multer({
|
||||||
storage,
|
storage: imageStorage,
|
||||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制
|
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制
|
||||||
fileFilter: (req, file, cb) => {
|
fileFilter: (req, file, cb) => {
|
||||||
const allowedTypes = /jpeg|jpg|png|gif/;
|
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(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// 静态文件服务(Vue应用和上传的图片)
|
// 静态文件服务(Vue应用、上传的图片、音乐文件)
|
||||||
app.use(express.static(path.join(__dirname, 'dist')));
|
app.use(express.static(path.join(__dirname, 'dist')));
|
||||||
app.use('/uploads', express.static(uploadDir));
|
app.use('/uploads', express.static(uploadDir));
|
||||||
|
app.use('/assets/music', express.static(musicUploadDir)); // 新增:音乐文件静态访问
|
||||||
|
|
||||||
// API: 获取配置数据
|
// ===================== 原有API(保留) =====================
|
||||||
|
// API: 获取整体配置数据
|
||||||
app.get('/api/config', (req, res) => {
|
app.get('/api/config', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
|
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) => {
|
app.post('/api/config', (req, res) => {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(req.body, null, 2), 'utf8');
|
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: '保存配置文件失败' });
|
res.status(500).json({ error: '保存配置文件失败' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 第78行(新增位置)
|
|
||||||
// API: 获取音乐配置
|
// ===================== 新增:音乐专属API =====================
|
||||||
|
// API: 获取音乐配置(单独返回musicConfig)
|
||||||
app.get('/api/musicConfig', (req, res) => {
|
app.get('/api/musicConfig', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
|
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
|
||||||
const config = JSON.parse(configData);
|
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) {
|
} 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: 上传图片
|
// API: 上传图片
|
||||||
app.post('/api/upload', upload.single('image'), (req, res) => {
|
app.post('/api/upload', imageUpload.single('image'), (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({ error: '没有文件上传' });
|
return res.status(400).json({ error: '没有文件上传' });
|
||||||
@@ -125,16 +239,23 @@ app.delete('/api/upload/:filename', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===================== 前端路由兼容(保留) =====================
|
||||||
// 处理Vue Router历史模式 - 使用正则表达式代替通配符
|
// 处理Vue Router历史模式 - 使用正则表达式代替通配符
|
||||||
app.get(/^((?!\/api).)*$/, (req, res) => {
|
app.get(/^((?!\/api).)*$/, (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===================== 服务器启动(保留+优化) =====================
|
||||||
// 启动服务器并监听错误
|
// 启动服务器并监听错误
|
||||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
const server = app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`服务器运行在 http://localhost:${PORT}`);
|
console.log(`服务器运行在 http://localhost:${PORT}`);
|
||||||
console.log('服务器已成功启动,可以访问 http://localhost:3000');
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听服务器错误
|
// 监听服务器错误
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import BattleRanking from '../views/BattleRanking.vue';
|
import BattleRanking from '../views/BattleRanking.vue'; // 首页组件
|
||||||
import AdminPanel from '../views/AdminPanel.vue';
|
import AdminPanel from '../views/AdminPanel.vue'; // 管理员面板组件
|
||||||
// 新增:引入音乐播放器(写在原有import之后,路由数组之前)
|
import { musicPlayer } from '@/utils/musicPlayer'; // 音乐播放器实例
|
||||||
import { musicPlayer } from '@/utils/musicPlayer';
|
import { getMusicConfig } from '@/services/configService'; // 音乐配置读取服务
|
||||||
|
|
||||||
|
// 路由配置
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -18,32 +19,56 @@ const routes = [
|
|||||||
meta: { title: '管理员面板' }
|
meta: { title: '管理员面板' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 捕获所有未匹配的路由,重定向到首页
|
// 404路由:未匹配路径重定向到首页
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
redirect: '/'
|
redirect: '/'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 创建路由实例
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
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 === '/') {
|
if (to.path === '/') {
|
||||||
musicPlayer.play();
|
try {
|
||||||
} else if (to.path.startsWith('/admin')) {
|
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();
|
musicPlayer.pause();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 全局前置守卫,设置页面标题
|
// 4. 设置页面标题(可选增强)
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
// 设置文档标题
|
|
||||||
if (to.meta.title) {
|
if (to.meta.title) {
|
||||||
document.title = to.meta.title;
|
document.title = to.meta.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// 配置文件API路径
|
// 配置文件API路径
|
||||||
// 修复后(绝对路径,直接请求后端3000端口)
|
// 修复后(绝对路径,直接请求后端3000端口)
|
||||||
const CONFIG_API_URL = 'http://localhost:3000/api/config';
|
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: {
|
battleEndTime: {
|
||||||
date: new Date().toISOString().split('T')[0],
|
date: new Date().toISOString().split('T')[0],
|
||||||
time: '00:00:00'
|
time: '00:00:00'
|
||||||
},
|
},
|
||||||
|
|
||||||
backgroundConfig: {
|
backgroundConfig: {
|
||||||
useBackgroundImage: true,
|
useBackgroundImage: true,
|
||||||
backgroundImage: '/battle-background.jpg', // 默认战旗背景图片
|
backgroundImage: '/battle-background.jpg', // 默认战旗背景图片
|
||||||
@@ -319,8 +327,6 @@ export const saveBattleEndTime = async (endTime) => {
|
|||||||
return await writeConfig(config);
|
return await writeConfig(config);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取背景配置
|
* 获取背景配置
|
||||||
* @returns {Object} 背景配置
|
* @returns {Object} 背景配置
|
||||||
@@ -340,14 +346,104 @@ export const saveBackgroundConfig = async (backgroundConfig) => {
|
|||||||
config.backgroundConfig = backgroundConfig;
|
config.backgroundConfig = backgroundConfig;
|
||||||
return await writeConfig(config);
|
return await writeConfig(config);
|
||||||
};
|
};
|
||||||
// 新增:获取音乐开关配置
|
|
||||||
|
/**
|
||||||
|
* 获取音乐配置(优先读后端接口,失败则兜底本地配置)
|
||||||
|
* @returns {Object} 音乐配置 { enabled, filePath }
|
||||||
|
*/
|
||||||
export const getMusicConfig = async () => {
|
export const getMusicConfig = async () => {
|
||||||
try {
|
try {
|
||||||
// 调用后端新增的/api/musicConfig接口
|
// 优先调用后端音乐配置接口
|
||||||
const response = await fetch('http://localhost:3000/api/musicConfig');
|
const response = await fetch(MUSIC_API_URL);
|
||||||
return await response.json();
|
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) {
|
} catch (error) {
|
||||||
// 报错时返回默认关闭状态
|
console.error('读取音乐配置接口失败,兜底读取本地配置:', error);
|
||||||
return { enabled: false };
|
// 兜底逻辑:读取本地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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,30 +1,137 @@
|
|||||||
// 单例模式:全局唯一的音乐播放器
|
// src/utils/musicPlayer.js
|
||||||
class MusicPlayer {
|
class MusicPlayer {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 创建音频实例,指定背景音乐文件路径(需提前把音乐文件放public/assets/music下)
|
this.audio = null;
|
||||||
this.audio = new Audio('/assets/music/background.mp3');
|
this.isPlaying = false;
|
||||||
// 默认设置循环播放(满足“循环播放”需求)
|
this.defaultPath = "/assets/music/background.mp3";
|
||||||
this.audio.loop = true;
|
this.enabled = false;
|
||||||
// 初始状态:未播放
|
|
||||||
this.isPlaying = 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() {
|
play() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
console.log("首页播放开关未开启,跳过音乐播放");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.audio) {
|
||||||
|
this.initMusicConfig(this.defaultPath, false);
|
||||||
|
console.warn("未初始化音乐配置,使用兜底路径且关闭播放开关");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isPlaying) {
|
if (!this.isPlaying) {
|
||||||
this.audio.play();
|
this.audio.play()
|
||||||
this.isPlaying = true;
|
.then(() => {
|
||||||
|
this.isPlaying = true;
|
||||||
|
console.log("音乐播放成功,当前路径:", this.getCurrentPath());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("音乐播放失败(浏览器自动播放限制/路径错误):", err);
|
||||||
|
this.isPlaying = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暂停音乐(切到管理员页面时执行)
|
/**
|
||||||
|
* 暂停音乐(保留原有逻辑)
|
||||||
|
*/
|
||||||
pause() {
|
pause() {
|
||||||
if (this.isPlaying) {
|
if (this.audio && this.isPlaying) {
|
||||||
this.audio.pause();
|
this.audio.pause();
|
||||||
this.isPlaying = false;
|
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();
|
export const musicPlayer = new MusicPlayer();
|
||||||
@@ -62,6 +62,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div v-if="currentTab === 'config'" class="config-content">
|
||||||
<h2 class="game-subtitle">⚙️ 显示配置管理</h2>
|
<h2 class="game-subtitle">⚙️ 显示配置管理</h2>
|
||||||
@@ -657,7 +714,7 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保存按钮 -->
|
<!-- 保存按钮 -->
|
||||||
<div class="save-section">
|
<div class="save-section">
|
||||||
<button @click="saveData" class="save-btn">💾 保存所有数据</button>
|
<button @click="saveData" class="save-btn">💾 保存所有数据</button>
|
||||||
@@ -792,9 +849,15 @@ import {
|
|||||||
refreshData,
|
refreshData,
|
||||||
initializeData
|
initializeData
|
||||||
} from '../data/mockData.js';
|
} from '../data/mockData.js';
|
||||||
|
// 新增音乐相关依赖引入
|
||||||
|
import { uploadMusicFile, updateMusicConfig, getMusicConfig } from '../services/configService.js';
|
||||||
|
import { musicPlayer } from '@/utils/musicPlayer';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// 新增音乐配置变量
|
||||||
|
const selectedMusicFile = ref(null); // 选中的MP3文件
|
||||||
|
const uploadMsg = ref(''); // 上传提示信息
|
||||||
|
const musicEnabled = ref(false); // 首页播放开关状态
|
||||||
|
const currentMusicPath = ref(''); // 当前音乐路径
|
||||||
// 返回首页
|
// 返回首页
|
||||||
const goToHome = () => {
|
const goToHome = () => {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
@@ -816,7 +879,8 @@ const tabs = [
|
|||||||
{ key: 'bonus', label: '奖金设置' },
|
{ key: 'bonus', label: '奖金设置' },
|
||||||
{ key: 'config', label: '显示配置' },
|
{ key: 'config', label: '显示配置' },
|
||||||
{ key: 'champion', label: '冠军Logo配置' },
|
{ key: 'champion', label: '冠军Logo配置' },
|
||||||
{ key: 'endTime', label: '结束时间设置' }
|
{ key: 'endTime', label: '结束时间设置' },
|
||||||
|
{ key: 'music', label: '背景音乐设置' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 冠军Logo配置
|
// 冠军Logo配置
|
||||||
@@ -826,33 +890,140 @@ const championLogos = ref({
|
|||||||
teamChampionSize: 60, // 默认60px
|
teamChampionSize: 60, // 默认60px
|
||||||
individualChampionSize: 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配置
|
// 组件挂载时初始化冠军Logo配置
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await initializeData();
|
await initializeData();
|
||||||
// 重新加载本地数据副本
|
// 重新加载本地数据副本(合并第二个onMounted的逻辑)
|
||||||
localIndividualRankings.value = [...individualRankings];
|
localIndividualRankings.value = [...individualRankings];
|
||||||
localTeamRankings.value = [...teamRankings];
|
localTeamRankings.value = [...teamRankings];
|
||||||
localBonusRules.value = [...bonusRules];
|
localBonusRules.value = [...bonusRules];
|
||||||
localDisplayConfig.value = { ...displayConfig };
|
localDisplayConfig.value = { ...displayConfig };
|
||||||
|
localBattleEndTime.value = { ...battleEndTime };
|
||||||
|
|
||||||
// 确保皇冠位置配置存在
|
// 确保皇冠位置配置存在
|
||||||
if (!localDisplayConfig.value.crownPosition) {
|
if (!localDisplayConfig.value.crownPosition) {
|
||||||
localDisplayConfig.value.crownPosition = { top: '-100px' };
|
localDisplayConfig.value.crownPosition = { top: '-100px' };
|
||||||
} else if (!localDisplayConfig.value.crownPosition.top) {
|
} else if (!localDisplayConfig.value.crownPosition.top) {
|
||||||
localDisplayConfig.value.crownPosition.top = '-100px';
|
localDisplayConfig.value.crownPosition.top = '-100px';
|
||||||
}
|
}
|
||||||
localBattleEndTime.value = { ...battleEndTime };
|
|
||||||
|
|
||||||
// 初始化冠军Logo配置
|
// 初始化冠军Logo配置
|
||||||
if (displayConfig.championLogos) {
|
if (displayConfig.championLogos) {
|
||||||
championLogos.value = { ...displayConfig.championLogos };
|
championLogos.value = { ...displayConfig.championLogos };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增:初始化音乐配置
|
||||||
|
await initMusicConfig();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化数据失败:', error);
|
console.error('初始化数据失败:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理冠军Logo上传
|
// 处理冠军Logo上传
|
||||||
const handleChampionLogoUpload = async (event, type) => {
|
const handleChampionLogoUpload = async (event, type) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
@@ -983,22 +1154,6 @@ const localBonusRules = ref([...bonusRules]);
|
|||||||
const localDisplayConfig = ref({ ...displayConfig });
|
const localDisplayConfig = ref({ ...displayConfig });
|
||||||
const localBattleEndTime = ref({ ...battleEndTime });
|
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);
|
const showAddIndividual = ref(false);
|
||||||
@@ -1981,4 +2136,61 @@ const deleteBonusRule = (index) => {
|
|||||||
margin: 20px;
|
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>
|
</style>
|
||||||
@@ -203,6 +203,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed, reactive, proxyRefs } from 'vue';
|
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed, reactive, proxyRefs } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
@@ -213,10 +214,12 @@ import {
|
|||||||
battleEndTime,
|
battleEndTime,
|
||||||
initializeData
|
initializeData
|
||||||
} from '../data/mockData.js';
|
} from '../data/mockData.js';
|
||||||
import { readConfig } from '../services/configService.js';
|
import { readConfig, getMusicConfig } from '../services/configService.js';
|
||||||
|
|
||||||
import { getMusicConfig } from '../services/configService.js';
|
|
||||||
import { musicPlayer } from '@/utils/musicPlayer';
|
import { musicPlayer } from '@/utils/musicPlayer';
|
||||||
|
// 新增:选项卡激活状态(控制音乐面板显示/隐藏)
|
||||||
|
const activeTab = ref('');
|
||||||
|
|
||||||
|
|
||||||
// 创建默认显示配置的函数
|
// 创建默认显示配置的函数
|
||||||
function createDefaultDisplayConfig() {
|
function createDefaultDisplayConfig() {
|
||||||
return {
|
return {
|
||||||
@@ -933,14 +936,27 @@ const handleDisplayConfigChange = () => {
|
|||||||
// 在实际项目中,可能需要通过WebSocket或轮询来更新配置
|
// 在实际项目中,可能需要通过WebSocket或轮询来更新配置
|
||||||
|
|
||||||
onUnmounted(() => {
|
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);
|
if (countdownInterval) clearInterval(countdownInterval);
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
|
|
||||||
// 移除拖放相关的事件监听
|
|
||||||
document.removeEventListener('mousemove', drag);
|
document.removeEventListener('mousemove', drag);
|
||||||
document.removeEventListener('mouseup', endDrag);
|
document.removeEventListener('mouseup', endDrag);
|
||||||
|
|
||||||
// 移除触摸事件监听
|
|
||||||
document.removeEventListener('touchmove', touchMove);
|
document.removeEventListener('touchmove', touchMove);
|
||||||
document.removeEventListener('touchend', endTouch);
|
document.removeEventListener('touchend', endTouch);
|
||||||
document.removeEventListener('touchcancel', endTouch);
|
document.removeEventListener('touchcancel', endTouch);
|
||||||
|
|||||||
Reference in New Issue
Block a user