Compare commits
3 Commits
server
...
0fdf92e924
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fdf92e924 | ||
|
|
70ecd4fc67 | ||
|
|
76f0153569 |
@@ -864,11 +864,6 @@
|
||||
"role": "manager"
|
||||
}
|
||||
],
|
||||
"music": {
|
||||
"enabled": true,
|
||||
"filePath": "",
|
||||
"volume": "1"
|
||||
},
|
||||
"displayConfig": {
|
||||
"showBonusModule": true,
|
||||
"individual": {
|
||||
@@ -955,5 +950,36 @@
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
86
server.js
86
server.js
@@ -32,7 +32,6 @@ const storage = multer.diskStorage({
|
||||
}
|
||||
});
|
||||
|
||||
// 文件上传
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制
|
||||
@@ -48,23 +47,6 @@ 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());
|
||||
@@ -95,7 +77,6 @@ app.post('/api/config', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// API: 上传图片
|
||||
app.post('/api/upload', upload.single('image'), (req, res) => {
|
||||
try {
|
||||
@@ -134,73 +115,6 @@ 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'));
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<!-- 页脚 -->
|
||||
<footer class="game-footer">
|
||||
<div class="footer-content">
|
||||
<p>© 2025-2026 聚上集团 | 云上企 版权所有.</p>
|
||||
<p>© 2025-2026 聚上集团 | 云上企. 所有权利保留.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
saveDisplayConfig as saveDisplayConfigToConfig,
|
||||
getBattleEndTime,
|
||||
saveBattleEndTime as saveBattleEndTimeToConfig,
|
||||
getDrumConfig,
|
||||
saveDrumConfig as saveDrumConfigToConfig,
|
||||
getBonusRules,
|
||||
saveBonusRules as saveBonusRulesToConfig,
|
||||
getMusicConfig,
|
||||
saveMusicConfig as saveMusicConfigToConfig
|
||||
saveBonusRules as saveBonusRulesToConfig
|
||||
} from '../services/configService';
|
||||
|
||||
// 初始化空数据占位符,将在initializeData中正确加载
|
||||
@@ -29,8 +29,7 @@ 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 musicConfig = { enabled: false, filePath: '' };
|
||||
|
||||
export let drumConfig = {};
|
||||
|
||||
// 保存结束时间
|
||||
export const saveBattleEndTime = async (endTime) => {
|
||||
@@ -60,7 +59,30 @@ 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) => {
|
||||
@@ -96,8 +118,7 @@ export const refreshData = async () => {
|
||||
systemUsers = await getSystemUsers();
|
||||
displayConfig = await getDisplayConfig();
|
||||
battleEndTime = await getBattleEndTime();
|
||||
musicConfig = await getMusicConfig();
|
||||
|
||||
drumConfig = await getDrumConfig();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import BattleRanking from '../views/BattleRanking.vue'; // 首页组件
|
||||
import AdminPanel from '../views/AdminPanel.vue'; // 管理员面板组件
|
||||
import { musicPlayer } from '../utils/musicPlayer'; // 音乐播放器实例
|
||||
import { getMusicConfig } from '../services/configService'; // 音乐配置读取服务
|
||||
import BattleRanking from '../views/BattleRanking.vue';
|
||||
import AdminPanel from '../views/AdminPanel.vue';
|
||||
|
||||
// 路由配置
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
@@ -19,56 +16,23 @@ const routes = [
|
||||
meta: { title: '管理员面板' }
|
||||
},
|
||||
{
|
||||
// 404路由:未匹配路径重定向到首页
|
||||
// 捕获所有未匹配的路由,重定向到首页
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/'
|
||||
}
|
||||
];
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
// 路由守卫:页面切换时控制音乐状态
|
||||
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. 设置页面标题(可选增强)
|
||||
// 全局前置守卫,设置页面标题
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 设置文档标题
|
||||
if (to.meta.title) {
|
||||
document.title = to.meta.title;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
@@ -102,16 +102,25 @@ 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', // 默认战旗背景图片
|
||||
@@ -323,6 +332,26 @@ 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} 背景配置
|
||||
@@ -341,102 +370,4 @@ 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 };
|
||||
}
|
||||
};
|
||||
@@ -1,182 +0,0 @@
|
||||
// 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();
|
||||
@@ -62,124 +62,6 @@
|
||||
</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>
|
||||
@@ -773,9 +655,174 @@
|
||||
</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>
|
||||
@@ -907,27 +954,14 @@ import {
|
||||
saveDisplayConfig,
|
||||
battleEndTime,
|
||||
saveBattleEndTime,
|
||||
drumConfig,
|
||||
saveDrumConfig,
|
||||
refreshData,
|
||||
initializeData
|
||||
} from '../data/mockData.js';
|
||||
|
||||
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('/');
|
||||
@@ -950,7 +984,7 @@ const tabs = [
|
||||
{ key: 'config', label: '显示配置' },
|
||||
{ key: 'champion', label: '冠军Logo配置' },
|
||||
{ key: 'endTime', label: '结束时间设置' },
|
||||
{ key: 'music', label: '背景音乐设置' }
|
||||
{ key: 'drum', label: '战鼓配置' }
|
||||
];
|
||||
|
||||
// 冠军Logo配置
|
||||
@@ -960,245 +994,6 @@ 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 () => {
|
||||
@@ -1209,26 +1004,30 @@ 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 };
|
||||
}
|
||||
|
||||
// 新增:初始化音乐配置
|
||||
await initMusicConfig();
|
||||
// 重新处理强拍位置
|
||||
if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) {
|
||||
localDrumConfig.value.pattern.strongBeatsStr =
|
||||
localDrumConfig.value.pattern.strongBeats.join(',') || '1,4';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化数据失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理冠军Logo上传
|
||||
const handleChampionLogoUpload = async (event, type) => {
|
||||
const file = event.target.files[0];
|
||||
@@ -1339,6 +1138,13 @@ 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('数据刷新成功!');
|
||||
@@ -1358,7 +1164,57 @@ 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);
|
||||
@@ -1470,6 +1326,12 @@ 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');
|
||||
|
||||
@@ -1484,12 +1346,7 @@ const saveData = async () => {
|
||||
// 保存冠军Logo配置
|
||||
currentConfig.displayConfig.championLogos = championLogos.value;
|
||||
currentConfig.battleEndTime = localBattleEndTime.value;
|
||||
// 保存音乐配置
|
||||
currentConfig.music = {
|
||||
enabled: musicEnabled.value,
|
||||
filePath: currentMusicPath.value,
|
||||
volume: parseFloat(musicVolume.value) // 确保音量是数字类型
|
||||
};
|
||||
currentConfig.drumConfig = configToSave;
|
||||
|
||||
// 一次性保存所有配置
|
||||
const result = await writeConfig(currentConfig);
|
||||
@@ -2347,143 +2204,4 @@ 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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div @click="handleAnyInteraction" @scroll="handleAnyInteraction" @touchstart="handleAnyInteraction">
|
||||
<div>
|
||||
<!-- 第一部分:百日大战主题 - 使用banner0.png图片 -->
|
||||
<section class="theme-section card-game">
|
||||
<div class="theme-container">
|
||||
@@ -7,7 +7,20 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- 第二部分:战鼓动画(浮动并支持拖放) -->
|
||||
<section v-if="localDisplayConfig.showDrum" class="drums-section card-game" @mousedown="startDrag"
|
||||
@click="handleDrumClick" :style="{ left: drumsPosition.x + 'px', top: drumsPosition.y + 'px' }">
|
||||
<div class="drums-container">
|
||||
<!-- 战鼓动画在上面 -->
|
||||
<div class="drums-animation">
|
||||
<div class="drum glow-border" :class="{ beating: isBeating }">🥁</div>
|
||||
<div class="drum" :class="{ beating: isBeating }">🥁</div>
|
||||
<div class="trophy" style="font-size: 2.5rem; filter: drop-shadow(0 0 10px var(--gold-primary));">🏆</div>
|
||||
<div class="drum" :class="{ beating: isBeating }">🥁</div>
|
||||
<div class="drum" :class="{ beating: isBeating }">🥁</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 任务设置模块 -->
|
||||
<section class="task-settings-section card-game">
|
||||
@@ -36,7 +49,7 @@
|
||||
<div class="game-subtitle total-score-total-title">
|
||||
<img src="/completed_performance.png"
|
||||
alt="总战绩"
|
||||
style="width: 880px; display: block; margin: 0 auto; height: auto;"
|
||||
style="width: 600px; display: block; margin: 0 auto; height: auto;"
|
||||
class="total-score-total-image">
|
||||
</div>
|
||||
<div class="total-score-content">
|
||||
@@ -203,7 +216,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed, reactive, proxyRefs } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
@@ -212,11 +224,10 @@ import {
|
||||
bonusRules,
|
||||
displayConfig,
|
||||
battleEndTime,
|
||||
initializeData,
|
||||
musicConfig as importedMusicConfig
|
||||
drumConfig,
|
||||
initializeData
|
||||
} from '../data/mockData.js';
|
||||
import { readConfig } from '../services/configService.js';
|
||||
import { musicPlayer } from '../utils/musicPlayer';
|
||||
|
||||
// 创建默认显示配置的函数
|
||||
function createDefaultDisplayConfig() {
|
||||
@@ -399,37 +410,10 @@ const localDisplayConfig = ref(() => {
|
||||
return defaultDisplayConfig;
|
||||
});
|
||||
const taskSettings = ref({
|
||||
mainTitle: '2300万',
|
||||
mainTitle: '3000万',
|
||||
subtitle: '时间: 2025-11-12 - 2026-02-08'
|
||||
});
|
||||
|
||||
|
||||
const localMusicConfig = ref({
|
||||
enabled: true,
|
||||
filePath: '',
|
||||
volume: 1
|
||||
});
|
||||
|
||||
// 添加首次交互处理函数
|
||||
const handleFirstInteraction = () => {
|
||||
console.log("用户首次交互触发");
|
||||
if (localMusicConfig.value.enabled) {
|
||||
console.log("尝试播放音乐...");
|
||||
musicPlayer.play();
|
||||
} else {
|
||||
console.log("音乐未启用,跳过播放");
|
||||
}
|
||||
};
|
||||
|
||||
// 添加任意交互处理函数(点击、滚动等都会触发)
|
||||
const handleAnyInteraction = () => {
|
||||
// 只有在音乐启用且当前未播放时才尝试播放
|
||||
if (localMusicConfig.value.enabled && !musicPlayer.isPlaying) {
|
||||
console.log("检测到用户交互,尝试播放音乐...");
|
||||
musicPlayer.play();
|
||||
}
|
||||
};
|
||||
|
||||
// 加载任务设置和初始化所有数据
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
@@ -458,17 +442,6 @@ onBeforeMount(async () => {
|
||||
const configCopy = JSON.parse(JSON.stringify(config.displayConfig));
|
||||
localDisplayConfig.value = mergeConfig(defaultDisplayConfig, configCopy);
|
||||
}
|
||||
|
||||
if (config.music) {
|
||||
localMusicConfig.value = config.music;
|
||||
console.log("从服务器加载的音乐配置:", config.music);
|
||||
console.log("音乐文件路径:", config.music.filePath);
|
||||
console.log("音乐是否启用:", config.music.enabled);
|
||||
console.log("音乐音量:", config.music.volume);
|
||||
console.log("音量类型:", typeof config.music.volume);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
@@ -719,7 +692,17 @@ const hours = ref(0);
|
||||
const minutes = ref(0);
|
||||
const seconds = ref(0);
|
||||
|
||||
// 战鼓动画状态
|
||||
const isBeating = ref(false);
|
||||
let beatInterval = null;
|
||||
let countdownInterval = null;
|
||||
|
||||
// 音频上下文和战鼓音效
|
||||
let audioContext = null;
|
||||
const isPlayingSound = ref(false);
|
||||
|
||||
// 战鼓位置状态
|
||||
const drumsPosition = ref({ x: 20, y: 20 });
|
||||
// 倒计时位置状态已移除,直接在模板中使用固定位置
|
||||
// 奖金设置模块位置状态 - 使用reactive存储实际定位值
|
||||
const bonusPosition = reactive({ x: 'auto', y: 'auto' });
|
||||
@@ -743,7 +726,13 @@ function throttle(func, limit) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 开始拖动战鼓
|
||||
const startDrag = (e) => {
|
||||
isDragging = true;
|
||||
dragOffset.x = e.clientX - drumsPosition.value.x;
|
||||
dragOffset.y = e.clientY - drumsPosition.value.y;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// 开始拖动奖金模块(鼠标事件)
|
||||
const startBonusDrag = (e) => {
|
||||
@@ -892,11 +881,237 @@ const calculateCountdown = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化音频上下文
|
||||
const initAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
};
|
||||
|
||||
// 音频缓存,用于存储加载的MP3文件
|
||||
const audioBufferCache = ref({});
|
||||
|
||||
// 加载MP3文件到音频缓冲区
|
||||
const loadAudioFile = async (filePath) => {
|
||||
try {
|
||||
// 检查是否已缓存
|
||||
if (audioBufferCache.value[filePath]) {
|
||||
return audioBufferCache.value[filePath];
|
||||
}
|
||||
|
||||
// 加载音频文件
|
||||
const response = await fetch(filePath);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
|
||||
// 缓存音频缓冲区
|
||||
audioBufferCache.value[filePath] = audioBuffer;
|
||||
return audioBuffer;
|
||||
} catch (error) {
|
||||
console.error('加载音频文件失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 播放战鼓音效
|
||||
const playDrumSound = async (isStrongBeat = false) => {
|
||||
// 检查是否启用声音播放
|
||||
if (!audioContext || isPlayingSound.value || drumConfig?.sound?.enabled === false) return;
|
||||
|
||||
isPlayingSound.value = true;
|
||||
|
||||
try {
|
||||
// 使用配置的音效参数
|
||||
const soundConfig = drumConfig?.sound || {};
|
||||
const patternConfig = drumConfig?.pattern || {};
|
||||
|
||||
// 检查是否配置了MP3文件路径
|
||||
if (soundConfig.soundSrc && soundConfig.soundSrc.trim() !== '') {
|
||||
// 使用MP3文件播放
|
||||
const audioBuffer = await loadAudioFile(soundConfig.soundSrc);
|
||||
|
||||
if (audioBuffer) {
|
||||
// 创建音频源节点
|
||||
const source = audioContext.createBufferSource();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
// 连接节点
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
// 设置音量,支持强拍音量增强
|
||||
const baseVolume = soundConfig.volume || 1.0;
|
||||
const accentMultiplier = patternConfig.accentMultiplier || 1.2;
|
||||
const volume = isStrongBeat ? baseVolume * accentMultiplier : baseVolume;
|
||||
gainNode.gain.value = volume;
|
||||
|
||||
// 播放声音
|
||||
source.start(0);
|
||||
|
||||
// 设置完成后重置播放状态
|
||||
setTimeout(() => {
|
||||
isPlayingSound.value = false;
|
||||
}, audioBuffer.duration * 1000 + 100);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有配置MP3文件或加载失败,回退到合成音效
|
||||
// 创建振荡器节点
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
// 连接节点
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
// 设置战鼓音效参数,支持强拍
|
||||
const baseVolume = soundConfig.volume || 1.0;
|
||||
// 使用pattern配置中的强拍音量倍数
|
||||
const accentMultiplier = patternConfig.accentMultiplier || 1.2;
|
||||
const volume = isStrongBeat ? baseVolume * accentMultiplier : baseVolume;
|
||||
|
||||
// 使用soundConfig中的type1
|
||||
oscillator.type = soundConfig.type1 || 'sine';
|
||||
|
||||
// 基础频率,支持强拍频率偏移
|
||||
const baseFrequency = soundConfig.frequency1 || 150;
|
||||
const frequencyOffset = isStrongBeat ? (patternConfig.accentFrequencyOffset || 0) / 100 : 0;
|
||||
const actualFrequency = baseFrequency * (1 + frequencyOffset);
|
||||
|
||||
oscillator.frequency.setValueAtTime(actualFrequency, audioContext.currentTime);
|
||||
// 频率渐变
|
||||
oscillator.frequency.exponentialRampToValueAtTime(actualFrequency * 0.5, audioContext.currentTime + 0.1);
|
||||
|
||||
// 设置音量包络
|
||||
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
||||
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + (soundConfig.attackTime || 0.01));
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + (soundConfig.decayTime || 0.3));
|
||||
|
||||
// 播放声音
|
||||
oscillator.start();
|
||||
oscillator.stop(audioContext.currentTime + (soundConfig.decayTime || 0.3));
|
||||
|
||||
// 双音调效果 - 始终使用
|
||||
const oscillator2 = audioContext.createOscillator();
|
||||
const gainNode2 = audioContext.createGain();
|
||||
|
||||
oscillator2.connect(gainNode2);
|
||||
gainNode2.connect(audioContext.destination);
|
||||
|
||||
oscillator2.type = soundConfig.type2 || 'triangle';
|
||||
oscillator2.frequency.setValueAtTime(soundConfig.frequency2 || 100, audioContext.currentTime);
|
||||
|
||||
gainNode2.gain.setValueAtTime(0, audioContext.currentTime);
|
||||
gainNode2.gain.linearRampToValueAtTime(volume * 0.8, audioContext.currentTime + (soundConfig.attackTime || 0.01));
|
||||
gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + (soundConfig.decayTime || 0.3) + 0.2);
|
||||
|
||||
oscillator2.start();
|
||||
oscillator2.stop(audioContext.currentTime + (soundConfig.decayTime || 0.3) + 0.2);
|
||||
|
||||
// 设置完成后重置播放状态
|
||||
setTimeout(() => {
|
||||
isPlayingSound.value = false;
|
||||
}, (soundConfig.decayTime || 0.3) * 1000 + 150);
|
||||
} catch (error) {
|
||||
console.error('播放战鼓音效出错:', error);
|
||||
isPlayingSound.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 修改相关函数以支持异步
|
||||
const handleDrumClick = async () => {
|
||||
// 如果音频上下文未初始化,初始化它
|
||||
if (!audioContext) {
|
||||
initAudioContext();
|
||||
}
|
||||
|
||||
// 如果音频上下文被暂停,恢复它
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
|
||||
// 触发战鼓动画和音效,使用配置的点击效果
|
||||
const animationConfig = drumConfig?.animation || {};
|
||||
isBeating.value = true;
|
||||
await playDrumSound(true); // 点击总是强拍
|
||||
|
||||
setTimeout(() => {
|
||||
isBeating.value = false;
|
||||
}, animationConfig.clickBeatDuration || 250);
|
||||
};
|
||||
|
||||
// 战鼓动画效果
|
||||
const startDrumAnimation = () => {
|
||||
// 检查是否显示战鼓
|
||||
if (drumConfig?.showDrum === false) return;
|
||||
|
||||
// 使用配置的动画和节拍参数
|
||||
const animationConfig = drumConfig?.animation || {};
|
||||
const patternConfig = drumConfig?.pattern || {};
|
||||
|
||||
// 检查是否启用动画
|
||||
if (animationConfig.enabled === false) return;
|
||||
|
||||
let beatCount = 0;
|
||||
// 使用配置的节拍间隔
|
||||
const interval = animationConfig.beatInterval || 200;
|
||||
|
||||
beatInterval = setInterval(() => {
|
||||
beatCount++;
|
||||
// 使用配置的节拍模式和总拍数
|
||||
const totalBeats = patternConfig.totalBeats || 4;
|
||||
const currentBeat = ((beatCount - 1) % totalBeats) + 1;
|
||||
|
||||
// 根据节拍模式确定是否是强拍
|
||||
const strongBeats = patternConfig.strongBeats || [1, 4];
|
||||
const isStrongBeat = strongBeats.includes(currentBeat);
|
||||
|
||||
// 执行动画和音效
|
||||
isBeating.value = true;
|
||||
|
||||
// 根据是否是强拍播放音效
|
||||
playDrumSound(isStrongBeat);
|
||||
|
||||
// 设置CSS变量,支持强拍动画增强
|
||||
const drums = document.querySelectorAll('.drum');
|
||||
drums.forEach(drum => {
|
||||
// 使用配置的动画参数
|
||||
drum.style.setProperty('--drum-scale', isStrongBeat ?
|
||||
(animationConfig.beatScale || 1.3) * (1 + (patternConfig.accentAnimation || 0) / 100) :
|
||||
(animationConfig.beatScale || 1.3));
|
||||
drum.style.setProperty('--drum-translate-y', isStrongBeat ?
|
||||
`${(animationConfig.beatTranslateY || -15) * (1 + (patternConfig.accentAnimation || 0) / 100)}px` :
|
||||
`${animationConfig.beatTranslateY || -15}px`);
|
||||
drum.style.setProperty('--drum-rotate', `${animationConfig.beatRotate || 5}deg`);
|
||||
drum.style.setProperty('--drum-brightness', isStrongBeat ? '1.4' : '1.3');
|
||||
drum.style.setProperty('--drum-saturation', isStrongBeat ? '1.3' : '1.2');
|
||||
});
|
||||
|
||||
// 根据节拍类型设置持续时间
|
||||
const beatDuration = isStrongBeat
|
||||
? (animationConfig.beatDuration || 150)
|
||||
: (animationConfig.beatDuration || 100);
|
||||
|
||||
setTimeout(() => {
|
||||
isBeating.value = false;
|
||||
}, beatDuration);
|
||||
}, interval);
|
||||
};
|
||||
|
||||
// 已移至文件中异步版本的handleDrumClick函数
|
||||
|
||||
// 跳转到管理员页面
|
||||
const goToAdmin = () => {
|
||||
router.push('/admin');
|
||||
};
|
||||
|
||||
// 监听窗口点击事件,用于用户交互后初始化音频上下文
|
||||
document.addEventListener('click', initAudioContext, { once: true });
|
||||
document.addEventListener('touchstart', initAudioContext, { once: true });
|
||||
|
||||
const handleResize = () => {
|
||||
// 计算并设置排名明细区域的最小高度,使其底部与视口对齐
|
||||
const rankingsSection = document.querySelector('.rankings-section');
|
||||
@@ -914,43 +1129,7 @@ onMounted(async () => {
|
||||
try {
|
||||
// 异步初始化数据
|
||||
await initializeData();
|
||||
|
||||
localMusicConfig.value = await readConfig().music;
|
||||
|
||||
if (localMusicConfig.value.enabled) {
|
||||
// 获取音量设置,如果没有则使用默认值0.5
|
||||
const volume = localMusicConfig.value.volume !== undefined ? localMusicConfig.value.volume : 0.5;
|
||||
console.log("音乐配置信息:", localMusicConfig.value);
|
||||
console.log("音乐文件路径:", localMusicConfig.value.filePath);
|
||||
console.log("音乐是否启用:", localMusicConfig.value.enabled);
|
||||
console.log("音乐音量:", volume);
|
||||
console.log("音量类型:", typeof volume);
|
||||
|
||||
// 检查音乐文件是否存在
|
||||
if (localMusicConfig.value.filePath) {
|
||||
console.log("正在检查音乐文件是否存在...");
|
||||
fetch(localMusicConfig.value.filePath)
|
||||
.then(response => {
|
||||
console.log("音乐文件状态:", response.status, response.statusText);
|
||||
if (response.ok) {
|
||||
console.log("音乐文件存在,可以正常访问");
|
||||
} else {
|
||||
console.error("音乐文件无法访问,状态码:", response.status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("检查音乐文件时出错:", error);
|
||||
});
|
||||
}
|
||||
|
||||
musicPlayer.initMusicConfig(localMusicConfig.value.filePath, localMusicConfig.value.enabled, volume);
|
||||
// 注意:由于浏览器自动播放策略限制,这里不直接调用play()
|
||||
// 而是等待用户的第一次交互(点击)后再播放
|
||||
console.log("音乐已准备就绪,等待用户首次交互后播放...");
|
||||
} else {
|
||||
musicPlayer.pause();
|
||||
console.log("音乐未启用,已暂停");
|
||||
}
|
||||
|
||||
// 更新本地显示配置,确保columnAlignments属性存在
|
||||
if (displayConfig) {
|
||||
const configCopy = JSON.parse(JSON.stringify(displayConfig));
|
||||
@@ -977,6 +1156,7 @@ onMounted(async () => {
|
||||
|
||||
calculateCountdown();
|
||||
countdownInterval = setInterval(calculateCountdown, 10); // 改为10ms更新一次以显示毫秒
|
||||
startDrumAnimation();
|
||||
// 监听窗口大小变化,确保排名明细与底部对齐
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize(); // 初始调整
|
||||
@@ -1006,11 +1186,8 @@ const handleDisplayConfigChange = () => {
|
||||
// 在实际项目中,可能需要通过WebSocket或轮询来更新配置
|
||||
|
||||
onUnmounted(() => {
|
||||
// 强制暂停音乐
|
||||
musicPlayer.pause();
|
||||
musicPlayer.setMuted(true);
|
||||
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
if (beatInterval) clearInterval(beatInterval);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
|
||||
// 移除拖放相关的事件监听
|
||||
@@ -1021,6 +1198,12 @@ onUnmounted(() => {
|
||||
document.removeEventListener('touchmove', touchMove);
|
||||
document.removeEventListener('touchend', endTouch);
|
||||
document.removeEventListener('touchcancel', endTouch);
|
||||
|
||||
// 清理音频资源
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
audioContext = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1226,7 +1409,7 @@ onUnmounted(() => {
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.total-score-total-image {
|
||||
width: 340px !important; /* 调整总战绩图片大小,覆盖内联样式 */
|
||||
width: 280px !important; /* 调整总战绩图片大小,覆盖内联样式 */
|
||||
}
|
||||
|
||||
/* 移动端总战绩金额字体调小 */
|
||||
@@ -1542,7 +1725,97 @@ 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 {
|
||||
@@ -1649,29 +1922,7 @@ onUnmounted(() => {
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
margin-top: 0.8rem;
|
||||
max-height: 420px;
|
||||
// 只对左侧战区的.rank-table生效(通过父容器限定)
|
||||
.team-rankings-container & {
|
||||
-ms-overflow-style: none; /* IE/Edge 隐藏 */
|
||||
scrollbar-width: none; /* Firefox 隐藏 */
|
||||
}
|
||||
// Chrome/Safari 隐藏左侧滚动条(伪元素必须单独写)
|
||||
.team-rankings-container &::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
// ========== 新增:右侧英雄排名滚动条保留原样 ==========
|
||||
.individual-rankings-container & {
|
||||
-ms-overflow-style: auto; /* 恢复IE/Edge默认 */
|
||||
scrollbar-width: auto; /* 恢复Firefox默认 */
|
||||
}
|
||||
// Chrome/Safari 恢复右侧滚动条
|
||||
.individual-rankings-container &::-webkit-scrollbar {
|
||||
display: block !important;
|
||||
width: 6px !important; // 恢复默认滚动条宽度
|
||||
}
|
||||
|
||||
max-height: 427px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -1828,7 +2079,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: calc(var(--individual-champion-font-size, 6rem) * 2);
|
||||
font-size: calc(var(--individual-champion-font-size, 5rem) * 2);
|
||||
font-weight: bold;
|
||||
color: #fff2c4;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, .8), 0 0 20px rgb(16 16 16 / 30%), 1px 1px 2px rgba(0, 0, 0, .8);
|
||||
@@ -1850,7 +2101,7 @@ onUnmounted(() => {
|
||||
/* 照片容器样式 */
|
||||
.team-logo.photo-container {
|
||||
overflow: hidden;
|
||||
margin-top: -97px;
|
||||
margin-top: -96px;
|
||||
}
|
||||
|
||||
.individual-champion {
|
||||
@@ -2047,7 +2298,11 @@ onUnmounted(() => {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
/* 战鼓部分调整 */
|
||||
.drums-section {
|
||||
transform: scale(0.8);
|
||||
/* 缩小战鼓元素 */
|
||||
}
|
||||
|
||||
/* 2. 倒计时模块调整 - 移至冠军战区上方,缩小时间显示为一行 */
|
||||
.timer-float {
|
||||
@@ -2352,12 +2607,4 @@ onUnmounted(() => {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
.team-rankings-container .rank-table {
|
||||
-ms-overflow-style: none !important;
|
||||
scrollbar-width: none !important;
|
||||
}
|
||||
.team-rankings-container .rank-table::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user