|
|
@@ -107,8 +107,10 @@
|
|
|
<span class="item-name">{{ item.name || item.athleteName }}</span>
|
|
|
<span class="item-team">{{ item.teamName }}</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>
|
|
|
+ <span class="item-score">
|
|
|
+ {{ item.score || '-' }}
|
|
|
+ <span v-if="item.score && item.score !== '-' && item.score !== '无成绩'" class="score-unit">{{ personalUnit }}</span>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -220,8 +222,10 @@
|
|
|
<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>
|
|
|
+ <span class="item-score">
|
|
|
+ {{ item.score || '-' }}
|
|
|
+ <span v-if="item.score && item.score !== '-' && item.score !== '无成绩'" class="score-unit">{{ teamUnit }}</span>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -233,8 +237,7 @@
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, onMounted, onUnmounted, getCurrentInstance, toRefs } from 'vue';
|
|
|
-import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
|
|
|
-import { listGameScore, getBonusData, getProjectScoreData } from '@/api/system/gameScore';
|
|
|
+import { getProjectScoreData, getRankingBoardData, updateBonusData, exportBonusExcel } from '@/api/system/gameScore';
|
|
|
import { listGameEventProject } from '@/api/system/gameEventProject';
|
|
|
import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
|
|
|
import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
|
|
|
@@ -276,6 +279,33 @@ const projectProgress = ref<ProjectProgressVo[]>([]);
|
|
|
const teamScores = ref<TeamScore[]>([]);
|
|
|
const filteredTeamScores = ref<any[]>([]); // 用于显示筛选后的队伍积分
|
|
|
|
|
|
+// 获取当前项目的单位
|
|
|
+const getProjectUnit = (project: any) => {
|
|
|
+ if (!project) return '';
|
|
|
+ if (project.countUnit) return project.countUnit;
|
|
|
+
|
|
|
+ // 自动兜底逻辑
|
|
|
+ const rule = String(project.scoreRule);
|
|
|
+ if (rule === '1') return '秒'; // 计时类
|
|
|
+ if (rule === '2' || rule === '3' || rule === '6') return '米'; // 距离/远度/高度类
|
|
|
+
|
|
|
+ // 备选方案:按项目名称关键字识别
|
|
|
+ if (project.projectName?.includes('计时')) return '秒';
|
|
|
+ if (project.projectName?.includes('米') || project.projectName?.includes('远') || project.projectName?.includes('高')) return '米';
|
|
|
+
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+const personalUnit = computed(() => {
|
|
|
+ const project = personalProjectOptions.value.find(p => p.projectId == selectedProjectId.value);
|
|
|
+ return getProjectUnit(project);
|
|
|
+});
|
|
|
+
|
|
|
+const teamUnit = computed(() => {
|
|
|
+ const project = teamProjectOptions.value.find(p => p.projectId == selectedTeamProjectId.value);
|
|
|
+ return getProjectUnit(project);
|
|
|
+});
|
|
|
+
|
|
|
const ALL_GROUPS_VALUE = 'all';
|
|
|
// 分组相关数据
|
|
|
const rankGroupOptions = ref<RankGroupVO[]>([]);
|
|
|
@@ -359,51 +389,29 @@ const handleTeamCountChange = (value: number) => {
|
|
|
// 获取团队项目排行榜数据
|
|
|
const loadTeamProjectScores = async () => {
|
|
|
if (!selectedTeamProjectId.value) return;
|
|
|
- const res = await getProjectScoreData({
|
|
|
+ const res = await getRankingBoardData({
|
|
|
eventId: defaultEventInfo.value.eventId,
|
|
|
projectId: selectedTeamProjectId.value,
|
|
|
classification: '1',
|
|
|
- pageNum: 1,
|
|
|
- pageSize: 1000 // 获取全量以便筛选
|
|
|
+ rgId: selectedRankGroupId.value || undefined
|
|
|
});
|
|
|
|
|
|
- // 创建分组ID到分组名称的映射
|
|
|
- const groupMap = new Map();
|
|
|
- rankGroupOptions.value.forEach(group => {
|
|
|
- groupMap.set(group.rgId, group.rgName);
|
|
|
- });
|
|
|
-
|
|
|
- const teamScoreList = (res.rows || []).map(item => {
|
|
|
- return {
|
|
|
- ...item,
|
|
|
- classification: '1',
|
|
|
- rgName: groupMap.get(item.rgId) || item.rgName || '-'
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 获取当前项目的排序方式
|
|
|
- const currentProject = teamProjectOptions.value.find(p => p.projectId == selectedTeamProjectId.value);
|
|
|
- const orderType = currentProject ? currentProject.orderType : '1'; // 默认降序
|
|
|
+ // 直接使用后端返回的有序且带排名的列表
|
|
|
+ const teamScoreList = res.data || [];
|
|
|
|
|
|
- teamScores.value = sortAndRankList(teamScoreList, orderType);
|
|
|
- // 应用分组筛选
|
|
|
- handleRankGroupChange(selectedRankGroupId.value);
|
|
|
+ teamScores.value = teamScoreList.map(item => ({
|
|
|
+ ...item,
|
|
|
+ classification: '1'
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 更新过滤后的显示列表(由于后端已按分组过滤,这里直接同步)
|
|
|
+ filteredTeamScores.value = teamScores.value;
|
|
|
};
|
|
|
|
|
|
// 处理分组筛选变化
|
|
|
const handleRankGroupChange = (rgId: string | number | null | undefined) => {
|
|
|
- if (rgId === null || rgId === undefined || rgId === '' || rgId === ALL_GROUPS_VALUE) {
|
|
|
- // 显示所有队伍
|
|
|
- filteredTeamScores.value = teamScores.value;
|
|
|
- } else {
|
|
|
- // 筛选所属分组的队伍(现在后端已处理父子关系,但此处是前端过滤已加载的数据)
|
|
|
- // 注意:如果看板是全量数据在前端筛选,前端依然需要知道哪些子分组属于该父分组
|
|
|
- // 或者更简单的做法:看板数据在获取时就根据 rgId 传参给后端。
|
|
|
-
|
|
|
- // 鉴于看板通常是全量计算好后前端切分,我们这里保留一个简单的打平用于辅助过滤
|
|
|
- const targetIds = getFlatIds(rgId);
|
|
|
- filteredTeamScores.value = teamScores.value.filter(team => targetIds.includes(team.rgId));
|
|
|
- }
|
|
|
+ // 分组筛选直接触发后端查询,不再在前端过滤
|
|
|
+ loadTeamProjectScores();
|
|
|
};
|
|
|
|
|
|
// 从扁平列表中获取某个节点下的所有子孙ID(用于前端筛选)
|
|
|
@@ -530,89 +538,6 @@ const loadProjectProgress = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 获取成绩的数值表示(用于排序)
|
|
|
-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 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;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const parsed = parseFloat(valStr);
|
|
|
- return isNaN(parsed) ? 0 : parsed;
|
|
|
-};
|
|
|
-
|
|
|
-// 前端排序与排名处理
|
|
|
-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;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 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;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 2. 计算排名(支持并列)
|
|
|
- let currentRank = 0;
|
|
|
- let lastScoreValue = -1;
|
|
|
-
|
|
|
- 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[]) => {
|
|
|
@@ -627,21 +552,15 @@ const refreshPersonalRanking = async () => {
|
|
|
if (!selectedProjectId.value) return;
|
|
|
personalRefreshing.value = true;
|
|
|
try {
|
|
|
- const res = await getProjectScoreData({
|
|
|
+ const res = await getRankingBoardData({
|
|
|
eventId: defaultEventInfo.value.eventId,
|
|
|
projectId: selectedProjectId.value,
|
|
|
- classification: '0',
|
|
|
- pageNum: 1,
|
|
|
- pageSize: personalDisplayCount.value
|
|
|
+ classification: '0'
|
|
|
});
|
|
|
- // 接口返回的是分页数据,取 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 = (res.data || []).map(item => ({ ...item, classification: '0' }));
|
|
|
|
|
|
- athleteScoreList.value = sortAndRankList(rows, orderType);
|
|
|
// 手动刷新时重置倒计时
|
|
|
startCountdown();
|
|
|
// 显示成功消息
|
|
|
@@ -909,7 +828,7 @@ onUnmounted(() => {
|
|
|
onMounted(async () => {
|
|
|
try{
|
|
|
startAutoRefresh();
|
|
|
- await gameEventStore.fetchDefaultEvent();
|
|
|
+ // await gameEventStore.fetchDefaultEvent();
|
|
|
// 并行加载其他数据
|
|
|
await Promise.all([
|
|
|
// 加载所有项目选项并进行前端分类
|
|
|
@@ -1205,6 +1124,21 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
/* 标题样式 */
|
|
|
+.item-score {
|
|
|
+ width: 120px;
|
|
|
+ /* text-align: right; */
|
|
|
+ font-weight: bold;
|
|
|
+ color: green;
|
|
|
+ font-family: 'Digital-7', 'Courier New', Courier, monospace;
|
|
|
+}
|
|
|
+
|
|
|
+.score-unit {
|
|
|
+ font-size: 0.8em;
|
|
|
+ /* margin-left: 4px; */
|
|
|
+ color: green;
|
|
|
+ font-family: 'Inter', sans-serif;
|
|
|
+}
|
|
|
+
|
|
|
.header-title {
|
|
|
text-align: center;
|
|
|
font-weight: bold;
|