修改部分规则模块删除无用前端 新增前端界面

This commit is contained in:
zouju
2026-02-06 17:22:22 +08:00
parent 000f8d4bc5
commit a440094138
583 changed files with 26241 additions and 26046 deletions

View File

@@ -0,0 +1,692 @@
<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>