chore: 纯网页版本

This commit is contained in:
2025-11-11 17:29:51 +08:00
parent 867beb5de7
commit 22016ac339
4 changed files with 1356 additions and 98 deletions

View File

@@ -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 {