24 Commits

Author SHA1 Message Date
41268e5a62 chore:任务改为2300万 2026-01-04 10:19:36 +08:00
ddef52e6ce chore: 线上能够播放音乐的版本 2025-12-16 08:29:14 +08:00
2d417cd631 chore: 可以上传播放版本 2025-12-15 18:26:49 +08:00
5f6d43e66b chore: 支持背景音乐音量设置 2025-12-15 17:54:35 +08:00
5fad2f97f8 chore: 支持背景音乐设置 2025-12-15 17:44:10 +08:00
4a829b7dfc Merge pull request 'w2改动了滚动条' (#7) from w2 into server
Reviewed-on: #7
2025-11-29 03:47:46 +00:00
bc62fa197b chore:改动了滚动条 2025-11-29 11:46:08 +08:00
5ed3d67537 Merge branch 'server' of http://git.aigc-quickapp.com/WeWork/vs100 into server 2025-11-29 10:34:00 +08:00
b28275bd14 Merge pull request 'tmp-upload' (#5) from tmp-upload into server
Reviewed-on: #5
2025-11-29 02:33:52 +00:00
Zhukj
d9b066e880 Merge branch '111' into tmp-upload 2025-11-29 10:28:31 +08:00
f9400a76ca Merge branch 'server' of http://git.aigc-quickapp.com/WeWork/vs100 into server 2025-11-29 09:06:49 +08:00
9ed6935707 Merge pull request 'chore:在Desktop(touch)上鼠标可以拖动奖金' (#2) from tmp1 into server
Reviewed-on: #2
Reviewed-by: ZF Sun <admin@noreply.localhost>
2025-11-29 01:05:55 +00:00
7fb210599f chore:在Desktop(touch)上鼠标可以拖动奖金 2025-11-29 08:59:42 +08:00
2eea1fb213 chore:在Desktop(touch)上鼠标可以拖动奖金 2025-11-29 08:36:57 +08:00
6cbcce505a chore:在Desktop(touch)中鼠标可以拖动奖金 2025-11-28 13:56:30 +08:00
e33f227aa0 Merge branch 'server' of http://git.aigc-quickapp.com/WeWork/vs100 into server 2025-11-28 11:49:15 +08:00
Zhukj
6fade7fe0c feat: 添加奖品触摸支持并优化移动端布局 2025-11-28 11:27:16 +08:00
d9e88f757f chore:在Desktop(touch)上鼠标可以拖动奖品 2025-11-28 09:45:04 +08:00
b6f20fd76d Merge branch 'server' of http://git.aigc-quickapp.com/WeWork/vs100 into server 2025-11-28 09:15:57 +08:00
Zhukj
6e1b1d5317 feat:添加奖品模块触摸拖动功能 2025-11-27 18:24:32 +08:00
Zhukj
dbe9c877e0 chore:修改英雄榜排名模块大小,虎狼之师和英雄榜字体大小,战绩图片和总战绩字体大小 2025-11-26 18:26:52 +08:00
Zhukj
027e66c37e refactor:修改战区与英雄模块对齐 2025-11-25 16:35:28 +08:00
7e90289fdd chore(footer): 修改footer文字 2025-11-25 14:43:54 +08:00
2a9153217b chore: 调整虎狼之师、英雄榜图片 2025-11-25 13:47:36 +08:00
12 changed files with 1323 additions and 858 deletions

View File

@@ -864,6 +864,11 @@
"role": "manager"
}
],
"music": {
"enabled": true,
"filePath": "",
"volume": "1"
},
"displayConfig": {
"showBonusModule": true,
"individual": {
@@ -950,36 +955,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
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 508 KiB

View File

@@ -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'));

View File

@@ -25,7 +25,7 @@
<!-- 页脚 -->
<footer class="game-footer">
<div class="footer-content">
<p>&copy; 2025-2026 聚上集团 | 云上企. 所有权利保留.</p>
<p>&copy; 2025-2026 聚上集团 | 云上企 版权所有.</p>
</div>
</footer>
</div>
@@ -108,7 +108,28 @@
.footer-content {
font-size: 14px;
text-shadow: 1px 1px 10px black;
/* 雕刻效果使用多层text-shadow创建凸起感 */
text-shadow:
-1px -1px 0 rgba(255, 255, 255, 0.3), /* 高光 - 左上 */
1px 1px 0 rgba(0, 0, 0, 0.5), /* 阴影 - 右下 */
0 0 5px rgba(17, 17, 17, 0.5); /* 光晕 */
/* 背景和内边距 */
padding: 8px 16px;
border-radius: 4px;
/* 添加轻微的3D效果 */
transform: perspective(1000px) rotateX(0deg);
transition: all 0.3s ease;
}
/* 鼠标悬停时增强3D效果 */
.footer-content:hover {
transform: perspective(1000px) rotateX(2deg) translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
text-shadow:
-1px -1px 0 rgba(255, 255, 255, 0.4),
1px 1px 0 rgba(0, 0, 0, 0.6),
0 0 8px rgba(15, 15, 15, 0.7);
}
/* 针对1920x1080分辨率的banner精确定位 */
@@ -172,5 +193,20 @@
.app-logo {
width: 200px; /* 移动设备上设置固定宽度为200px */
}
/* 移动设备上的页脚雕刻效果适配 */
.footer-content {
font-size: 12px;
padding: 6px 12px;
text-shadow:
-1px -1px 0 rgba(255, 255, 255, 0.2),
1px 1px 0 rgba(0, 0, 0, 0.4),
0 0 3px rgba(255, 215, 0, 0.4);
}
.footer-content:hover {
transform: none; /* 移动设备上禁用悬停效果 */
box-shadow: none;
}
}
</style>

View File

@@ -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);

View File

@@ -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();
});

View File

@@ -102,25 +102,16 @@ const getDefaultConfig = () => ({
}
}
},
// ========== 音乐配置默认值和displayConfig同级 ==========
music: {
enabled: false,
filePath: '',
volume: 0.5 // 默认音量50%
},
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 +323,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 +341,102 @@ 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();
// 确保音量是数字类型
const normalizedMusicConfig = {
...musicConfig,
volume: typeof musicConfig.volume === 'string' ? parseFloat(musicConfig.volume) : musicConfig.volume
};
config.music = normalizedMusicConfig;
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 };
}
};

182
src/utils/musicPlayer.js Normal file
View File

@@ -0,0 +1,182 @@
// src/utils/musicPlayer.js
class MusicPlayer {
constructor() {
this.audio = null;
this.isPlaying = false;
this.defaultPath = "";
this.enabled = false;
this.volume = 0.5; // 默认音量50%
}
/**
* 适配组件调用的 init 方法(核心:复用 initMusicConfig 逻辑)
* @param {string} path 音乐文件路径(组件中传入的 musicPath
*/
init(path) {
// 组件调用 init 时,复用已有的 initMusicConfig开关状态先传 this.enabled后续组件会通过配置更新
this.initMusicConfig(path, this.enabled, this.volume);
}
/**
* 原有初始化音乐配置方法(保留,适配动态配置)
* @param {string} filePath 音乐路径
* @param {boolean} enabled 播放开关
*/
initMusicConfig(filePath, enabled, volume = 0.5) {
console.log("初始化音乐配置:", { filePath, enabled, volume });
this.enabled = enabled;
// 确保音量是数字类型
this.volume = typeof volume === 'string' ? parseFloat(volume) : volume;
console.log("处理后的音量值:", this.volume, "类型:", typeof this.volume);
let validPath = this.defaultPath;
if (filePath && filePath.endsWith('.mp3')) {
validPath = filePath;
} else if (filePath) {
console.warn(`音乐路径无效非MP3格式${filePath},使用兜底路径`);
}
console.log("使用的音乐路径:", validPath);
if (this.audio) {
this.audio.pause();
this.audio = null;
}
this.audio = new Audio(validPath);
this.audio.loop = true;
this.audio.volume = this.volume;
console.log("音频对象创建完成,音量设置为:", this.audio.volume);
}
/**
* 播放音乐(保留原有逻辑,适配开关)
*/
play() {
console.log("调用 play 方法,当前状态:", { enabled: this.enabled, hasAudio: !!this.audio, isPlaying: this.isPlaying });
if (!this.enabled) {
console.log("首页播放开关未开启,跳过音乐播放");
return;
}
if (!this.audio) {
console.warn("音频对象未初始化");
this.initMusicConfig(this.defaultPath, false);
console.warn("未初始化音乐配置,使用兜底路径且关闭播放开关");
return;
}
console.log("音频源路径:", this.audio.src);
console.log("音频就绪状态:", this.audio.readyState);
if (!this.isPlaying) {
this.audio.play()
.then(() => {
this.isPlaying = true;
console.log("音乐播放成功,当前路径:", this.getCurrentPath());
})
.catch(err => {
console.error("音乐播放失败(浏览器自动播放限制/路径错误):", err);
console.error("错误详情:", {
name: err.name,
message: err.message,
code: err.code
});
this.isPlaying = false;
});
} else {
console.log("音乐已在播放中");
}
}
/**
* 暂停音乐(保留原有逻辑)
*/
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 ? "音乐已静音" : "音乐已取消静音");
}
}
/**
* 设置音量
* @param {number} volume 音量值 (0.0 到 1.0)
*/
setVolume(volume) {
if (this.audio) {
// 确保音量是数字类型
const numericVolume = typeof volume === 'string' ? parseFloat(volume) : volume;
// 限制音量范围在0.0到1.0之间
this.volume = Math.max(0, Math.min(1, numericVolume));
this.audio.volume = this.volume;
console.log(`音乐音量已设置为: ${Math.round(this.volume * 100)}%`);
}
}
/**
* 新增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, this.volume);
console.log("音乐路径已更新为:", newPath);
if (this.enabled && this.isPlaying) {
this.pause();
this.play();
}
}
}
// 导出全局唯一实例
export const musicPlayer = new MusicPlayer();

View File

@@ -62,6 +62,124 @@
</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>
<!-- 新增已上传音乐列表 -->
<div class="music-list-section" style="margin: 20px 0;">
<h4 class="text-gold">🎵 已上传音乐列表</h4>
<div class="music-list" v-if="musicList.length > 0">
<div
class="music-item"
v-for="music in musicList"
:key="music.filePath"
:class="{ active: music.filePath === currentMusicPath }"
@click="switchToMusic(music.filePath)"
>
<span class="music-name">{{ music.filename }}</span>
<div class="music-actions">
<span v-if="music.filePath === currentMusicPath" class="current-indicator"> 当前使用</span>
<button
@click="deleteMusicFile(music.filename, $event)"
class="btn-delete-music"
title="删除音乐文件"
>
🗑
</button>
</div>
</div>
</div>
<p v-else class="upload-hint">暂无已上传的音乐文件</p>
</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>
<!-- 音乐测试播放控件 -->
<div class="config-item" style="margin: 15px 0;">
<label class="checkbox-label">
<span class="text-gold">音乐测试播放</span>
<button
@click="testMusicPlay"
:disabled="!currentMusicPath"
class="btn-game"
style="margin-right: 10px; padding: 8px 16px; font-size: 14px;"
>
🎵 测试播放
</button>
<button
@click="testMusicPause"
:disabled="!currentMusicPath"
class="btn-game-secondary"
style="padding: 8px 16px; font-size: 14px;"
>
停止播放
</button>
</label>
<div v-if="testPlayMsg" class="upload-hint" :style="testPlayMsg.includes('失败') ? 'color: red;' : 'color: #667eea;'">
{{ testPlayMsg }}
</div>
</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>
<!-- 音量控制滑块 -->
<div class="config-item" style="margin: 15px 0;">
<label class="checkbox-label">
<span class="text-gold">音乐音量调节</span>
<input type="range" min="0" max="1" step="0.01" v-model="musicVolume" @input="handleMusicVolumeChange" class="volume-slider">
<span class="volume-value">{{ Math.round(musicVolume * 100) }}%</span>
</label>
</div>
<!-- 提示文本复用Logo上传的hint样式 -->
<p class="upload-hint">
仅支持MP3格式音频文件建议文件大小不超过10MB上传后立即生效
</p>
<p class="upload-hint">
开关关闭时首页将停止播放背景音乐管理员页面始终静音
</p>
</div>
</div>
</div>
<!-- 显示配置管理 -->
<div v-if="currentTab === 'config'" class="config-content">
<h2 class="game-subtitle"> 显示配置管理</h2>
@@ -655,174 +773,9 @@
</div>
</div>
<!-- 战鼓配置 -->
<div v-if="currentTab === 'drum'" class="drum-config-content">
<h2 class="game-subtitle">🥁 战鼓配置管理</h2>
<!-- 音效配置 -->
<div class="config-section">
<h3 class="text-gold">🔊 音效配置</h3>
<div class="config-options">
<div class="config-item">
<label class="checkbox-label">
<input type="checkbox" v-model="localDrumConfig.sound.enabled">
<span>启用音效</span>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>音量 (0-1)</span>
<input type="number" v-model.number="localDrumConfig.sound.volume" min="0" max="1" step="0.1"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>起音时间 (s)</span>
<input type="number" v-model.number="localDrumConfig.sound.attackTime" min="0.001" max="0.5"
step="0.01" class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>衰减时间 (s)</span>
<input type="number" v-model.number="localDrumConfig.sound.decayTime" min="0.05" max="1" step="0.05"
class="width-input">
</label>
</div>
<h4 style="margin-top: 15px; color: #666;">🎵 第一个音调</h4>
<div class="config-item">
<label class="checkbox-label">
<span>音调类型</span>
<select v-model="localDrumConfig.sound.type1" class="select-input">
<option value="sine">正弦波</option>
<option value="square">方波</option>
<option value="triangle">三角波</option>
<option value="sawtooth">锯齿波</option>
</select>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>频率 (Hz)</span>
<input type="number" v-model.number="localDrumConfig.sound.frequency1" min="50" max="500"
class="width-input">
</label>
</div>
<h4 style="margin-top: 15px; color: #666;">🎵 第二个音调</h4>
<div class="config-item">
<label class="checkbox-label">
<span>音调类型</span>
<select v-model="localDrumConfig.sound.type2" class="select-input">
<option value="sine">正弦波</option>
<option value="square">方波</option>
<option value="triangle">三角波</option>
<option value="sawtooth">锯齿波</option>
</select>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>频率 (Hz)</span>
<input type="number" v-model.number="localDrumConfig.sound.frequency2" min="50" max="500"
class="width-input">
</label>
</div>
</div>
</div>
<!-- 动画配置 -->
<div class="config-section">
<h3>🎬 动画配置</h3>
<div class="config-options">
<div class="config-item">
<label class="checkbox-label">
<input type="checkbox" v-model="localDrumConfig.animation.enabled">
<span>启用动画</span>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>节拍间隔 (ms)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatInterval" min="50" max="1000"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>跳动缩放比例</span>
<input type="number" v-model.number="localDrumConfig.animation.beatScale" min="1.0" max="2.0"
step="0.1" class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>跳动上下位移 (px)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatTranslateY" min="-50" max="50"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>跳动旋转角度 (deg)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatRotate" min="0" max="20"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>单次跳动持续时间 (ms)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatDuration" min="50" max="500"
class="width-input">
</label>
</div>
</div>
</div>
<!-- 节拍模式配置 -->
<div class="config-section">
<h3>🎵 节拍模式配置</h3>
<div class="config-options">
<div class="config-item">
<label class="checkbox-label">
<span>每小节总拍数</span>
<input type="number" v-model.number="localDrumConfig.pattern.totalBeats" min="1" max="8"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍位置 (1-4)</span>
<input type="text" v-model="localDrumConfig.pattern.strongBeatsStr" placeholder="如: 1,4"
class="text-input" @input="updateStrongBeats">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍音量倍数</span>
<input type="number" v-model.number="localDrumConfig.pattern.accentMultiplier" min="1" max="3"
step="0.1" class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍频率偏移 (%)</span>
<input type="number" v-model.number="localDrumConfig.pattern.accentFrequencyOffset" min="-50" max="50"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍动画增强 (%)</span>
<input type="number" v-model.number="localDrumConfig.pattern.accentAnimation" min="0" max="100"
class="width-input">
</label>
</div>
</div>
</div>
</div>
</div>
<!-- 保存按钮 -->
<div class="save-section">
<button @click="saveData" class="save-btn">💾 保存所有数据</button>
@@ -954,14 +907,27 @@ 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 musicVolume = ref(0.5); // 音乐音量控制0.0-1.0默认50%
// 返回首页
const goToHome = () => {
router.push('/');
@@ -984,7 +950,7 @@ const tabs = [
{ key: 'config', label: '显示配置' },
{ key: 'champion', label: '冠军Logo配置' },
{ key: 'endTime', label: '结束时间设置' },
{ key: 'drum', label: '战鼓配置' }
{ key: 'music', label: '背景音乐设置' }
];
// 冠军Logo配置
@@ -994,6 +960,245 @@ 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 || '';
musicVolume.value = musicConfig.volume !== undefined ? musicConfig.volume : 0.5; // 默认50%音量
// 获取音乐文件列表
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,
volume: musicVolume.value
});
uploadMsg.value = musicEnabled.value ? '✅ 背景音乐已开启' : '⏸️ 背景音乐已关闭';
setTimeout(() => {
uploadMsg.value = '';
}, 2000);
} catch (error) {
console.error('保存音乐配置失败:', error);
uploadMsg.value = '❌ 操作失败,请重试';
// 回滚开关状态
musicEnabled.value = !musicEnabled.value;
}
};
// 6.1 处理音量变化
const handleMusicVolumeChange = async () => {
try {
await saveMusicConfig({
enabled: musicEnabled.value,
filePath: currentMusicPath.value,
volume: musicVolume.value
});
// 更新播放器音量
musicPlayer.setVolume(musicVolume.value);
uploadMsg.value = `✅ 音量已设置为 ${Math.round(musicVolume.value * 100)}%`;
setTimeout(() => {
uploadMsg.value = '';
}, 2000);
} catch (error) {
console.error('保存音乐配置失败:', error);
uploadMsg.value = '❌ 音量设置失败,请重试';
}
};
// 7. 测试播放音乐
const testPlayMsg = ref('');
const testMusicPlay = () => {
if (!currentMusicPath.value) {
testPlayMsg.value = '❌ 请先选择或上传音乐文件';
return;
}
try {
// 初始化音乐播放器(管理员页面强制启用播放)
musicPlayer.initMusicConfig(currentMusicPath.value, true, musicVolume.value);
musicPlayer.play();
testPlayMsg.value = `🎵 开始播放音乐(音量:${Math.round(musicVolume.value * 100)}%`;
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 +1209,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 +1339,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 +1358,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 +1470,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 +1484,12 @@ const saveData = async () => {
// 保存冠军Logo配置
currentConfig.displayConfig.championLogos = championLogos.value;
currentConfig.battleEndTime = localBattleEndTime.value;
currentConfig.drumConfig = configToSave;
// 保存音乐配置
currentConfig.music = {
enabled: musicEnabled.value,
filePath: currentMusicPath.value,
volume: parseFloat(musicVolume.value) // 确保音量是数字类型
};
// 一次性保存所有配置
const result = await writeConfig(currentConfig);
@@ -2204,4 +2347,143 @@ 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;
}
/* 音量控制滑块样式 */
.volume-slider {
width: 200px;
margin: 0 10px;
vertical-align: middle;
}
.volume-value {
display: inline-block;
width: 40px;
text-align: center;
font-weight: bold;
color: #667eea;
}
</style>

File diff suppressed because it is too large Load Diff