|
@@ -18,7 +18,7 @@
|
|
|
<p>随时掌握运动会情况</p>
|
|
<p>随时掌握运动会情况</p>
|
|
|
</div>
|
|
</div>
|
|
|
<el-row :gutter="20" class="ranking-row">
|
|
<el-row :gutter="20" class="ranking-row">
|
|
|
- <el-col :span="8">
|
|
|
|
|
|
|
+ <el-col :span="12">
|
|
|
<el-card shadow="hover" class="ranking-card">
|
|
<el-card shadow="hover" class="ranking-card">
|
|
|
<template #header>
|
|
<template #header>
|
|
|
<div class="card-header header-one">
|
|
<div class="card-header header-one">
|
|
@@ -29,6 +29,22 @@
|
|
|
</div>
|
|
</div>
|
|
|
<!-- 控件在一行,均匀分布 -->
|
|
<!-- 控件在一行,均匀分布 -->
|
|
|
<div class="header-controls">
|
|
<div class="header-controls">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="selectedProjectId"
|
|
|
|
|
+ placeholder="选择项目"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ @change="handleProjectChange"
|
|
|
|
|
+ class="project-select"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ style="width: 150px;"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="project in personalProjectOptions"
|
|
|
|
|
+ :key="project.projectId"
|
|
|
|
|
+ :label="project.projectName"
|
|
|
|
|
+ :value="project.projectId"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
<div class="display-count-control">
|
|
<div class="display-count-control">
|
|
|
<span class="control-label">前</span>
|
|
<span class="control-label">前</span>
|
|
|
<el-input-number
|
|
<el-input-number
|
|
@@ -63,31 +79,50 @@
|
|
|
<span class="header-rank">名次</span>
|
|
<span class="header-rank">名次</span>
|
|
|
<span class="header-name">姓名</span>
|
|
<span class="header-name">姓名</span>
|
|
|
<span class="header-team">队伍名称</span>
|
|
<span class="header-team">队伍名称</span>
|
|
|
- <span class="header-score">积分</span>
|
|
|
|
|
|
|
+ <span class="header-project">项目</span>
|
|
|
|
|
+ <span class="header-score">成绩</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-for="(item, index) in displayedAthleteList" :key="index" class="ranking-item">
|
|
<div v-for="(item, index) in displayedAthleteList" :key="index" class="ranking-item">
|
|
|
<div class="item-content">
|
|
<div class="item-content">
|
|
|
<span class="item-rank">{{ getRankDisplay(item, index, athleteScoreList) }}</span>
|
|
<span class="item-rank">{{ getRankDisplay(item, index, athleteScoreList) }}</span>
|
|
|
- <span class="item-name">{{ item.athleteName }}</span>
|
|
|
|
|
|
|
+ <span class="item-name">{{ item.name || item.athleteName }}</span>
|
|
|
<span class="item-team">{{ item.teamName }}</span>
|
|
<span class="item-team">{{ item.teamName }}</span>
|
|
|
- <span class="item-time">{{ item.totalScore }}</span>
|
|
|
|
|
|
|
+ <span class="item-project-name">{{ selectedProjectName }}</span>
|
|
|
|
|
+ <span class="item-score" v-if="item.classification=='0'">{{item.individualPerformance || item.score || item.totalScore }}</span>
|
|
|
|
|
+ <span class="item-score" v-else-if="item.classification=='1'">{{item.teamPerformance || item.score || item.totalScore }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
- <el-col :span="8">
|
|
|
|
|
|
|
+ <el-col :span="12">
|
|
|
<el-card shadow="hover" class="ranking-card">
|
|
<el-card shadow="hover" class="ranking-card">
|
|
|
<template #header>
|
|
<template #header>
|
|
|
<div class="card-header header-three">
|
|
<div class="card-header header-three">
|
|
|
<div class="team-ranking-header">
|
|
<div class="team-ranking-header">
|
|
|
<!-- 标题单独一行,居中对齐 -->
|
|
<!-- 标题单独一行,居中对齐 -->
|
|
|
<div class="header-title">
|
|
<div class="header-title">
|
|
|
- <span>团队积分排行榜</span>
|
|
|
|
|
|
|
+ <span>团队项目排行榜</span>
|
|
|
</div>
|
|
</div>
|
|
|
<!-- 控件在一行,均匀分布 -->
|
|
<!-- 控件在一行,均匀分布 -->
|
|
|
<div class="header-controls">
|
|
<div class="header-controls">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="selectedTeamProjectId"
|
|
|
|
|
+ placeholder="选择项目"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ @change="handleTeamProjectChange"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ class="project-select"
|
|
|
|
|
+ style="width: 150px;"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="project in teamProjectOptions"
|
|
|
|
|
+ :key="project.projectId"
|
|
|
|
|
+ :label="project.projectName"
|
|
|
|
|
+ :value="project.projectId"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
<el-select
|
|
<el-select
|
|
|
v-model="selectedRankGroupId"
|
|
v-model="selectedRankGroupId"
|
|
|
placeholder="选择分组"
|
|
placeholder="选择分组"
|
|
@@ -136,47 +171,20 @@
|
|
|
<div class="ranking-header">
|
|
<div class="ranking-header">
|
|
|
<div class="header-content">
|
|
<div class="header-content">
|
|
|
<span class="header-rank">名次</span>
|
|
<span class="header-rank">名次</span>
|
|
|
|
|
+ <span class="header-project">项目</span>
|
|
|
<span class="header-team-name">队伍名称</span>
|
|
<span class="header-team-name">队伍名称</span>
|
|
|
<span class="header-group">组别</span>
|
|
<span class="header-group">组别</span>
|
|
|
- <span class="header-score">总分</span>
|
|
|
|
|
|
|
+ <span class="header-score">成绩</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-for="(item, index) in displayedTeamList" :key="index" class="ranking-item">
|
|
<div v-for="(item, index) in displayedTeamList" :key="index" class="ranking-item">
|
|
|
<div class="item-content">
|
|
<div class="item-content">
|
|
|
<span class="item-rank">{{ getRankDisplay(item, index, filteredTeamScores) }}</span>
|
|
<span class="item-rank">{{ getRankDisplay(item, index, filteredTeamScores) }}</span>
|
|
|
- <span class="item-team-name">{{ item.teamName }}</span>
|
|
|
|
|
- <span class="item-group">{{ item.rgName }}</span>
|
|
|
|
|
- <span class="item-score">{{ item.score }}分</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-card shadow="hover" class="ranking-card">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="card-header header-two">
|
|
|
|
|
- <span>项目进度</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <div class="progress-info">
|
|
|
|
|
- <p>{{ completedTasks }} / {{ totalTasks }}</p>
|
|
|
|
|
- <el-progress :percentage="progressPercentage" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="ranking-list ranking-list-full">
|
|
|
|
|
- <div v-for="(item, index) in projectProgress" :key="index" class="ranking-item">
|
|
|
|
|
- <div class="item-content">
|
|
|
|
|
- <span class="item-name">{{ item.projectName }}</span>
|
|
|
|
|
- <span class="item-type">{{ getProjectTypeText(item.projectType) }} {{ item.classification === '0' ? '个人' : '团体' }}</span>
|
|
|
|
|
- <span class="item-time">{{ formatTime(item.startTime) }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <!-- 如果有组别信息,显示组别详情 -->
|
|
|
|
|
- <div v-if="item.groups && item.groups.length > 0" class="group-details">
|
|
|
|
|
- <div v-for="group in item.groups" :key="group.groupId" class="group-item">
|
|
|
|
|
- <span class="group-name">{{ group.groupName }}</span>
|
|
|
|
|
- <span class="group-time">{{ formatTimeOnly(group.beginTime) }}</span>
|
|
|
|
|
- <span class="group-status" :class="'status-' + group.status">{{ group.statusText }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <span class="item-project-name">{{ selectedTeamProjectName }}</span>
|
|
|
|
|
+ <span class="item-team-name">{{ item.name || item.teamName }}</span>
|
|
|
|
|
+ <span class="item-group">{{ item.rgName || '-' }}</span>
|
|
|
|
|
+ <span class="item-score" v-if="item.classification=='1'">{{ item.teamPerformance || item.score || item.totalScore }}</span>
|
|
|
|
|
+ <span class="item-score" v-else>{{ item.score || item.totalScore }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -189,7 +197,8 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, onMounted, onUnmounted, getCurrentInstance, toRefs } from 'vue';
|
|
import { ref, computed, onMounted, onUnmounted, getCurrentInstance, toRefs } from 'vue';
|
|
|
import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
|
|
import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
|
|
|
-import { listGameScore, getBonusData } from '@/api/system/gameScore';
|
|
|
|
|
|
|
+import { listGameScore, getBonusData, getProjectScoreData } from '@/api/system/gameScore';
|
|
|
|
|
+import { listGameEventProject } from '@/api/system/gameEventProject';
|
|
|
import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
|
|
import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
|
|
|
import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
|
|
import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
|
|
|
import { useGameEventStore } from '@/store/modules/gameEvent';
|
|
import { useGameEventStore } from '@/store/modules/gameEvent';
|
|
@@ -227,13 +236,30 @@ const progressPercentage = computed(() => totalTasks.value > 0 ? Math.round((com
|
|
|
const projectProgress = ref<ProjectProgressVo[]>([]);
|
|
const projectProgress = ref<ProjectProgressVo[]>([]);
|
|
|
|
|
|
|
|
const teamScores = ref<TeamScore[]>([]);
|
|
const teamScores = ref<TeamScore[]>([]);
|
|
|
-const filteredTeamScores = ref<TeamScore[]>([]); // 用于显示筛选后的队伍积分
|
|
|
|
|
|
|
+const filteredTeamScores = ref<any[]>([]); // 用于显示筛选后的队伍积分
|
|
|
|
|
|
|
|
const ALL_GROUPS_VALUE = 'all';
|
|
const ALL_GROUPS_VALUE = 'all';
|
|
|
// 分组相关数据
|
|
// 分组相关数据
|
|
|
const rankGroupOptions = ref<RankGroupVO[]>([]);
|
|
const rankGroupOptions = ref<RankGroupVO[]>([]);
|
|
|
const selectedRankGroupId = ref<string | number | null>(null);
|
|
const selectedRankGroupId = ref<string | number | null>(null);
|
|
|
|
|
|
|
|
|
|
+const personalProjectOptions = ref<any[]>([]);
|
|
|
|
|
+const selectedProjectId = ref<string | number | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+const selectedProjectName = computed(() => {
|
|
|
|
|
+ const project = personalProjectOptions.value.find(p => p.projectId === selectedProjectId.value);
|
|
|
|
|
+ return project ? project.projectName : '-';
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 团队项目选择相关
|
|
|
|
|
+const teamProjectOptions = ref<any[]>([]);
|
|
|
|
|
+const selectedTeamProjectId = ref<string | number | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+const selectedTeamProjectName = computed(() => {
|
|
|
|
|
+ const project = teamProjectOptions.value.find(p => p.projectId === selectedTeamProjectId.value);
|
|
|
|
|
+ return project ? project.projectName : '-';
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
// 显示数量控制
|
|
// 显示数量控制
|
|
|
const personalDisplayCount = ref(10); // 个人排行榜显示数量,默认10
|
|
const personalDisplayCount = ref(10); // 个人排行榜显示数量,默认10
|
|
|
const teamDisplayCount = ref(10); // 团队排行榜显示数量,默认10
|
|
const teamDisplayCount = ref(10); // 团队排行榜显示数量,默认10
|
|
@@ -251,13 +277,14 @@ const countdownInterval = ref<NodeJS.Timeout | null>(null); // 倒计时定时
|
|
|
|
|
|
|
|
// 启动倒计时
|
|
// 启动倒计时
|
|
|
const startCountdown = () => {
|
|
const startCountdown = () => {
|
|
|
- // stopCountdown();
|
|
|
|
|
|
|
+ stopCountdown();
|
|
|
countdownSeconds.value = autoRefreshSeconds.value;
|
|
countdownSeconds.value = autoRefreshSeconds.value;
|
|
|
|
|
|
|
|
countdownInterval.value = setInterval(() => {
|
|
countdownInterval.value = setInterval(() => {
|
|
|
countdownSeconds.value--;
|
|
countdownSeconds.value--;
|
|
|
|
|
|
|
|
if (countdownSeconds.value <= 0) {
|
|
if (countdownSeconds.value <= 0) {
|
|
|
|
|
+ refreshAllData();
|
|
|
countdownSeconds.value = autoRefreshSeconds.value;
|
|
countdownSeconds.value = autoRefreshSeconds.value;
|
|
|
}
|
|
}
|
|
|
}, 1000);
|
|
}, 1000);
|
|
@@ -291,10 +318,15 @@ const handleTeamCountChange = (value: number) => {
|
|
|
teamDisplayCount.value = value;
|
|
teamDisplayCount.value = value;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// 获取队伍积分排行榜
|
|
|
|
|
-const loadTeamScores = async () => {
|
|
|
|
|
- const teamScoreRes = await getBonusData({
|
|
|
|
|
|
|
+// 获取团队项目排行榜数据
|
|
|
|
|
+const loadTeamProjectScores = async () => {
|
|
|
|
|
+ if (!selectedTeamProjectId.value) return;
|
|
|
|
|
+ const res = await getProjectScoreData({
|
|
|
eventId: defaultEventInfo.value.eventId,
|
|
eventId: defaultEventInfo.value.eventId,
|
|
|
|
|
+ projectId: selectedTeamProjectId.value,
|
|
|
|
|
+ classification: '1',
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 1000 // 获取全量以便筛选
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 创建分组ID到分组名称的映射
|
|
// 创建分组ID到分组名称的映射
|
|
@@ -303,20 +335,21 @@ const loadTeamScores = async () => {
|
|
|
groupMap.set(group.rgId, group.rgName);
|
|
groupMap.set(group.rgId, group.rgName);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // // 将队伍信息和积分结合
|
|
|
|
|
- const teamScoreList = teamScoreRes.data.rows.map(teamScore => {
|
|
|
|
|
|
|
+ const teamScoreList = (res.rows || []).map(item => {
|
|
|
return {
|
|
return {
|
|
|
- teamId: teamScore.teamId,
|
|
|
|
|
- teamName: teamScore.teamName,
|
|
|
|
|
- score: teamScore.totalScore,
|
|
|
|
|
- rank: teamScore.rank,
|
|
|
|
|
- rgId: teamScore.rgId,
|
|
|
|
|
- rgName: groupMap.get(teamScore.rgId) || '-' // 添加分组名称
|
|
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ classification: '1',
|
|
|
|
|
+ rgName: groupMap.get(item.rgId) || item.rgName || '-'
|
|
|
}
|
|
}
|
|
|
- })
|
|
|
|
|
- // // 默认显示所有队伍
|
|
|
|
|
- filteredTeamScores.value = teamScoreList;
|
|
|
|
|
- teamScores.value = teamScoreList;
|
|
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前项目的排序方式
|
|
|
|
|
+ const currentProject = teamProjectOptions.value.find(p => p.projectId == selectedTeamProjectId.value);
|
|
|
|
|
+ const orderType = currentProject ? currentProject.orderType : '1'; // 默认降序
|
|
|
|
|
+
|
|
|
|
|
+ teamScores.value = sortAndRankList(teamScoreList, orderType);
|
|
|
|
|
+ // 应用分组筛选
|
|
|
|
|
+ handleRankGroupChange(selectedRankGroupId.value);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 处理分组筛选变化
|
|
// 处理分组筛选变化
|
|
@@ -329,21 +362,9 @@ const handleRankGroupChange = (rgId: string | number | null | undefined) => {
|
|
|
filteredTeamScores.value = teamScores.value.filter(team => team.rgId === rgId);
|
|
filteredTeamScores.value = teamScores.value.filter(team => team.rgId === rgId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 重新计算排名
|
|
|
|
|
- let currentRank = 1;
|
|
|
|
|
- for (let i = 0; i < filteredTeamScores.value.length; i++) {
|
|
|
|
|
- const team = filteredTeamScores.value[i];
|
|
|
|
|
- const currentScore = team.score || 0;
|
|
|
|
|
-
|
|
|
|
|
- if (i > 0) {
|
|
|
|
|
- const previousScore = filteredTeamScores.value[i - 1].score || 0;
|
|
|
|
|
- if (currentScore !== previousScore) {
|
|
|
|
|
- currentRank = i + 1;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- team.rank = currentRank;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 重新计算排名(基于已有的 rank 字段进行筛选显示,不重新计算,因为数据已经是有序的了)
|
|
|
|
|
+ // 只是同步到 filteredTeamScores 供前端展示
|
|
|
|
|
+ // 注意:如果是从 teamScores 中过滤出来的,它们已经带有了全局排名信息
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 获取分组选项
|
|
// 获取分组选项
|
|
@@ -363,6 +384,46 @@ const loadRankGroupOptions = async () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// 获取项目选项(个人和团队)
|
|
|
|
|
+const loadProjectOptions = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await listGameEventProject({
|
|
|
|
|
+ eventId: defaultEventInfo.value.eventId,
|
|
|
|
|
+ orderByColumn: 'createTime',
|
|
|
|
|
+ isAsc: 'desc',
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 1000
|
|
|
|
|
+ });
|
|
|
|
|
+ const allProjects = res.rows || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 个人项目过滤
|
|
|
|
|
+ personalProjectOptions.value = allProjects.filter(p => p.classification === '0');
|
|
|
|
|
+ if (personalProjectOptions.value.length > 0 && !selectedProjectId.value) {
|
|
|
|
|
+ selectedProjectId.value = personalProjectOptions.value[0].projectId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 团队项目过滤
|
|
|
|
|
+ teamProjectOptions.value = allProjects.filter(p => p.classification === '1');
|
|
|
|
|
+ if (teamProjectOptions.value.length > 0 && !selectedTeamProjectId.value) {
|
|
|
|
|
+ selectedTeamProjectId.value = teamProjectOptions.value[0].projectId;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取项目列表失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 处理项目切换
|
|
|
|
|
+const handleProjectChange = (projectId: string | number) => {
|
|
|
|
|
+ selectedProjectId.value = projectId;
|
|
|
|
|
+ refreshPersonalRanking();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 处理团队项目切换
|
|
|
|
|
+const handleTeamProjectChange = (projectId: string | number) => {
|
|
|
|
|
+ selectedTeamProjectId.value = projectId;
|
|
|
|
|
+ refreshTeamRanking();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
// 加载项目进度信息
|
|
// 加载项目进度信息
|
|
|
const loadProjectProgress = async () => {
|
|
const loadProjectProgress = async () => {
|
|
|
try {
|
|
try {
|
|
@@ -421,90 +482,123 @@ const loadProjectProgress = async () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// 获取项目类型文本
|
|
|
|
|
-const getProjectTypeText = (projectType: string) => {
|
|
|
|
|
- if (!game_project_type.value || !projectType) return '未知';
|
|
|
|
|
|
|
+// 获取成绩的数值表示(用于排序)
|
|
|
|
|
+const getNumericValue = (val: string | number | null | undefined) => {
|
|
|
|
|
+ if (val === null || val === undefined || val === '' || val === '0' || val === '00:00:00.000') return 0;
|
|
|
|
|
+ if (typeof val === 'number') return val;
|
|
|
|
|
|
|
|
- const typeItem = game_project_type.value.find((item: any) => item.value === projectType);
|
|
|
|
|
- return typeItem ? typeItem.label : '未知';
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 格式化时间显示(包含日期)
|
|
|
|
|
-const formatTime = (timeStr: string) => {
|
|
|
|
|
- if (!timeStr) return '-';
|
|
|
|
|
- try {
|
|
|
|
|
- const date = new Date(timeStr);
|
|
|
|
|
- // 检查是否是有效日期
|
|
|
|
|
- if (isNaN(date.getTime())) {
|
|
|
|
|
- return timeStr;
|
|
|
|
|
|
|
+ const valStr = String(val);
|
|
|
|
|
+ // 处理计时格式 00:00:00.000
|
|
|
|
|
+ if (valStr.includes(':')) {
|
|
|
|
|
+ const parts = valStr.split(':');
|
|
|
|
|
+ if (parts.length === 3) {
|
|
|
|
|
+ const hours = parseInt(parts[0]) || 0;
|
|
|
|
|
+ const minutes = parseInt(parts[1]) || 0;
|
|
|
|
|
+ const secondsWithMs = parts[2];
|
|
|
|
|
+ let seconds = 0;
|
|
|
|
|
+ let ms = 0;
|
|
|
|
|
+ if (secondsWithMs.includes('.')) {
|
|
|
|
|
+ const secondsParts = secondsWithMs.split('.');
|
|
|
|
|
+ seconds = parseInt(secondsParts[0]) || 0;
|
|
|
|
|
+ ms = parseInt(secondsParts[1]) || 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ seconds = parseInt(secondsWithMs) || 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return hours * 3600000 + minutes * 60000 + seconds * 1000 + ms;
|
|
|
}
|
|
}
|
|
|
- return date.toLocaleString('zh-CN', {
|
|
|
|
|
- month: '2-digit',
|
|
|
|
|
- day: '2-digit',
|
|
|
|
|
- hour: '2-digit',
|
|
|
|
|
- minute: '2-digit',
|
|
|
|
|
- hour12: false
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- return timeStr;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const parsed = parseFloat(valStr);
|
|
|
|
|
+ return isNaN(parsed) ? 0 : parsed;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// 格式化时间显示(仅时间,用于组别详情)
|
|
|
|
|
-const formatTimeOnly = (timeStr: string) => {
|
|
|
|
|
- if (!timeStr) return '-';
|
|
|
|
|
- try {
|
|
|
|
|
- const date = new Date(timeStr);
|
|
|
|
|
- // 检查是否是有效日期
|
|
|
|
|
- if (isNaN(date.getTime())) {
|
|
|
|
|
- return timeStr;
|
|
|
|
|
|
|
+// 前端排序与排名处理
|
|
|
|
|
+const sortAndRankList = (list: any[], orderType: string) => {
|
|
|
|
|
+ if (!list || list.length === 0) return [];
|
|
|
|
|
+
|
|
|
|
|
+ // 成绩取值助手函数
|
|
|
|
|
+ const getScoreValue = (item: any) => {
|
|
|
|
|
+ if (item.classification == '0') {
|
|
|
|
|
+ return item.individualPerformance ?? item.score ?? item.totalScore;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return item.teamPerformance ?? item.score ?? item.totalScore;
|
|
|
}
|
|
}
|
|
|
- return date.toLocaleTimeString('zh-CN', {
|
|
|
|
|
- hour: '2-digit',
|
|
|
|
|
- minute: '2-digit',
|
|
|
|
|
- hour12: false
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- return timeStr;
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
-// 获取排名显示文本(支持并列排名,排名数值连续)
|
|
|
|
|
-const getRankDisplay = (item: any, index: number, list: any[]) => {
|
|
|
|
|
- // 如果项目有rank字段(如团队排名),直接使用
|
|
|
|
|
- if (item.rank !== undefined) {
|
|
|
|
|
- return `第${item.rank}名`;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 1. 排序逻辑
|
|
|
|
|
+ const sorted = [...list].sort((a, b) => {
|
|
|
|
|
+ const valA = getScoreValue(a);
|
|
|
|
|
+ const valB = getScoreValue(b);
|
|
|
|
|
+
|
|
|
|
|
+ const scoreA = getNumericValue(valA);
|
|
|
|
|
+ const scoreB = getNumericValue(valB);
|
|
|
|
|
+
|
|
|
|
|
+ // 0 值(未参赛/无效成绩)永远排在最后
|
|
|
|
|
+ if (scoreA === 0 && scoreB === 0) return 0;
|
|
|
|
|
+ if (scoreA === 0) return 1;
|
|
|
|
|
+ if (scoreB === 0) return -1;
|
|
|
|
|
+
|
|
|
|
|
+ if (orderType == '0') { // 升序(计时类,用时越短越好)
|
|
|
|
|
+ return scoreA - scoreB;
|
|
|
|
|
+ } else { // 降序(远度/积分,分值越高越好)
|
|
|
|
|
+ return scoreB - scoreA;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // 个人排名逻辑(排名数值连续)
|
|
|
|
|
- // 计算当前项目的实际排名
|
|
|
|
|
- // 排名 = 比当前积分高的不同积分数量 + 1
|
|
|
|
|
- const currentScore = item.totalScore || 0;
|
|
|
|
|
- const higherScores = new Set();
|
|
|
|
|
|
|
+ // 2. 计算排名(支持并列)
|
|
|
|
|
+ let currentRank = 0;
|
|
|
|
|
+ let lastScoreValue = -1;
|
|
|
|
|
|
|
|
- for (let i = 0; i < list.length; i++) {
|
|
|
|
|
- if (list[i].totalScore > currentScore) {
|
|
|
|
|
- higherScores.add(list[i].totalScore);
|
|
|
|
|
|
|
+ return sorted.map((item, index) => {
|
|
|
|
|
+ const val = getScoreValue(item);
|
|
|
|
|
+ const scoreValue = getNumericValue(val);
|
|
|
|
|
+
|
|
|
|
|
+ if (scoreValue === 0) {
|
|
|
|
|
+ item.rank = '-'; // 无效成绩不计排名
|
|
|
|
|
+ } else if (scoreValue !== lastScoreValue) {
|
|
|
|
|
+ currentRank = index + 1;
|
|
|
|
|
+ lastScoreValue = scoreValue;
|
|
|
|
|
+ item.rank = currentRank;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ item.rank = currentRank;
|
|
|
}
|
|
}
|
|
|
|
|
+ return item;
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取排名显示文本
|
|
|
|
|
+const getRankDisplay = (item: any, index: number, list: any[]) => {
|
|
|
|
|
+ if (item.rank === '-' || item.rank === undefined) {
|
|
|
|
|
+ return '未入榜';
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- const actualRank = higherScores.size + 1;
|
|
|
|
|
- return `第${actualRank}名`;
|
|
|
|
|
|
|
+ return `第${item.rank}名`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 刷新个人排行榜数据
|
|
// 刷新个人排行榜数据
|
|
|
const refreshPersonalRanking = async () => {
|
|
const refreshPersonalRanking = async () => {
|
|
|
|
|
+ if (!selectedProjectId.value) return;
|
|
|
personalRefreshing.value = true;
|
|
personalRefreshing.value = true;
|
|
|
try {
|
|
try {
|
|
|
- const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
|
|
|
|
|
- // 按照totalScore字段降序排序(分数高的在前面)
|
|
|
|
|
- athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
|
|
|
|
|
|
|
+ const res = await getProjectScoreData({
|
|
|
|
|
+ eventId: defaultEventInfo.value.eventId,
|
|
|
|
|
+ projectId: selectedProjectId.value,
|
|
|
|
|
+ classification: '0',
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: personalDisplayCount.value
|
|
|
|
|
+ });
|
|
|
|
|
+ // 接口返回的是分页数据,取 rows
|
|
|
|
|
+ const rows = (res.rows || []).map(item => ({ ...item, classification: '0' }));
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前项目的排序方式
|
|
|
|
|
+ const currentProject = personalProjectOptions.value.find(p => p.projectId == selectedProjectId.value);
|
|
|
|
|
+ const orderType = currentProject ? currentProject.orderType : '1'; // 默认降序
|
|
|
|
|
+
|
|
|
|
|
+ athleteScoreList.value = sortAndRankList(rows, orderType);
|
|
|
// 手动刷新时重置倒计时
|
|
// 手动刷新时重置倒计时
|
|
|
startCountdown();
|
|
startCountdown();
|
|
|
// 显示成功消息
|
|
// 显示成功消息
|
|
|
ElMessage.success('个人排行榜数据已刷新');
|
|
ElMessage.success('个人排行榜数据已刷新');
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('刷新个人排行榜失败:', error);
|
|
|
|
|
ElMessage.error('刷新个人排行榜失败');
|
|
ElMessage.error('刷新个人排行榜失败');
|
|
|
} finally {
|
|
} finally {
|
|
|
personalRefreshing.value = false;
|
|
personalRefreshing.value = false;
|
|
@@ -515,8 +609,8 @@ const refreshPersonalRanking = async () => {
|
|
|
const refreshTeamRanking = async () => {
|
|
const refreshTeamRanking = async () => {
|
|
|
teamRefreshing.value = true;
|
|
teamRefreshing.value = true;
|
|
|
try {
|
|
try {
|
|
|
- // 重新加载队伍积分排行榜
|
|
|
|
|
- await loadTeamScores();
|
|
|
|
|
|
|
+ // 重新加载团队项目排行榜
|
|
|
|
|
+ await loadTeamProjectScores();
|
|
|
// 手动刷新时重置倒计时
|
|
// 手动刷新时重置倒计时
|
|
|
startCountdown();
|
|
startCountdown();
|
|
|
|
|
|
|
@@ -539,12 +633,10 @@ const refreshAllData = async () => {
|
|
|
// 并行刷新所有数据
|
|
// 并行刷新所有数据
|
|
|
await Promise.all([
|
|
await Promise.all([
|
|
|
refreshPersonalRanking(),
|
|
refreshPersonalRanking(),
|
|
|
- refreshTeamRanking(),
|
|
|
|
|
- loadProjectProgress()
|
|
|
|
|
|
|
+ refreshTeamRanking()
|
|
|
]);
|
|
]);
|
|
|
- ElMessage.success('所有数据已刷新');
|
|
|
|
|
|
|
+ // ElMessage.success('所有数据已刷新');
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('刷新数据失败:', error);
|
|
|
|
|
ElMessage.error('刷新数据失败');
|
|
ElMessage.error('刷新数据失败');
|
|
|
} finally {
|
|
} finally {
|
|
|
personalRefreshing.value = false;
|
|
personalRefreshing.value = false;
|
|
@@ -554,16 +646,8 @@ const refreshAllData = async () => {
|
|
|
|
|
|
|
|
// 开启自动刷新
|
|
// 开启自动刷新
|
|
|
const startAutoRefresh = () => {
|
|
const startAutoRefresh = () => {
|
|
|
- if (autoRefreshInterval.value) {
|
|
|
|
|
- clearInterval(autoRefreshInterval.value);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- autoRefreshInterval.value = setInterval(() => {
|
|
|
|
|
- // refreshAllData();
|
|
|
|
|
- }, autoRefreshSeconds.value * 1000);
|
|
|
|
|
- // 开启倒计时
|
|
|
|
|
|
|
+ // 开启倒计时,倒计时结束会自动触发刷新
|
|
|
startCountdown();
|
|
startCountdown();
|
|
|
- ElMessage.success(`已开启自动刷新,每${autoRefreshSeconds.value}秒刷新一次`);
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 组件卸载时清理定时器
|
|
// 组件卸载时清理定时器
|
|
@@ -579,18 +663,21 @@ onMounted(async () => {
|
|
|
try{
|
|
try{
|
|
|
startAutoRefresh();
|
|
startAutoRefresh();
|
|
|
await gameEventStore.fetchDefaultEvent();
|
|
await gameEventStore.fetchDefaultEvent();
|
|
|
- const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
|
|
|
|
|
- // 按照totalScore字段降序排序(分数高的在前面)
|
|
|
|
|
- athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
|
|
|
|
|
// 并行加载其他数据
|
|
// 并行加载其他数据
|
|
|
await Promise.all([
|
|
await Promise.all([
|
|
|
- // 加载队伍积分排行榜
|
|
|
|
|
- loadTeamScores(),
|
|
|
|
|
|
|
+ // 加载所有项目选项并进行前端分类
|
|
|
|
|
+ loadProjectOptions(),
|
|
|
// 加载分组选项
|
|
// 加载分组选项
|
|
|
loadRankGroupOptions(),
|
|
loadRankGroupOptions(),
|
|
|
- // 加载项目进度信息
|
|
|
|
|
- loadProjectProgress()
|
|
|
|
|
]);
|
|
]);
|
|
|
|
|
+
|
|
|
|
|
+ // 在获取到项目列表并设置默认项目后,加载对应项目的排名
|
|
|
|
|
+ if (selectedProjectId.value || selectedTeamProjectId.value) {
|
|
|
|
|
+ await Promise.all([
|
|
|
|
|
+ refreshPersonalRanking(),
|
|
|
|
|
+ refreshTeamRanking()
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载数据失败:', error);
|
|
console.error('加载数据失败:', error);
|
|
|
}
|
|
}
|
|
@@ -764,6 +851,12 @@ onMounted(async () => {
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.header-project {
|
|
|
|
|
+ flex: 1.5;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.header-name {
|
|
.header-name {
|
|
|
flex: 2;
|
|
flex: 2;
|
|
|
text-align: left;
|
|
text-align: left;
|
|
@@ -803,6 +896,11 @@ onMounted(async () => {
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.item-project-name {
|
|
|
|
|
+ flex: 1.5;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* 团队积分排行榜数据样式 */
|
|
/* 团队积分排行榜数据样式 */
|
|
|
.item-team-name {
|
|
.item-team-name {
|
|
|
flex: 2;
|
|
flex: 2;
|
|
@@ -871,7 +969,7 @@ onMounted(async () => {
|
|
|
/* 控件区域样式 */
|
|
/* 控件区域样式 */
|
|
|
.header-controls {
|
|
.header-controls {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
|
|
|
|
+ justify-content: center;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 15px;
|
|
gap: 15px;
|
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|