692 lines
19 KiB
Vue
692 lines
19 KiB
Vue
<template>
|
||
<a-modal :open="visible" class="ks-ranking-modal" centered width="98%" :footer="null" @cancel="handleCancel">
|
||
<div class="modal-overlay"></div>
|
||
<div class="ranking-container">
|
||
<!-- 表头 -->
|
||
<div class="table-header">
|
||
<div class="header-row">
|
||
<div class="header-cell rank">排名</div>
|
||
<div class="header-cell team">团队</div>
|
||
<div class="header-cell red-win-rate">红方胜率</div>
|
||
<div class="header-cell blue-win-rate">蓝方胜率</div>
|
||
<div class="header-cell red-matches">红方场次</div>
|
||
<div class="header-cell blue-matches">蓝方场次</div>
|
||
<div class="header-cell invalid-matches">博弈关键节点</div>
|
||
<div class="header-cell total-score">总分</div>
|
||
<div class="header-cell total-win-rate">总胜率</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 排行榜主体 -->
|
||
<div class="ranking-body">
|
||
<template v-for="(team, index) in sortedTeams">
|
||
<div
|
||
class="team-row"
|
||
:class="[team.isFlipping ? 'flipping' : '', indexClasses[index] ? indexClasses[index] : '']"
|
||
:style="getRowStyle(index)"
|
||
>
|
||
<div class="row-front">
|
||
<div class="cell rank" :class="getRankClass(team.rank)">
|
||
<span class="rank-number">{{ team.rank }}</span>
|
||
<!-- <span v-if="team.rankChange !== 0" class="rank-change" :class="getChangeClass(team.rankChange)">-->
|
||
<!-- {{ getChangeSymbol(team.rankChange) }}-->
|
||
<!-- </span>-->
|
||
</div>
|
||
<div class="cell team">
|
||
<div class="team-info">
|
||
<div class="team-name">{{ team.name }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="cell red-win-rate">
|
||
{{ String(team.redWinRate).substring(0, 5) }}%
|
||
</div>
|
||
<div class="cell blue-win-rate">
|
||
{{ String(team.blueWinRate).substring(0, 5) }}%
|
||
</div>
|
||
<div class="cell red-matches">{{ team.redMatches }}</div>
|
||
<div class="cell blue-matches">{{ team.blueMatches }}</div>
|
||
<div class="cell invalid-matches">{{ team.invalidMatches }}</div>
|
||
<div class="cell total-score">{{ team.totalScore }}</div>
|
||
<div class="cell total-win-rate">
|
||
{{ String(team.totalWinRate).substring(0, 5) }}%
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 行背面(翻转时显示) -->
|
||
<div class="row-back">
|
||
<div class="back-content">
|
||
<div class="back-title">团队详情</div>
|
||
<div class="back-stats">
|
||
<div>总场次: {{ team.totalMatches }}</div>
|
||
<div>红方胜场: {{ team.redWins }}</div>
|
||
<div>蓝方胜场: {{ team.blueWins }}</div>
|
||
<div>连续胜场: {{ team.winStreak }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 控制面板 -->
|
||
<!-- <div class="control-panel">-->
|
||
<!-- <button @click="toggleAutoRefresh" :class="{ active: autoRefresh }">-->
|
||
<!-- {{ autoRefresh ? '暂停更新' : '开始更新' }}-->
|
||
<!-- </button>-->
|
||
<!-- <button @click="manualUpdate">手动更新排名</button>-->
|
||
<!-- <button @click="triggerFlip">手动翻转</button>-->
|
||
<!-- <div class="timer-display">-->
|
||
<!-- 下次更新: {{ nextUpdateTime }}秒-->
|
||
<!-- </div>-->
|
||
<!-- </div>-->
|
||
</div>
|
||
|
||
<template #title>
|
||
<div class="header-export-button">
|
||
<a-tooltip title="排名结果导出" placement="bottom">
|
||
<CloudDownloadOutlined class="download-icon" @click="handleExport"/>
|
||
<!-- <span style="margin-left:10px;">排名结果导出</span>-->
|
||
</a-tooltip>
|
||
</div>
|
||
</template>
|
||
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { computed, defineComponent, onMounted, onUnmounted, ref, type CSSProperties, type SetupContext } from 'vue';
|
||
import {CloudDownloadOutlined} from '@ant-design/icons-vue';
|
||
// 定义团队数据接口
|
||
interface Team {
|
||
id: number;
|
||
name: string;
|
||
rank: number;
|
||
redWinRate: number;
|
||
blueWinRate: number;
|
||
redMatches: number;
|
||
blueMatches: number;
|
||
invalidMatches: string;
|
||
totalScore: number;
|
||
totalWinRate: number;
|
||
rankChange: number;
|
||
isFlipping?: boolean;
|
||
// 计算属性(实例getter)
|
||
totalMatches: number;
|
||
redWins: number;
|
||
blueWins: number;
|
||
winStreak: number;
|
||
}
|
||
|
||
// 定义Props类型
|
||
interface RankProps {
|
||
visible: boolean;
|
||
}
|
||
|
||
// 定义Emits类型
|
||
type RankEmits = {
|
||
cancel: []; // 无参数的cancel事件
|
||
};
|
||
|
||
// 团队数据(添加类型注解)
|
||
const teams = ref<Team[]>([
|
||
{ id: 1, name: '团队3', rank: 1, redWinRate: 85, blueWinRate: 72, redMatches: 20, blueMatches: 18, invalidMatches: "2:30'50''", totalScore: 95, totalWinRate: 78, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 2, name: '红方', rank: 2, redWinRate: 78, blueWinRate: 81, redMatches: 22, blueMatches: 20, invalidMatches: "1:31'30''", totalScore: 92, totalWinRate: 79, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 3, name: '团队3', rank: 3, redWinRate: 80, blueWinRate: 75, redMatches: 18, blueMatches: 16, invalidMatches: "2:10'22''", totalScore: 90, totalWinRate: 77, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 4, name: '团队4', rank: 4, redWinRate: 72, blueWinRate: 85, redMatches: 19, blueMatches: 21, invalidMatches: "3:01'12''", totalScore: 88, totalWinRate: 76, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 5, name: '团队8', rank: 5, redWinRate: 68, blueWinRate: 79, redMatches: 17, blueMatches: 19, invalidMatches: "2:45'21''", totalScore: 85, totalWinRate: 73, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 6, name: '团队5', rank: 6, redWinRate: 75, blueWinRate: 70, redMatches: 16, blueMatches: 15, invalidMatches: "1:02'33''", totalScore: 82, totalWinRate: 72, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 7, name: '团队7', rank: 7, redWinRate: 70, blueWinRate: 65, redMatches: 14, blueMatches: 13, invalidMatches: "3:20'10''", totalScore: 78, totalWinRate: 68, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 8, name: '团队6', rank: 8, redWinRate: 65, blueWinRate: 72, redMatches: 12, blueMatches: 14, invalidMatches: "2:33'16''", totalScore: 75, totalWinRate: 67, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 9, name: '蓝方0', rank: 9, redWinRate: 60, blueWinRate: 68, redMatches: 10, blueMatches: 12, invalidMatches: "2:12'54''", totalScore: 70, totalWinRate: 63, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
{ id: 10, name: '团队9', rank: 10, redWinRate: 55, blueWinRate: 60, redMatches: 8, blueMatches: 10, invalidMatches: "3:01'02''", totalScore: 65, totalWinRate: 58, rankChange: 0,
|
||
totalMatches: 0, redWins: 0, blueWins: 0, winStreak: 0 },
|
||
]);
|
||
|
||
export default defineComponent({
|
||
components: {
|
||
CloudDownloadOutlined,
|
||
},
|
||
props: {
|
||
visible: {
|
||
type: Boolean,
|
||
required: true
|
||
},
|
||
},
|
||
emits: {
|
||
cancel: () => true
|
||
},
|
||
setup(_props: RankProps, { emit }: SetupContext<RankEmits>) {
|
||
const autoRefresh = ref<boolean>(true);
|
||
const nextUpdateTime = ref<number>(30);
|
||
|
||
let rankTimer: any | null = null;
|
||
let flipTimer: any | null = null;
|
||
let countdownTimer: any | null = null;
|
||
|
||
const indexClasses: Record<number, string> = {
|
||
0: 'first-row',
|
||
1: 'second-row',
|
||
2: 'third-row',
|
||
};
|
||
|
||
const sortedTeams = computed<Team[]>(() => {
|
||
return [...teams.value].sort((a, b) => a.rank - b.rank);
|
||
});
|
||
|
||
const getRowStyle = (index: number): CSSProperties => {
|
||
const delay = index * 0.1;
|
||
return {
|
||
'--flip-delay': `${delay}s`,
|
||
};
|
||
};
|
||
|
||
// 获取排名样式类
|
||
const getRankClass = (rank: number): string => {
|
||
if (rank === 1) return 'rank-first';
|
||
if (rank === 2) return 'rank-second';
|
||
if (rank === 3) return 'rank-third';
|
||
return '';
|
||
};
|
||
|
||
// 获取变化样式类
|
||
const getChangeClass = (change: number): string => {
|
||
if (change > 0) return 'change-up';
|
||
if (change < 0) return 'change-down';
|
||
return '';
|
||
};
|
||
|
||
// 获取变化符号
|
||
const getChangeSymbol = (change: number): string => {
|
||
if (change > 0) return `↑${change}`;
|
||
if (change < 0) return `↓${Math.abs(change)}`;
|
||
return '-';
|
||
};
|
||
|
||
// 手动更新排名
|
||
const manualUpdate = (): void => {
|
||
updateRankings();
|
||
};
|
||
|
||
// 手动触发翻转
|
||
const triggerFlip = (): void => {
|
||
teams.value.forEach(team => {
|
||
team.isFlipping = true;
|
||
setTimeout(() => {
|
||
team.isFlipping = false;
|
||
}, 600);
|
||
});
|
||
};
|
||
|
||
// 切换自动更新
|
||
const toggleAutoRefresh = (): void => {
|
||
autoRefresh.value = !autoRefresh.value;
|
||
if (autoRefresh.value) {
|
||
startTimers();
|
||
} else {
|
||
clearTimers();
|
||
}
|
||
};
|
||
|
||
// 更新排名逻辑
|
||
const updateRankings = (): void => {
|
||
// 保存旧排名(Record类型:key为team.id,value为rank)
|
||
const oldRanks: Record<number, number> = teams.value.reduce((acc, team) => {
|
||
acc[team.id] = team.rank;
|
||
return acc;
|
||
}, {} as Record<number, number>);
|
||
|
||
// 随机打乱并重新分配排名
|
||
const shuffled: Team[] = [...teams.value]
|
||
.sort(() => Math.random() - 0.5)
|
||
.map((team, index) => ({
|
||
...team,
|
||
rank: index + 1,
|
||
// 随机更新一些数据(保持数值范围合理性)
|
||
redWinRate: Math.min(100, Math.max(50, team.redWinRate + (Math.random() * 10 - 5))),
|
||
blueWinRate: Math.min(100, Math.max(50, team.blueWinRate + (Math.random() * 10 - 5))),
|
||
redMatches: team.redMatches + Math.floor(Math.random() * 3),
|
||
blueMatches: team.blueMatches + Math.floor(Math.random() * 2),
|
||
totalScore: team.totalScore + Math.floor(Math.random() * 5),
|
||
totalWinRate: Math.min(100, Math.max(50, team.totalWinRate + (Math.random() * 8 - 4))),
|
||
}))
|
||
.map(team => {
|
||
// 计算排名变化
|
||
const oldRank = oldRanks[team.id] as number;
|
||
team.rankChange = oldRank - team.rank;
|
||
return team;
|
||
});
|
||
|
||
teams.value = shuffled;
|
||
};
|
||
|
||
// 翻转动画
|
||
const flipRows = (): void => {
|
||
teams.value.forEach(team => {
|
||
team.isFlipping = true;
|
||
setTimeout(() => {
|
||
team.isFlipping = false;
|
||
}, 600);
|
||
});
|
||
};
|
||
|
||
// 开始定时器
|
||
const startTimers = (): void => {
|
||
// 排名更新定时器(30秒)
|
||
rankTimer = setInterval(() => {
|
||
updateRankings();
|
||
}, 30000);
|
||
|
||
// 翻转定时器(15秒)
|
||
flipTimer = setInterval(() => {
|
||
flipRows();
|
||
}, 15000);
|
||
|
||
// 倒计时显示
|
||
nextUpdateTime.value = 30;
|
||
countdownTimer = setInterval(() => {
|
||
nextUpdateTime.value--;
|
||
if (nextUpdateTime.value <= 0) {
|
||
nextUpdateTime.value = 30;
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
// 清除定时器
|
||
const clearTimers = (): void => {
|
||
if (rankTimer) clearInterval(rankTimer);
|
||
if (flipTimer) clearInterval(flipTimer);
|
||
if (countdownTimer) clearInterval(countdownTimer);
|
||
// 重置定时器变量
|
||
rankTimer = null;
|
||
flipTimer = null;
|
||
countdownTimer = null;
|
||
};
|
||
|
||
// 生命周期:挂载时初始化
|
||
onMounted(() => {
|
||
// 为每个团队添加计算属性(实例getter)
|
||
teams.value = teams.value.map(team => ({
|
||
...team,
|
||
isFlipping: false,
|
||
get totalMatches() {
|
||
// 注意:invalidMatches是时间字符串,这里原逻辑有问题,暂时保持原有写法
|
||
return this.redMatches + this.blueMatches + this.invalidMatches.length;
|
||
},
|
||
get redWins() {
|
||
return Math.round(this.redMatches * this.redWinRate / 100);
|
||
},
|
||
get blueWins() {
|
||
return Math.round(this.blueMatches * this.blueWinRate / 100);
|
||
},
|
||
get winStreak() {
|
||
return Math.floor(Math.random() * 10) + 1;
|
||
},
|
||
}));
|
||
|
||
if (autoRefresh.value) {
|
||
startTimers();
|
||
}
|
||
});
|
||
|
||
// 生命周期:卸载时清理
|
||
onUnmounted(() => {
|
||
clearTimers();
|
||
});
|
||
|
||
// 取消事件处理
|
||
const handleCancel = (): void => emit('cancel');
|
||
|
||
const handleExport = () => {
|
||
try {
|
||
// 创建临时a标签用于触发下载
|
||
const link = document.createElement('a');
|
||
// 设置导出接口地址(如果有URL参数可直接拼接,如:/api/xxx?startTime=2026-01-01)
|
||
link.href = '/api/modelDeduction/downloadRankData';
|
||
// 自定义下载文件名(后端也可通过响应头覆盖此值)
|
||
link.download = '排名数据.xlsx';
|
||
// 部分浏览器需要将a标签加入DOM才能触发下载
|
||
document.body.appendChild(link);
|
||
// 触发点击下载
|
||
link.click();
|
||
// 下载完成后移除临时标签,清理DOM
|
||
document.body.removeChild(link);
|
||
} catch (error) {
|
||
// 异常捕获,给用户友好提示
|
||
console.error('导出失败:', error);
|
||
alert('数据导出失败,请稍后重试!');
|
||
}
|
||
};
|
||
|
||
return {
|
||
handleCancel,
|
||
indexClasses,
|
||
sortedTeams,
|
||
getRowStyle,
|
||
getRankClass,
|
||
getChangeClass,
|
||
toggleAutoRefresh,
|
||
autoRefresh,
|
||
manualUpdate,
|
||
triggerFlip,
|
||
nextUpdateTime,
|
||
getChangeSymbol,
|
||
handleExport,
|
||
};
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style lang="less">
|
||
|
||
.ks-ranking-modal {
|
||
position: relative;
|
||
background: #0d1f34;
|
||
background: url('@/assets/rank/titled-container.png') center / 100% 100%;
|
||
|
||
.ant-modal-close {
|
||
right: 65px;
|
||
top: 65px;
|
||
}
|
||
|
||
.ant-modal-content{
|
||
position: relative;
|
||
}
|
||
|
||
.export-button{
|
||
position: absolute;
|
||
right: 60px;
|
||
bottom: 40px;
|
||
}
|
||
|
||
.header-export-button{
|
||
position: absolute;
|
||
right: 100px;
|
||
top: 68px;
|
||
.download-icon{
|
||
cursor: pointer;
|
||
width: 23px;
|
||
height: 23px;
|
||
font-size: 14px;
|
||
border-radius: 50%;
|
||
border: 1px solid #10e5ff;
|
||
text-align: center;
|
||
display: block;
|
||
line-height: 20px;
|
||
color: #10e5ff;
|
||
}
|
||
}
|
||
|
||
//.modal-overlay {
|
||
// background: #000000b0;
|
||
// position: absolute;
|
||
// width: 100%;
|
||
// height: 100%;
|
||
// z-index: -1;
|
||
//}
|
||
|
||
.ranking-container {
|
||
padding: 10px 30px;
|
||
}
|
||
|
||
.table-header {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
margin-top: 100px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.header-row {
|
||
display: grid;
|
||
grid-template-columns: 0.8fr 1.5fr 1.2fr 1.2fr 1fr 1fr 1fr 0.8fr 1.2fr;
|
||
padding: 15px 20px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header-cell {
|
||
color: #8da2c0;
|
||
font-weight: bold;
|
||
font-size: 13px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
&.rank{
|
||
margin-left: 15px;
|
||
}
|
||
}
|
||
|
||
.ranking-body {
|
||
min-height: 600px;
|
||
}
|
||
|
||
.team-row {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
position: relative;
|
||
transform-style: preserve-3d;
|
||
transition: transform 0.6s ease;
|
||
transform-origin: center center;
|
||
height: 50px;
|
||
overflow: hidden;
|
||
background: url('@/assets/rank/bg-3.png') center / 100% 100%;
|
||
|
||
.rank-number {
|
||
width: 60px;
|
||
height: 40px;
|
||
text-align: center;
|
||
font-size: 18px;
|
||
color: #fff;
|
||
line-height: 40px;
|
||
}
|
||
|
||
&.first-row {
|
||
background: url('@/assets/rank/bg-1.png') center / 100% 100%;
|
||
|
||
.rank-number {
|
||
background: url('@/assets/rank/icon-1.png') center / 100% 100%;
|
||
}
|
||
}
|
||
|
||
&.second-row {
|
||
background: url('@/assets/rank/bg-2.png') center / 100% 100%;
|
||
|
||
.rank-number {
|
||
background: url('@/assets/rank/icon-2.png') center / 100% 100%;
|
||
}
|
||
}
|
||
|
||
&.third-row {
|
||
.rank-number {
|
||
background: url('@/assets/rank/icon-3.png') center / 100% 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.team-row.flipping {
|
||
transform: rotateX(180deg);
|
||
transition-delay: var(--flip-delay);
|
||
}
|
||
|
||
.row-front,
|
||
.row-back {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
backface-visibility: hidden;
|
||
display: grid;
|
||
grid-template-columns: 0.8fr 1.5fr 1.2fr 1.2fr 1fr 1fr 1fr 0.8fr 1.2fr;
|
||
padding: 0 20px;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.row-back {
|
||
background: linear-gradient(135deg, #4a00e0 0%, #8e2de2 100%);
|
||
transform: rotateX(180deg);
|
||
color: white;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.back-content {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.back-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.back-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px 30px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.rank,
|
||
.cell {
|
||
color: #fff;
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
//.rank {
|
||
// font-weight: bold;
|
||
// font-size: 18px;
|
||
// justify-content: center;
|
||
// position: relative;
|
||
//}
|
||
|
||
.rank-first {
|
||
color: #ffd700;
|
||
}
|
||
|
||
.rank-second {
|
||
color: #c0c0c0;
|
||
}
|
||
|
||
.rank-third {
|
||
color: #cd7f32;
|
||
}
|
||
|
||
.rank-number {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.rank-change {
|
||
font-size: 12px;
|
||
padding: 2px 6px;
|
||
border-radius: 10px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.change-up {
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.change-down {
|
||
color: #f44336;
|
||
}
|
||
|
||
.team-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.team-name {
|
||
font-weight: bold;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.team-id {
|
||
font-size: 11px;
|
||
color: #8da2c0;
|
||
}
|
||
|
||
.progress-bar {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
height: 20px;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.progress-bar.blue .progress-fill {
|
||
background: linear-gradient(90deg, #2196F3, #03A9F4);
|
||
}
|
||
|
||
.progress-bar.total .progress-fill {
|
||
background: linear-gradient(90deg, #FF9800, #FFC107);
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #f44336, #ff9800);
|
||
border-radius: 10px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.progress-text {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 12px;
|
||
color: white;
|
||
font-weight: bold;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.control-panel {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
margin-top: 25px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.control-panel button {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: white;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.control-panel button:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.control-panel button.active {
|
||
background: linear-gradient(135deg, #4a00e0, #8e2de2);
|
||
}
|
||
|
||
.timer-display {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 20px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 6px;
|
||
color: #8da2c0;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
</style> |