930 lines
26 KiB
Vue
930 lines
26 KiB
Vue
<template>
|
||
<div class="battle-ranking">
|
||
<!-- 第一部分:百日大战主题 -->
|
||
<section class="theme-section">
|
||
<div class="theme-container">
|
||
<h1 class="main-title">🔥 百人大战排行榜 🔥</h1>
|
||
<p class="subtitle">2025年度精英挑战赛 | 百日冲刺 · 争创佳绩</p>
|
||
<div class="timer">
|
||
<span class="label">距离结束还有:</span>
|
||
<div class="countdown">
|
||
<span class="time-item">{{ days }}天</span>
|
||
<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"
|
||
@mousedown="startDrag"
|
||
@click="handleDrumClick"
|
||
:style="{ left: drumsPosition.x + 'px', top: drumsPosition.y + 'px' }"
|
||
>
|
||
<div class="drums-container">
|
||
<!-- 战鼓动画在上面 -->
|
||
<div class="drums-animation">
|
||
<div class="drum" :class="{ beating: isBeating }">🥁</div>
|
||
<div class="drum" :class="{ beating: isBeating }">🥁</div>
|
||
<div class="trophy">🏆</div>
|
||
<div class="drum" :class="{ beating: isBeating }">🥁</div>
|
||
<div class="drum" :class="{ beating: isBeating }">🥁</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 第三部分:奖金设置(行布局) -->
|
||
<section class="bonus-section">
|
||
<h2>🎯 奖金设置</h2>
|
||
<div class="bonus-rules-row">
|
||
<div
|
||
v-for="(rule, index) in displayBonusRules"
|
||
:key="index"
|
||
class="bonus-rule-item"
|
||
>
|
||
<div class="rule-header">
|
||
<span class="rank-range">名次: {{ rule.rank }}</span>
|
||
<span class="rule-desc">{{ rule.description }}</span>
|
||
</div>
|
||
<div class="rule-details">
|
||
<p>🏅 个人奖励: {{ rule.individualBonus }}</p>
|
||
<p>👥 团队奖励: {{ rule.teamBonus }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 第四部分:排名明细 -->
|
||
<section class="rankings-section">
|
||
<div class="rankings-container">
|
||
<!-- 个人排名 -->
|
||
<div class="individual-rankings">
|
||
<h2 class="section-title">👤 个人排名</h2>
|
||
<div class="rank-table">
|
||
<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">{{ localDisplayConfig.individual?.scoreColumn?.displayName || '得分' }}</span>
|
||
<span v-if="localDisplayConfig.individual?.showLevel" class="level-col">等级</span>
|
||
<span v-if="localDisplayConfig.individual?.showDepartment" class="dept-col">部门</span>
|
||
<span class="bonus-col">奖金</span>
|
||
</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
|
||
}"
|
||
>
|
||
<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">{{ localDisplayConfig.individual?.scoreColumn?.displayStyle === 'amount' ? '¥' + item.score : item.score }}</span>
|
||
<span v-if="localDisplayConfig.individual?.showLevel" class="level-col" :class="`level-${item.level}`">{{ item.level }}</span>
|
||
<span v-if="localDisplayConfig.individual?.showDepartment" class="dept-col">{{ item.department }}</span>
|
||
<span class="bonus-col">¥{{ item.bonus }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 战队排名 -->
|
||
<div class="team-rankings">
|
||
<h2 class="section-title">👥 战队排名</h2>
|
||
<div class="rank-table">
|
||
<div class="table-header" :style="{ 'grid-template-columns': teamGridTemplate }">
|
||
<span class="rank-col">排名</span>
|
||
<span class="name-col">战队名称</span>
|
||
<span class="score-col">{{ localDisplayConfig.team?.totalScoreColumn?.displayName || '总分' }}</span>
|
||
<span v-if="localDisplayConfig.team?.showMemberCount" class="member-col">人数</span>
|
||
<span v-if="localDisplayConfig.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
|
||
}"
|
||
>
|
||
<span class="rank-col">{{ index + 1 }}</span>
|
||
<span class="name-col">{{ item.name }}</span>
|
||
<span class="score-col">{{ localDisplayConfig.team?.totalScoreColumn?.displayStyle === 'amount' ? '¥' + item.totalScore : item.totalScore }}</span>
|
||
<span v-if="localDisplayConfig.team?.showMemberCount" class="member-col">{{ item.memberCount }}人</span>
|
||
<span v-if="localDisplayConfig.team?.showLeader" class="leader-col">{{ item.leader }}</span>
|
||
<span class="bonus-col">¥{{ item.bonus }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 浮动管理员入口 -->
|
||
<div class="admin-entry-float">
|
||
<button @click="goToAdmin" class="admin-btn-float">
|
||
🔐 管理员入口
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import {
|
||
individualRankings,
|
||
teamRankings,
|
||
bonusRules,
|
||
displayConfig,
|
||
battleEndTime,
|
||
drumConfig,
|
||
initializeData
|
||
} from '../data/mockData.js';
|
||
|
||
// 创建本地显示配置的副本,确保深拷贝并提供默认值
|
||
const createDefaultDisplayConfig = () => ({
|
||
individual: {
|
||
scoreColumn: {
|
||
displayName: '得分',
|
||
displayStyle: 'number'
|
||
},
|
||
showLevel: true,
|
||
showDepartment: true,
|
||
columnWidths: {}
|
||
},
|
||
team: {
|
||
totalScoreColumn: {
|
||
displayName: '总分',
|
||
displayStyle: 'number'
|
||
},
|
||
showMemberCount: true,
|
||
showLeader: true,
|
||
columnWidths: {}
|
||
}
|
||
});
|
||
|
||
// 创建默认奖金规则
|
||
const createDefaultBonusRules = () => [
|
||
{ rank: '1-3', description: '前三名', individualBonus: '¥10000, ¥8000, ¥5000', teamBonus: '¥50000, ¥30000, ¥20000' },
|
||
{ rank: '4-10', description: '四至十名', individualBonus: '¥3000/人', teamBonus: '¥10000/队' },
|
||
{ rank: '11-20', description: '十一至二十名', individualBonus: '¥1000/人', teamBonus: '¥5000/队' }
|
||
];
|
||
|
||
const localDisplayConfig = ref(
|
||
displayConfig ? JSON.parse(JSON.stringify(displayConfig)) : createDefaultDisplayConfig()
|
||
);
|
||
|
||
// 确保奖金规则有默认值
|
||
const displayBonusRules = computed(() => {
|
||
return Array.isArray(bonusRules) && bonusRules.length > 0
|
||
? bonusRules
|
||
: createDefaultBonusRules();
|
||
});
|
||
|
||
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 endDateStr = `${battleEndTime.date}T${battleEndTime.time}`;
|
||
const endDate = new Date(endDateStr).getTime();
|
||
const now = new Date().getTime();
|
||
const distance = endDate - now;
|
||
|
||
if (distance > 0) {
|
||
days.value = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||
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(() => {
|
||
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);
|
||
};
|
||
|
||
// 跳转到管理员页面
|
||
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');
|
||
if (rankingsSection) {
|
||
const windowHeight = window.innerHeight;
|
||
const rankingsTop = rankingsSection.offsetTop;
|
||
const rankingsMinHeight = windowHeight - rankingsTop - 20; // 减去一些边距
|
||
if (rankingsMinHeight > 0) {
|
||
rankingsSection.style.minHeight = rankingsMinHeight + 'px';
|
||
}
|
||
}
|
||
};
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
// 异步初始化数据
|
||
await initializeData();
|
||
// 更新本地显示配置
|
||
localDisplayConfig.value = displayConfig ? JSON.parse(JSON.stringify(displayConfig)) : createDefaultDisplayConfig();
|
||
} catch (error) {
|
||
console.error('初始化数据失败:', error);
|
||
// 使用默认配置
|
||
localDisplayConfig.value = createDefaultDisplayConfig();
|
||
}
|
||
|
||
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>
|
||
|
||
<style scoped>
|
||
.battle-ranking {
|
||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||
padding-bottom: 30px;
|
||
}
|
||
|
||
/* 主题部分 */
|
||
.theme-section {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 40px 0;
|
||
text-align: center;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.main-title {
|
||
font-size: 2.5rem;
|
||
color: #d63031;
|
||
margin-bottom: 8px;
|
||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 1.2rem;
|
||
color: #636e72;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.countdown {
|
||
font-size: 1.2rem;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.timer {
|
||
display: inline-block;
|
||
background: linear-gradient(45deg, #6c5ce7, #a29bfe);
|
||
color: white;
|
||
padding: 12px 25px;
|
||
border-radius: 50px;
|
||
font-weight: bold;
|
||
margin-bottom: 14px; /* 下移4px */
|
||
}
|
||
|
||
.time-item {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 5px 10px;
|
||
border-radius: 5px;
|
||
margin: 0 5px;
|
||
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 {
|
||
position: fixed;
|
||
left: 20px;
|
||
top: 20px;
|
||
padding: 20px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 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 {
|
||
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); }
|
||
}
|
||
|
||
/* 奖金设置部分(行布局) */
|
||
.bonus-section {
|
||
margin: 10px 20px 15px 20px; /* 下移10px */
|
||
padding: 15px;
|
||
background: linear-gradient(135deg, #ffeaa7, #fab1a0);
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.bonus-section h2 {
|
||
color: #d63031;
|
||
margin-bottom: 10px;
|
||
text-align: center;
|
||
font-size: 1.4rem;
|
||
}
|
||
|
||
.bonus-rules-row {
|
||
display: flex;
|
||
gap: 30px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
width: 100%;
|
||
}
|
||
|
||
.bonus-rule-item {
|
||
background: rgba(255, 255, 255, 0.8);
|
||
padding: 8px 10px;
|
||
border-radius: 12px;
|
||
border-left: 5px solid #e17055;
|
||
flex: 1;
|
||
min-width: 220px;
|
||
max-width: 100%;
|
||
transition: transform 0.3s ease;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.bonus-rule-item:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.rule-header {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
font-weight: bold;
|
||
color: #d63031;
|
||
font-size: 1.2rem;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.rule-details p {
|
||
margin: 3px 0;
|
||
font-size: 1rem;
|
||
word-wrap: break-word;
|
||
white-space: normal;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
/* 排名部分 */
|
||
.rankings-section {
|
||
margin: 0 20px 30px 20px;
|
||
}
|
||
|
||
.rankings-container {
|
||
display: flex;
|
||
gap: 15px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.individual-rankings,
|
||
.team-rankings {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 20px;
|
||
padding: 20px;
|
||
flex: 1;
|
||
min-width: 350px;
|
||
max-width: 550px;
|
||
}
|
||
|
||
.section-title {
|
||
color: #d63031;
|
||
text-align: center;
|
||
margin-bottom: 15px;
|
||
font-size: 1.6rem;
|
||
}
|
||
|
||
.rank-table {
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
min-height: 600px; /* 确保至少显示10行 */
|
||
max-height: 600px;
|
||
overflow-y: auto;
|
||
position: relative;
|
||
}
|
||
|
||
.table-header {
|
||
background: linear-gradient(45deg, #6c5ce7, #a29bfe);
|
||
color: white;
|
||
display: grid;
|
||
padding: 12px 10px;
|
||
font-weight: bold;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.team-rankings .table-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.table-row {
|
||
display: grid;
|
||
padding: 12px 10px;
|
||
border-bottom: 1px solid #eee;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.team-rankings .table-row {
|
||
padding: 12px 10px;
|
||
}
|
||
|
||
.table-row:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.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 {
|
||
background: linear-gradient(135deg, #ffd32a, #ff7979);
|
||
color: white;
|
||
}
|
||
|
||
.level-SSS {
|
||
color: #ffd700;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.level-SS {
|
||
color: #c0c0c0;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.level-S {
|
||
color: #cd7f32;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.level-A {
|
||
color: #4caf50;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.level-B {
|
||
color: #2196f3;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 浮动管理员入口 */
|
||
.admin-entry-float {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.admin-btn-float {
|
||
background: linear-gradient(45deg, #6c5ce7, #a29bfe);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 20px;
|
||
border-radius: 30px;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 20px rgba(108, 92, 231, 0.4);
|
||
transition: all 0.3s ease;
|
||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.admin-btn-float:hover {
|
||
transform: scale(1.1) translateY(-2px);
|
||
box-shadow: 0 6px 30px rgba(108, 92, 231, 0.6);
|
||
background: linear-gradient(45deg, #7f7fd5, #86a8e7);
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.main-title {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.drums-container {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.rankings-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.individual-rankings,
|
||
.team-rankings {
|
||
min-width: auto;
|
||
}
|
||
|
||
.table-header,
|
||
.table-row {
|
||
font-size: 0.9rem;
|
||
}
|
||
}
|
||
</style> |