chore: 纯网页版本
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -99,6 +99,56 @@ export const individualRankings = [
|
||||
department: '人力资源部',
|
||||
completedTasks: 28,
|
||||
bonus: 500
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: '李十三',
|
||||
score: 782,
|
||||
level: 'B',
|
||||
avatar: '⚡',
|
||||
department: '销售部',
|
||||
completedTasks: 26,
|
||||
bonus: 500
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: '张十四',
|
||||
score: 765,
|
||||
level: 'B',
|
||||
avatar: '⚡',
|
||||
department: '技术部',
|
||||
completedTasks: 24,
|
||||
bonus: 500
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: '王十五',
|
||||
score: 748,
|
||||
level: 'B',
|
||||
avatar: '⚡',
|
||||
department: '市场部',
|
||||
completedTasks: 22,
|
||||
bonus: 500
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: '赵十六',
|
||||
score: 732,
|
||||
level: 'C',
|
||||
avatar: '🎯',
|
||||
department: '财务部',
|
||||
completedTasks: 20,
|
||||
bonus: 300
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: '钱十七',
|
||||
score: 715,
|
||||
level: 'C',
|
||||
avatar: '🎯',
|
||||
department: '人力资源部',
|
||||
completedTasks: 18,
|
||||
bonus: 300
|
||||
}
|
||||
];
|
||||
|
||||
@@ -153,6 +203,106 @@ export const teamRankings = [
|
||||
leader: '钱七',
|
||||
completedTasks: 165,
|
||||
bonus: 6000
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '无敌战队',
|
||||
totalScore: 3980,
|
||||
memberCount: 5,
|
||||
level: 'A',
|
||||
leader: '孙八',
|
||||
completedTasks: 155,
|
||||
bonus: 4000
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '冲锋陷阵',
|
||||
totalScore: 3850,
|
||||
memberCount: 5,
|
||||
level: 'A',
|
||||
leader: '周九',
|
||||
completedTasks: 148,
|
||||
bonus: 4000
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '锐不可当',
|
||||
totalScore: 3720,
|
||||
memberCount: 5,
|
||||
level: 'A',
|
||||
leader: '吴十',
|
||||
completedTasks: 142,
|
||||
bonus: 4000
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: '同心协力',
|
||||
totalScore: 3600,
|
||||
memberCount: 5,
|
||||
level: 'B',
|
||||
leader: '郑十一',
|
||||
completedTasks: 135,
|
||||
bonus: 2000
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '众志成城',
|
||||
totalScore: 3480,
|
||||
memberCount: 5,
|
||||
level: 'B',
|
||||
leader: '王十二',
|
||||
completedTasks: 128,
|
||||
bonus: 2000
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: '气势如虹',
|
||||
totalScore: 3350,
|
||||
memberCount: 5,
|
||||
level: 'B',
|
||||
leader: '李十三',
|
||||
completedTasks: 122,
|
||||
bonus: 2000
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: '披荆斩棘',
|
||||
totalScore: 3220,
|
||||
memberCount: 5,
|
||||
level: 'B',
|
||||
leader: '张十四',
|
||||
completedTasks: 115,
|
||||
bonus: 2000
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: '勇攀高峰',
|
||||
totalScore: 3100,
|
||||
memberCount: 5,
|
||||
level: 'C',
|
||||
leader: '王十五',
|
||||
completedTasks: 108,
|
||||
bonus: 1000
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: '力争上游',
|
||||
totalScore: 2980,
|
||||
memberCount: 5,
|
||||
level: 'C',
|
||||
leader: '赵十六',
|
||||
completedTasks: 102,
|
||||
bonus: 1000
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: '蓄势待发',
|
||||
totalScore: 2850,
|
||||
memberCount: 5,
|
||||
level: 'C',
|
||||
leader: '钱十七',
|
||||
completedTasks: 95,
|
||||
bonus: 1000
|
||||
}
|
||||
];
|
||||
|
||||
@@ -192,6 +342,85 @@ export const systemUsers = [
|
||||
}
|
||||
];
|
||||
|
||||
// 显示配置
|
||||
export const displayConfig = {
|
||||
// 个人排名显示配置
|
||||
individual: {
|
||||
showLevel: false, // 显示等级列
|
||||
showDepartment: false, // 显示部门列
|
||||
scoreColumn: {
|
||||
displayName: '签单金额', // 列显示名称
|
||||
displayStyle: 'amount' // 显示样式: 'amount'(金额) 或 'number'(普通数字)
|
||||
},
|
||||
columnWidths: {
|
||||
rank: 60, // 排名列宽度
|
||||
avatar: 60, // 头像列宽度
|
||||
name: 1, // 姓名列宽度(1表示自动填充)
|
||||
score: 80, // 分数列宽度
|
||||
level: 80, // 等级列宽度
|
||||
department: 1, // 部门列宽度(1表示自动填充)
|
||||
bonus: 80 // 奖金列宽度
|
||||
}
|
||||
},
|
||||
// 战队排名显示配置
|
||||
team: {
|
||||
showMemberCount: false, // 显示人数列
|
||||
showLeader: false, // 显示队长列
|
||||
totalScoreColumn: {
|
||||
displayName: '签单金额', // 列显示名称
|
||||
displayStyle: 'amount' // 显示样式: 'amount'(金额) 或 'number'(普通数字)
|
||||
},
|
||||
columnWidths: {
|
||||
rank: 60, // 排名列宽度
|
||||
name: 1, // 战队名列宽度(1表示自动填充)
|
||||
score: 80, // 分数列宽度
|
||||
memberCount: 60, // 人数列宽度
|
||||
leader: 1, // 队长列宽度(1表示自动填充)
|
||||
bonus: 80 // 奖金列宽度
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 结束时间配置(精确到秒)
|
||||
export let battleEndTime = {
|
||||
date: '2026-02-01',
|
||||
time: '23:59:59'
|
||||
};
|
||||
|
||||
// 战鼓参数配置
|
||||
export let drumConfig = {
|
||||
// 音效参数
|
||||
sound: {
|
||||
volume: 1.0, // 音量 0-1
|
||||
frequency1: 150, // 第一个音调频率
|
||||
frequency2: 100, // 第二个音调频率
|
||||
attackTime: 0.01, // 起音时间
|
||||
decayTime: 0.3, // 衰减时间
|
||||
type1: 'sine', // 第一个振荡器类型
|
||||
type2: 'triangle' // 第二个振荡器类型
|
||||
},
|
||||
// 动画参数
|
||||
animation: {
|
||||
beatInterval: 200, // 节拍间隔(毫秒)
|
||||
beatScale: 1.3, // 跳动缩放比例
|
||||
beatTranslateY: -15, // 跳动上下位移
|
||||
beatRotate: 5, // 跳动旋转角度
|
||||
idlePulseDuration: 2, // 闲置脉动持续时间
|
||||
beatDuration: 100 // 单次跳动持续时间
|
||||
},
|
||||
// 节拍模式
|
||||
pattern: {
|
||||
strongBeats: [1, 4], // 强拍位置(1-4拍)
|
||||
totalBeats: 4 // 每小节总拍数
|
||||
}
|
||||
};
|
||||
|
||||
// 保存结束时间
|
||||
export const saveBattleEndTime = (endTime) => {
|
||||
battleEndTime = endTime;
|
||||
console.log('保存结束时间:', battleEndTime);
|
||||
};
|
||||
|
||||
// 保存数据的方法(模拟本地存储)
|
||||
export const saveIndividualRankings = (data) => {
|
||||
// 这里只是模拟,实际项目中可以考虑使用localStorage或后端API
|
||||
@@ -203,4 +432,19 @@ export const saveTeamRankings = (data) => {
|
||||
// 这里只是模拟,实际项目中可以考虑使用localStorage或后端API
|
||||
console.log('保存战队排名数据:', data);
|
||||
// 在真实环境中,可以调用API保存数据
|
||||
};
|
||||
|
||||
// 保存显示配置
|
||||
export const saveDisplayConfig = (config) => {
|
||||
// 这里只是模拟,实际项目中可以考虑使用localStorage或后端API
|
||||
console.log('保存显示配置:', config);
|
||||
// 在真实环境中,可以调用API保存数据
|
||||
};
|
||||
|
||||
// 保存战鼓配置
|
||||
export const saveDrumConfig = (config) => {
|
||||
// 这里只是模拟,实际项目中可以考虑使用localStorage或后端API
|
||||
console.log('保存战鼓配置:', config);
|
||||
drumConfig = { ...drumConfig, ...config };
|
||||
// 在真实环境中,可以调用API保存数据
|
||||
};
|
||||
@@ -25,12 +25,14 @@
|
||||
/>
|
||||
</div>
|
||||
<button @click="login" class="login-btn">登录</button>
|
||||
<button @click="goToHome" class="home-btn">返回首页</button>
|
||||
<div v-if="loginError" class="error-message">{{ loginError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理界面 --><div v-else class="management-container">
|
||||
<!-- 顶部导航 --><div class="top-nav">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="top-nav">
|
||||
<h1 class="nav-title">📊 百人大战管理系统</h1>
|
||||
<div class="nav-actions">
|
||||
<span class="welcome-text">欢迎您,{{ currentUser.username }}!</span>
|
||||
@@ -38,7 +40,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能选项卡 --><div class="tabs">
|
||||
<!-- 功能选项卡 -->
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
@@ -48,6 +51,204 @@
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 --><div class="tab-content">
|
||||
<!-- 结束时间设置 --><div v-if="currentTab === 'endTime'" class="end-time-content">
|
||||
<h2 class="config-title">⏰ 百人大战结束时间设置</h2>
|
||||
<div class="time-setting-section">
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<span>结束日期:</span>
|
||||
<input
|
||||
type="date"
|
||||
v-model="localBattleEndTime.date"
|
||||
class="text-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<span>结束时间:</span>
|
||||
<input
|
||||
type="time"
|
||||
step="1"
|
||||
v-model="localBattleEndTime.time"
|
||||
class="text-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="info-text">提示:设置的结束时间将用于动态计算并显示"距离结束还有多少时间"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示配置管理 --><div v-if="currentTab === 'config'" class="config-content">
|
||||
<h2 class="config-title">⚙️ 显示配置管理</h2>
|
||||
|
||||
<!-- 个人排名显示配置 -->
|
||||
<div class="config-section">
|
||||
<h3>👤 个人排名显示选项</h3>
|
||||
<div class="config-options">
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="localDisplayConfig.individual.showLevel"
|
||||
>
|
||||
<span>显示等级列</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="localDisplayConfig.individual.showDepartment"
|
||||
>
|
||||
<span>显示部门列</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<span>签单金额列显示名称:</span>
|
||||
<input type="text" v-model="localDisplayConfig.individual.scoreColumn.displayName"
|
||||
placeholder="输入显示名称" class="text-input">
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<span>签单金额显示样式:</span>
|
||||
<select v-model="localDisplayConfig.individual.scoreColumn.displayStyle" class="select-input">
|
||||
<option value="amount">金额(带¥符号)</option>
|
||||
<option value="number">普通数字</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="column-widths-section">
|
||||
<h4>列宽设置(单位:像素)</h4>
|
||||
<div class="column-width-item">
|
||||
<label>排名列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.individual.columnWidths.rank" min="40" max="200" class="width-input">
|
||||
</div>
|
||||
<div class="column-width-item">
|
||||
<label>头像列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.individual.columnWidths.avatar" min="40" max="200" class="width-input">
|
||||
</div>
|
||||
<div class="column-width-item">
|
||||
<label>姓名列:</label>
|
||||
<select v-model.number="localDisplayConfig.individual.columnWidths.name" class="width-select">
|
||||
<option :value="1">自动填充</option>
|
||||
<option value="100">100px</option>
|
||||
<option value="150">150px</option>
|
||||
<option value="200">200px</option>
|
||||
<option value="250">250px</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="column-width-item">
|
||||
<label>签单金额列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.individual.columnWidths.score" min="60" max="200" class="width-input">
|
||||
</div>
|
||||
<div class="column-width-item" v-if="localDisplayConfig.individual.showLevel">
|
||||
<label>等级列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.individual.columnWidths.level" min="60" max="200" class="width-input">
|
||||
</div>
|
||||
<div class="column-width-item" v-if="localDisplayConfig.individual.showDepartment">
|
||||
<label>部门列:</label>
|
||||
<select v-model.number="localDisplayConfig.individual.columnWidths.department" class="width-select">
|
||||
<option :value="1">自动填充</option>
|
||||
<option value="100">100px</option>
|
||||
<option value="150">150px</option>
|
||||
<option value="200">200px</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="column-width-item">
|
||||
<label>奖金列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.individual.columnWidths.bonus" min="60" max="200" class="width-input">
|
||||
</div>
|
||||
</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="localDisplayConfig.team.showMemberCount"
|
||||
>
|
||||
<span>显示人数列</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="localDisplayConfig.team.showLeader"
|
||||
>
|
||||
<span>显示队长列</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<span>签单金额列显示名称:</span>
|
||||
<input type="text" v-model="localDisplayConfig.team.totalScoreColumn.displayName"
|
||||
placeholder="输入显示名称" class="text-input">
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label class="checkbox-label">
|
||||
<span>签单金额显示样式:</span>
|
||||
<select v-model="localDisplayConfig.team.totalScoreColumn.displayStyle" class="select-input">
|
||||
<option value="amount">金额(带¥符号)</option>
|
||||
<option value="number">普通数字</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="column-widths-section">
|
||||
<h4>列宽设置(单位:像素)</h4>
|
||||
<div class="column-width-item">
|
||||
<label>排名列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.team.columnWidths.rank" min="40" max="200" class="width-input">
|
||||
</div>
|
||||
<div class="column-width-item">
|
||||
<label>战队名列:</label>
|
||||
<select v-model.number="localDisplayConfig.team.columnWidths.name" class="width-select">
|
||||
<option :value="1">自动填充</option>
|
||||
<option value="150">150px</option>
|
||||
<option value="200">200px</option>
|
||||
<option value="250">250px</option>
|
||||
<option value="300">300px</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="column-width-item">
|
||||
<label>签单金额列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.team.columnWidths.score" min="60" max="200" class="width-input">
|
||||
</div>
|
||||
<div class="column-width-item" v-if="localDisplayConfig.team.showMemberCount">
|
||||
<label>人数列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.team.columnWidths.memberCount" min="60" max="100" class="width-input">
|
||||
</div>
|
||||
<div class="column-width-item" v-if="localDisplayConfig.team.showLeader">
|
||||
<label>队长列:</label>
|
||||
<select v-model.number="localDisplayConfig.team.columnWidths.leader" class="width-select">
|
||||
<option :value="1">自动填充</option>
|
||||
<option value="100">100px</option>
|
||||
<option value="150">150px</option>
|
||||
<option value="200">200px</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="column-width-item">
|
||||
<label>奖金列:</label>
|
||||
<input type="number" v-model.number="localDisplayConfig.team.columnWidths.bonus" min="60" max="200" class="width-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人排名管理 --><div v-if="currentTab === 'individual'" class="rank-content">
|
||||
<div class="management-header">
|
||||
<h2>👤 个人排名管理</h2>
|
||||
@@ -57,9 +258,9 @@
|
||||
<div class="table-header">
|
||||
<span class="rank-col">排名</span>
|
||||
<span class="name-col">姓名</span>
|
||||
<span class="score-col">得分</span>
|
||||
<span class="level-col">等级</span>
|
||||
<span class="dept-col">部门</span>
|
||||
<span class="score-col">{{ displayConfig.individual.scoreColumn.displayName }}</span>
|
||||
<span v-if="displayConfig.individual.showLevel" class="level-col">等级</span>
|
||||
<span v-if="displayConfig.individual.showDepartment" class="dept-col">部门</span>
|
||||
<span class="bonus-col">奖金</span>
|
||||
<span class="action-col">操作</span>
|
||||
</div>
|
||||
@@ -71,9 +272,9 @@
|
||||
>
|
||||
<span class="rank-col">{{ index + 1 }}</span>
|
||||
<span class="name-col">{{ item.name }}</span>
|
||||
<span class="score-col">{{ item.score }}</span>
|
||||
<span class="level-col" :class="`level-${item.level}`">{{ item.level }}</span>
|
||||
<span class="dept-col">{{ item.department }}</span>
|
||||
<span class="score-col">{{ displayConfig.individual.scoreColumn.displayStyle === 'amount' ? '¥' + item.score : item.score }}</span>
|
||||
<span v-if="displayConfig.individual.showLevel" class="level-col" :class="`level-${item.level}`">{{ item.level }}</span>
|
||||
<span v-if="displayConfig.individual.showDepartment" class="dept-col">{{ item.department }}</span>
|
||||
<span class="bonus-col">¥{{ item.bonus }}</span>
|
||||
<span class="action-col">
|
||||
<button @click="editIndividual(item)" class="edit-btn">✏️ 编辑</button>
|
||||
@@ -92,9 +293,9 @@
|
||||
<div class="table-header">
|
||||
<span class="rank-col">排名</span>
|
||||
<span class="name-col">战队名称</span>
|
||||
<span class="score-col">总分</span>
|
||||
<span class="member-col">人数</span>
|
||||
<span class="leader-col">队长</span>
|
||||
<span class="score-col">{{ displayConfig.team.totalScoreColumn.displayName }}</span>
|
||||
<span v-if="displayConfig.team.showMemberCount" class="member-col">人数</span>
|
||||
<span v-if="displayConfig.team.showLeader" class="leader-col">队长</span>
|
||||
<span class="bonus-col">奖金</span>
|
||||
<span class="action-col">操作</span>
|
||||
</div>
|
||||
@@ -106,9 +307,9 @@
|
||||
>
|
||||
<span class="rank-col">{{ index + 1 }}</span>
|
||||
<span class="name-col">{{ item.name }}</span>
|
||||
<span class="score-col">{{ item.totalScore }}</span>
|
||||
<span class="member-col">{{ item.memberCount }}人</span>
|
||||
<span class="leader-col">{{ item.leader }}</span>
|
||||
<span class="score-col">{{ displayConfig.team.totalScoreColumn.displayStyle === 'amount' ? '¥' + item.totalScore : item.totalScore }}</span>
|
||||
<span v-if="displayConfig.team.showMemberCount" class="member-col">{{ item.memberCount }}人</span>
|
||||
<span v-if="displayConfig.team.showLeader" class="leader-col">{{ item.leader }}</span>
|
||||
<span class="bonus-col">¥{{ item.bonus }}</span>
|
||||
<span class="action-col">
|
||||
<button @click="editTeam(item)" class="edit-btn">✏️ 编辑</button>
|
||||
@@ -117,6 +318,259 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 战鼓配置 -->
|
||||
<div v-if="currentTab === 'drum'" class="drum-config-content">
|
||||
<h2 class="config-title">🥁 战鼓配置管理</h2>
|
||||
|
||||
<!-- 音效配置 -->
|
||||
<div class="config-section">
|
||||
<h3>🔊 音效配置</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">
|
||||
@@ -134,7 +588,7 @@
|
||||
<input v-model="individualForm.name" type="text" required class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>得分:</label>
|
||||
<label>签单金额:</label>
|
||||
<input v-model.number="individualForm.score" type="number" required class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -172,7 +626,7 @@
|
||||
<input v-model="teamForm.name" type="text" required class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>总分:</label>
|
||||
<label>签单金额:</label>
|
||||
<input v-model.number="teamForm.totalScore" type="number" required class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -205,11 +659,22 @@ import {
|
||||
teamRankings,
|
||||
systemUsers,
|
||||
saveIndividualRankings,
|
||||
saveTeamRankings
|
||||
saveTeamRankings,
|
||||
displayConfig,
|
||||
saveDisplayConfig,
|
||||
battleEndTime,
|
||||
saveBattleEndTime,
|
||||
drumConfig,
|
||||
saveDrumConfig
|
||||
} from '../data/mockData.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 返回首页
|
||||
const goToHome = () => {
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
// 登录相关
|
||||
const isLoggedIn = ref(false);
|
||||
const loginForm = reactive({
|
||||
@@ -222,13 +687,41 @@ const currentUser = ref({});
|
||||
// 标签页相关
|
||||
const tabs = [
|
||||
{ key: 'individual', label: '个人排名' },
|
||||
{ key: 'team', label: '战队排名' }
|
||||
{ key: 'team', label: '战队排名' },
|
||||
{ key: 'config', label: '显示配置' },
|
||||
{ key: 'endTime', label: '结束时间设置' },
|
||||
{ key: 'drum', label: '战鼓配置' }
|
||||
];
|
||||
const currentTab = ref('individual');
|
||||
|
||||
// 本地数据副本
|
||||
const localIndividualRankings = ref([...individualRankings]);
|
||||
const localTeamRankings = ref([...teamRankings]);
|
||||
const localDisplayConfig = ref({...displayConfig});
|
||||
const localBattleEndTime = ref({...battleEndTime});
|
||||
// 初始化本地战鼓配置副本
|
||||
const localDrumConfig = ref({...drumConfig});
|
||||
// 添加强拍位置的字符串表示,用于输入框
|
||||
localDrumConfig.value.pattern.strongBeatsStr = localDrumConfig.value.pattern.strongBeats?.join(',') || '1,4';
|
||||
|
||||
// 更新强拍位置数组
|
||||
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);
|
||||
@@ -298,6 +791,14 @@ const saveData = () => {
|
||||
// 调用保存方法
|
||||
saveIndividualRankings(localIndividualRankings.value);
|
||||
saveTeamRankings(localTeamRankings.value);
|
||||
saveDisplayConfig(localDisplayConfig.value);
|
||||
saveBattleEndTime(localBattleEndTime.value);
|
||||
// 保存战鼓配置前,确保强拍位置数组是最新的
|
||||
updateStrongBeats();
|
||||
// 移除临时的字符串表示,避免保存到配置中
|
||||
const configToSave = {...localDrumConfig.value};
|
||||
delete configToSave.pattern.strongBeatsStr;
|
||||
saveDrumConfig(configToSave);
|
||||
|
||||
alert('数据保存成功!');
|
||||
};
|
||||
@@ -461,7 +962,7 @@ const deleteTeam = (id) => {
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
width: calc(50% - 5px);
|
||||
padding: 12px;
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
@@ -470,12 +971,29 @@ const deleteTeam = (id) => {
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.home-btn {
|
||||
width: calc(50% - 5px);
|
||||
padding: 12px;
|
||||
background: linear-gradient(45deg, #28a745, #20c997);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.home-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e74c3c;
|
||||
text-align: center;
|
||||
@@ -555,6 +1073,149 @@ const deleteTeam = (id) => {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
/* 配置页面样式 */
|
||||
.config-content {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
color: #667eea;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.end-time-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;
|
||||
}
|
||||
|
||||
.time-setting-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
margin-top: 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 25px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.config-section h3 {
|
||||
color: #495057;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.config-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.column-widths-section {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #e3f2fd;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.column-widths-section h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: #1976d2;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.column-width-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.column-width-item label {
|
||||
width: 100px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.width-input {
|
||||
width: 80px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.width-select {
|
||||
width: 120px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-input,
|
||||
.select-input {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.select-input {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* 管理内容 */
|
||||
.management-header {
|
||||
display: flex;
|
||||
|
||||
@@ -12,13 +12,19 @@
|
||||
<span class="time-item">{{ hours }}时</span>
|
||||
<span class="time-item">{{ minutes }}分</span>
|
||||
<span class="time-item">{{ seconds }}秒</span>
|
||||
<span class="time-item milliseconds">{{ milliseconds }}毫秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 第二部分:战鼓动画 -->
|
||||
<section class="drums-section">
|
||||
<!-- 第二部分:战鼓动画(浮动并支持拖放) -->
|
||||
<section
|
||||
class="drums-section"
|
||||
@mousedown="startDrag"
|
||||
@click="handleDrumClick"
|
||||
:style="{ left: drumsPosition.x + 'px', top: drumsPosition.y + 'px' }"
|
||||
>
|
||||
<div class="drums-container">
|
||||
<!-- 战鼓动画在上面 -->
|
||||
<div class="drums-animation">
|
||||
@@ -59,19 +65,20 @@
|
||||
<div class="individual-rankings">
|
||||
<h2 class="section-title">👤 个人排名</h2>
|
||||
<div class="rank-table">
|
||||
<div class="table-header">
|
||||
<div class="table-header" :style="{ 'grid-template-columns': individualGridTemplate }">
|
||||
<span class="rank-col">排名</span>
|
||||
<span class="avatar-col">头像</span>
|
||||
<span class="name-col">姓名</span>
|
||||
<span class="score-col">得分</span>
|
||||
<span class="level-col">等级</span>
|
||||
<span class="dept-col">部门</span>
|
||||
<span class="score-col">{{ displayConfig.individual.scoreColumn.displayName }}</span>
|
||||
<span v-if="displayConfig.individual.showLevel" class="level-col">等级</span>
|
||||
<span v-if="displayConfig.individual.showDepartment" class="dept-col">部门</span>
|
||||
<span class="bonus-col">奖金</span>
|
||||
</div>
|
||||
<div
|
||||
<div
|
||||
v-for="(item, index) in individualRankings"
|
||||
:key="item.id"
|
||||
class="table-row"
|
||||
:style="{ 'grid-template-columns': individualGridTemplate }"
|
||||
:class="{
|
||||
'top-three': index < 3,
|
||||
'highlight': index === 0
|
||||
@@ -80,9 +87,9 @@
|
||||
<span class="rank-col">{{ index + 1 }}</span>
|
||||
<span class="avatar-col">{{ item.avatar }}</span>
|
||||
<span class="name-col">{{ item.name }}</span>
|
||||
<span class="score-col">{{ item.score }}</span>
|
||||
<span class="level-col" :class="`level-${item.level}`">{{ item.level }}</span>
|
||||
<span class="dept-col">{{ item.department }}</span>
|
||||
<span class="score-col">{{ displayConfig.individual.scoreColumn.displayStyle === 'amount' ? '¥' + item.score : item.score }}</span>
|
||||
<span v-if="displayConfig.individual.showLevel" class="level-col" :class="`level-${item.level}`">{{ item.level }}</span>
|
||||
<span v-if="displayConfig.individual.showDepartment" class="dept-col">{{ item.department }}</span>
|
||||
<span class="bonus-col">¥{{ item.bonus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,18 +99,19 @@
|
||||
<div class="team-rankings">
|
||||
<h2 class="section-title">👥 战队排名</h2>
|
||||
<div class="rank-table">
|
||||
<div class="table-header">
|
||||
<div class="table-header" :style="{ 'grid-template-columns': teamGridTemplate }">
|
||||
<span class="rank-col">排名</span>
|
||||
<span class="name-col">战队名称</span>
|
||||
<span class="score-col">总分</span>
|
||||
<span class="member-col">人数</span>
|
||||
<span class="leader-col">队长</span>
|
||||
<span class="score-col">{{ displayConfig.team.totalScoreColumn.displayName }}</span>
|
||||
<span v-if="displayConfig.team.showMemberCount" class="member-col">人数</span>
|
||||
<span v-if="displayConfig.team.showLeader" class="leader-col">队长</span>
|
||||
<span class="bonus-col">奖金</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in teamRankings"
|
||||
:key="item.id"
|
||||
class="table-row"
|
||||
:style="{ 'grid-template-columns': teamGridTemplate }"
|
||||
:class="{
|
||||
'top-three': index < 3,
|
||||
'highlight': index === 0
|
||||
@@ -111,9 +119,9 @@
|
||||
>
|
||||
<span class="rank-col">{{ index + 1 }}</span>
|
||||
<span class="name-col">{{ item.name }}</span>
|
||||
<span class="score-col">{{ item.totalScore }}</span>
|
||||
<span class="member-col">{{ item.memberCount }}人</span>
|
||||
<span class="leader-col">{{ item.leader }}</span>
|
||||
<span class="score-col">{{ displayConfig.team.totalScoreColumn.displayStyle === 'amount' ? '¥' + item.totalScore : item.totalScore }}</span>
|
||||
<span v-if="displayConfig.team.showMemberCount" class="member-col">{{ item.memberCount }}人</span>
|
||||
<span v-if="displayConfig.team.showLeader" class="leader-col">{{ item.leader }}</span>
|
||||
<span class="bonus-col">¥{{ item.bonus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,30 +139,130 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
individualRankings,
|
||||
teamRankings,
|
||||
bonusRules
|
||||
bonusRules,
|
||||
displayConfig,
|
||||
battleEndTime,
|
||||
drumConfig
|
||||
} from '../data/mockData.js';
|
||||
|
||||
// 创建本地显示配置的副本,确保深拷贝
|
||||
const localDisplayConfig = ref(JSON.parse(JSON.stringify(displayConfig)));
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 计算个人排名表格的列布局
|
||||
const individualGridTemplate = computed(() => {
|
||||
try {
|
||||
const config = localDisplayConfig.value?.individual;
|
||||
if (!config) return '60px 60px 1fr 80px 80px';
|
||||
|
||||
const widths = config.columnWidths || {};
|
||||
const cols = [];
|
||||
|
||||
cols.push((widths.rank || 60) + 'px'); // 排名
|
||||
cols.push((widths.avatar || 60) + 'px'); // 头像
|
||||
cols.push((widths.name === 1 || widths.name === '1') ? '1fr' : (widths.name || 120) + 'px'); // 姓名
|
||||
cols.push((widths.score || 80) + 'px'); // 分数
|
||||
|
||||
if (config.showLevel) {
|
||||
cols.push((widths.level || 80) + 'px'); // 等级
|
||||
}
|
||||
|
||||
if (config.showDepartment) {
|
||||
cols.push((widths.department === 1 || widths.department === '1') ? '1fr' : (widths.department || 100) + 'px'); // 部门
|
||||
}
|
||||
|
||||
cols.push((widths.bonus || 80) + 'px'); // 奖金
|
||||
|
||||
return cols.join(' ');
|
||||
} catch (error) {
|
||||
console.error('计算个人排名表格布局出错:', error);
|
||||
return '60px 60px 1fr 80px 80px'; // 兜底布局
|
||||
}
|
||||
});
|
||||
|
||||
// 计算战队排名表格的列布局
|
||||
const teamGridTemplate = computed(() => {
|
||||
try {
|
||||
const config = localDisplayConfig.value?.team;
|
||||
if (!config) return '60px 1fr 80px 80px';
|
||||
|
||||
const widths = config.columnWidths || {};
|
||||
const cols = [];
|
||||
|
||||
cols.push((widths.rank || 60) + 'px'); // 排名
|
||||
cols.push((widths.name === 1 || widths.name === '1') ? '1fr' : (widths.name || 150) + 'px'); // 战队名
|
||||
cols.push((widths.score || 80) + 'px'); // 分数
|
||||
|
||||
if (config.showMemberCount) {
|
||||
cols.push((widths.memberCount || 60) + 'px'); // 人数
|
||||
}
|
||||
|
||||
if (config.showLeader) {
|
||||
cols.push((widths.leader === 1 || widths.leader === '1') ? '1fr' : (widths.leader || 120) + 'px'); // 队长
|
||||
}
|
||||
|
||||
cols.push((widths.bonus || 80) + 'px'); // 奖金
|
||||
|
||||
return cols.join(' ');
|
||||
} catch (error) {
|
||||
console.error('计算战队排名表格布局出错:', error);
|
||||
return '60px 1fr 80px 80px'; // 兜底布局
|
||||
}
|
||||
});
|
||||
|
||||
// 倒计时状态
|
||||
const days = ref(0);
|
||||
const hours = ref(0);
|
||||
const minutes = ref(0);
|
||||
const seconds = ref(0);
|
||||
const milliseconds = 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 });
|
||||
let isDragging = false;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
|
||||
// 开始拖动
|
||||
const startDrag = (e) => {
|
||||
isDragging = true;
|
||||
dragOffset.x = e.clientX - drumsPosition.value.x;
|
||||
dragOffset.y = e.clientY - drumsPosition.value.y;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// 拖动中
|
||||
const drag = (e) => {
|
||||
if (isDragging) {
|
||||
drumsPosition.value.x = e.clientX - dragOffset.x;
|
||||
drumsPosition.value.y = e.clientY - dragOffset.y;
|
||||
}
|
||||
};
|
||||
|
||||
// 结束拖动
|
||||
const endDrag = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
// 计算倒计时
|
||||
const calculateCountdown = () => {
|
||||
const endDate = new Date('2024-12-31T23:59:59').getTime();
|
||||
// 使用配置的结束时间
|
||||
const endDateStr = `${battleEndTime.date}T${battleEndTime.time}`;
|
||||
const endDate = new Date(endDateStr).getTime();
|
||||
const now = new Date().getTime();
|
||||
const distance = endDate - now;
|
||||
|
||||
@@ -163,14 +271,166 @@ const calculateCountdown = () => {
|
||||
hours.value = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
minutes.value = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
seconds.value = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
// 确保显示0-999毫秒,并格式化为3位数字
|
||||
milliseconds.value = Math.floor(distance % 1000).toString().padStart(3, '0');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化音频上下文
|
||||
const initAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
};
|
||||
|
||||
// 播放战鼓音效
|
||||
const playDrumSound = (isStrongBeat = false) => {
|
||||
if (!audioContext || isPlayingSound.value) return;
|
||||
|
||||
isPlayingSound.value = true;
|
||||
|
||||
try {
|
||||
// 使用配置的音效参数
|
||||
const soundConfig = drumConfig?.sound || {};
|
||||
const patternConfig = drumConfig?.pattern || {};
|
||||
|
||||
// 创建振荡器节点
|
||||
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 startDrumAnimation = () => {
|
||||
// 使用配置的动画和节拍参数
|
||||
const animationConfig = drumConfig?.animation || {};
|
||||
const patternConfig = drumConfig?.pattern || {};
|
||||
|
||||
// 检查是否启用动画
|
||||
if (animationConfig.enabled === false) return;
|
||||
|
||||
let beatCount = 0;
|
||||
// 使用配置的节拍间隔
|
||||
const interval = animationConfig.beatInterval || 200;
|
||||
|
||||
beatInterval = setInterval(() => {
|
||||
isBeating.value = !isBeating.value;
|
||||
}, 500);
|
||||
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);
|
||||
};
|
||||
|
||||
// 处理点击战鼓播放音效
|
||||
const handleDrumClick = () => {
|
||||
// 如果音频上下文未初始化,初始化它
|
||||
if (!audioContext) {
|
||||
initAudioContext();
|
||||
}
|
||||
|
||||
// 如果音频上下文被暂停,恢复它
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
|
||||
// 触发战鼓动画和音效,使用配置的点击效果
|
||||
const animationConfig = drumConfig?.animation || {};
|
||||
isBeating.value = true;
|
||||
playDrumSound(true); // 点击总是强拍
|
||||
|
||||
setTimeout(() => {
|
||||
isBeating.value = false;
|
||||
}, animationConfig.clickBeatDuration || 250);
|
||||
};
|
||||
|
||||
// 跳转到管理员页面
|
||||
@@ -181,12 +441,71 @@ const goToAdmin = () => {
|
||||
onMounted(() => {
|
||||
calculateCountdown();
|
||||
countdownInterval = setInterval(calculateCountdown, 1000);
|
||||
// 延迟初始化音频上下文,避免浏览器自动播放限制
|
||||
setTimeout(() => {
|
||||
// 不自动初始化音频,等待用户交互后再初始化
|
||||
}, 1000);
|
||||
startDrumAnimation();
|
||||
});
|
||||
|
||||
// 监听窗口点击事件,用于用户交互后初始化音频上下文
|
||||
document.addEventListener('click', initAudioContext, { once: true });
|
||||
document.addEventListener('touchstart', initAudioContext, { once: true });
|
||||
|
||||
const handleResize = () => {
|
||||
// 计算并设置排名明细区域的最小高度,使其底部与视口对齐
|
||||
const rankingsSection = document.querySelector('.rankings-section');
|
||||
if (rankingsSection) {
|
||||
const windowHeight = window.innerHeight;
|
||||
const rankingsTop = rankingsSection.offsetTop;
|
||||
const rankingsMinHeight = windowHeight - rankingsTop - 20; // 减去一些边距
|
||||
if (rankingsMinHeight > 0) {
|
||||
rankingsSection.style.minHeight = rankingsMinHeight + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calculateCountdown();
|
||||
countdownInterval = setInterval(calculateCountdown, 10); // 改为10ms更新一次以显示毫秒
|
||||
startDrumAnimation();
|
||||
// 监听窗口大小变化,确保排名明细与底部对齐
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize(); // 初始调整
|
||||
|
||||
// 添加拖放相关的事件监听
|
||||
document.addEventListener('mousemove', drag);
|
||||
document.addEventListener('mouseup', endDrag);
|
||||
});
|
||||
|
||||
// 监听结束时间变化(在真实环境中,可能需要通过props或store来监听)
|
||||
const handleBattleEndTimeChange = () => {
|
||||
calculateCountdown();
|
||||
};
|
||||
|
||||
// 监听显示配置变化(在真实环境中,可能需要通过props或store来监听)
|
||||
const handleDisplayConfigChange = () => {
|
||||
// 更新本地配置
|
||||
localDisplayConfig.value = {...displayConfig};
|
||||
};
|
||||
|
||||
// 这里模拟监听配置变化
|
||||
// 在实际项目中,可能需要通过WebSocket或轮询来更新配置
|
||||
|
||||
onUnmounted(() => {
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
if (beatInterval) clearInterval(beatInterval);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
|
||||
// 移除拖放相关的事件监听
|
||||
document.removeEventListener('mousemove', drag);
|
||||
document.removeEventListener('mouseup', endDrag);
|
||||
|
||||
// 清理音频资源
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
audioContext = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -217,6 +536,11 @@ onUnmounted(() => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.timer {
|
||||
display: inline-block;
|
||||
background: linear-gradient(45deg, #6c5ce7, #a29bfe);
|
||||
@@ -224,7 +548,7 @@ onUnmounted(() => {
|
||||
padding: 12px 25px;
|
||||
border-radius: 50px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 14px; /* 下移4px */
|
||||
}
|
||||
|
||||
.time-item {
|
||||
@@ -232,17 +556,43 @@ onUnmounted(() => {
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
margin: 0 5px;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.5rem;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
/* 战鼓部分 */
|
||||
.time-item.milliseconds {
|
||||
font-size: 1.2rem;
|
||||
color: #ffcc00;
|
||||
font-weight: bold;
|
||||
animation: blink 0.5s infinite;
|
||||
}
|
||||
|
||||
/* 毫秒闪烁动画增强紧张感 */
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; transform: scale(1); }
|
||||
51%, 100% { opacity: 0.7; transform: scale(0.95); }
|
||||
}
|
||||
|
||||
/* 战鼓部分 - 浮动并支持拖放 */
|
||||
.drums-section {
|
||||
margin: 20px 0 10px 0;
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
cursor: move;
|
||||
z-index: 100;
|
||||
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 {
|
||||
@@ -258,11 +608,34 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.drum {
|
||||
transition: transform 0.3s ease;
|
||||
transition: transform 0.1s ease, filter 0.1s ease;
|
||||
animation: idlePulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
.drum.beating {
|
||||
transform: scale(1.2) translateY(-10px);
|
||||
/* 使用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 {
|
||||
@@ -276,7 +649,7 @@ onUnmounted(() => {
|
||||
|
||||
/* 奖金设置部分(行布局) */
|
||||
.bonus-section {
|
||||
margin: 0 20px 15px 20px;
|
||||
margin: 10px 20px 15px 20px; /* 下移10px */
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #ffeaa7, #fab1a0);
|
||||
border-radius: 20px;
|
||||
@@ -365,7 +738,8 @@ onUnmounted(() => {
|
||||
.rank-table {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
max-height: 400px;
|
||||
min-height: 600px; /* 确保至少显示10行 */
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
@@ -374,7 +748,6 @@ onUnmounted(() => {
|
||||
background: linear-gradient(45deg, #6c5ce7, #a29bfe);
|
||||
color: white;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 60px 1fr 80px 80px 1fr 80px;
|
||||
padding: 12px 10px;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
@@ -383,7 +756,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.team-rankings .table-header {
|
||||
grid-template-columns: 60px 1fr 80px 60px 1fr 80px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
@@ -391,14 +763,12 @@ onUnmounted(() => {
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 60px 1fr 80px 80px 1fr 80px;
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.team-rankings .table-row {
|
||||
grid-template-columns: 60px 1fr 80px 60px 1fr 80px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
@@ -408,6 +778,32 @@ onUnmounted(() => {
|
||||
|
||||
.table-row.top-three {
|
||||
background-color: #fff3cd;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.table-row.top-three:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 前三名特殊背景色 */
|
||||
.table-row:nth-child(1) {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-row:nth-child(2) {
|
||||
background: linear-gradient(135deg, #c0c0c0, #e0e0e0);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-row:nth-child(3) {
|
||||
background: linear-gradient(135deg, #cd7f32, #d7ccc8);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-row.highlight {
|
||||
|
||||
Reference in New Issue
Block a user