Files
auto-solution/modeler/src/views/ai/applications/ranking-modal.vue

692 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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.idvalue为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>