RankingBoardPage.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <template>
  2. <div class="ranking-board-page">
  3. <!-- 添加赛事名称标题 -->
  4. <div class="event-title">
  5. <h2>管网通天下 逐梦新征程详情</h2>
  6. <p>随时掌握运动会情况</p>
  7. </div>
  8. <el-row :gutter="20" class="ranking-row">
  9. <el-col :span="8">
  10. <el-card shadow="hover" class="ranking-card">
  11. <template #header>
  12. <div class="card-header header-one">
  13. <span>个人积分排行榜</span>
  14. </div>
  15. </template>
  16. <div class="ranking-list ranking-list-full">
  17. <div v-for="(item, index) in athleteScoreList" :key="index" class="ranking-item">
  18. <div class="item-content">
  19. <span class="item-rank">{{ getRankDisplay(item, index, athleteScoreList) }}</span>
  20. <span class="item-name">{{ item.athleteName }}</span>
  21. <span class="item-team">{{ item.teamName }}</span>
  22. <span class="item-time">{{ item.totalScore }}</span>
  23. </div>
  24. </div>
  25. </div>
  26. </el-card>
  27. </el-col>
  28. <el-col :span="8">
  29. <el-card shadow="hover" class="ranking-card">
  30. <template #header>
  31. <div class="card-header header-three">
  32. <span>团队积分排行榜</span>
  33. </div>
  34. </template>
  35. <div class="ranking-list ranking-list-full">
  36. <div v-for="(item, index) in teamScores" :key="index" class="ranking-item">
  37. <div class="item-content">
  38. <span class="item-rank">{{ getRankDisplay(item, index, teamScores) }}</span>
  39. <span class="item-team-name">{{ item.teamName }}</span>
  40. <span class="item-score">{{ item.score }}分</span>
  41. </div>
  42. </div>
  43. </div>
  44. </el-card>
  45. </el-col>
  46. <el-col :span="8">
  47. <el-card shadow="hover" class="ranking-card">
  48. <template #header>
  49. <div class="card-header header-two">
  50. <span>项目进度</span>
  51. </div>
  52. </template>
  53. <div class="progress-info">
  54. <p>{{ completedTasks }} / {{ totalTasks }}</p>
  55. <el-progress :percentage="progressPercentage" />
  56. </div>
  57. <div class="ranking-list ranking-list-full">
  58. <div v-for="(item, index) in projectProgress" :key="index" class="ranking-item">
  59. <div class="item-content">
  60. <span class="item-name">{{ item.projectName }}</span>
  61. <span class="item-type">{{ getProjectTypeText(item.projectType) }} {{ item.classification === '0' ? '个人' : '团体' }}</span>
  62. <span class="item-time">{{ formatTime(item.startTime) }}</span>
  63. </div>
  64. <!-- 如果有组别信息,显示组别详情 -->
  65. <div v-if="item.groups && item.groups.length > 0" class="group-details">
  66. <div v-for="group in item.groups" :key="group.groupId" class="group-item">
  67. <span class="group-name">{{ group.groupName }}</span>
  68. <span class="group-time">{{ formatTimeOnly(group.beginTime) }}</span>
  69. <span class="group-status" :class="'status-' + group.status">{{ group.statusText }}</span>
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. </el-card>
  75. </el-col>
  76. </el-row>
  77. </div>
  78. </template>
  79. <script setup lang="ts">
  80. import { ref, computed, onMounted } from 'vue';
  81. import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
  82. import { listGameScore } from '@/api/system/gameScore';
  83. import { listGameTeam } from '@/api/system/gameTeam';
  84. import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
  85. import { useRouter,useRoute } from 'vue-router';
  86. import { GameTeamVO } from '@/api/system/gameTeam/types';
  87. import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
  88. // 定义队伍积分排行榜的数据结构
  89. interface TeamScore {
  90. teamId: string | number;
  91. teamName: string;
  92. score: number;
  93. rank: number;
  94. }
  95. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  96. const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
  97. const route = useRoute();
  98. const eventId = route.params.eventId as string;
  99. const athleteScoreList = ref([]);
  100. const completedTasks = ref(0);
  101. const totalTasks = ref(0);
  102. const progressPercentage = computed(() => totalTasks.value > 0 ? (completedTasks.value / totalTasks.value) * 100 : 0);
  103. const projectProgress = ref<ProjectProgressVo[]>([]);
  104. const teamScores = ref<TeamScore[]>([]);
  105. // 获取队伍积分排行榜
  106. const loadTeamScores = async () => {
  107. // 获取所有成绩数据
  108. const scoreRes = await listGameScore({
  109. eventId: eventId,
  110. pageNum: 1,
  111. pageSize: 1000,
  112. orderByColumn: '',
  113. isAsc: ''
  114. });
  115. const scores = scoreRes.rows;
  116. // 获取所有队伍信息
  117. const teamRes = await listGameTeam({
  118. eventId: eventId,
  119. pageNum: 1,
  120. pageSize: 1000,
  121. orderByColumn: '',
  122. isAsc: ''
  123. });
  124. const teams = teamRes.rows;
  125. // 计算每个队伍的总积分
  126. const teamScoreMap = new Map();
  127. scores.forEach(score => {
  128. const teamId = score.teamId;
  129. if (teamId) {
  130. if (teamScoreMap.has(teamId)) {
  131. teamScoreMap.set(teamId, teamScoreMap.get(teamId) + score.scorePoint);
  132. } else {
  133. teamScoreMap.set(teamId, score.scorePoint);
  134. }
  135. }
  136. });
  137. // 将队伍信息和积分结合
  138. const teamScoreList = teams.map(team => {
  139. return {
  140. teamId: team.teamId,
  141. teamName: team.teamName,
  142. score: teamScoreMap.get(team.teamId) || 0,
  143. rank: 0 // 占位符,稍后设置
  144. };
  145. });
  146. // 按积分从高到低排序
  147. teamScoreList.sort((a, b) => b.score - a.score);
  148. // 添加排名(支持并列排名)
  149. let currentRank = 1;
  150. for (let i = 0; i < teamScoreList.length; i++) {
  151. const team = teamScoreList[i];
  152. const currentScore = team.score || 0;
  153. // 如果不是第一个,检查是否与前一个积分相同
  154. if (i > 0) {
  155. const previousScore = teamScoreList[i - 1].score || 0;
  156. if (currentScore !== previousScore) {
  157. currentRank = i + 1;
  158. }
  159. }
  160. team.rank = currentRank;
  161. }
  162. teamScores.value = teamScoreList;
  163. };
  164. // 加载项目进度信息
  165. const loadProjectProgress = async () => {
  166. try {
  167. const res = await getProjectProgress(eventId);
  168. projectProgress.value = res.data || [];
  169. // 计算已完成和总任务数
  170. let completed = 0;
  171. let total = 0;
  172. projectProgress.value.forEach(project => {
  173. if (project.groups && project.groups.length > 0) {
  174. // 有组别的项目,统计组别
  175. project.groups.forEach(group => {
  176. total++;
  177. if (group.status === '2') { // 已完成
  178. completed++;
  179. }
  180. });
  181. } else {
  182. // 没有组别的项目,直接统计项目
  183. total++;
  184. if (project.status === '2') { // 已完成
  185. completed++;
  186. }
  187. }
  188. });
  189. completedTasks.value = completed;
  190. totalTasks.value = total;
  191. // 前端也可以做一次排序确保,虽然后端已经排序了
  192. // 按完整时间排序(项目时间或最早组别时间)
  193. projectProgress.value.sort((a, b) => {
  194. const getEarliestTime = (project: ProjectProgressVo) => {
  195. if (project.groups && project.groups.length > 0) {
  196. // 有组别,找到最早的组别时间
  197. const groupTimes = project.groups
  198. .map(g => g.beginTime ? new Date(g.beginTime).getTime() : 0)
  199. .filter(time => time > 0);
  200. if (groupTimes.length > 0) {
  201. return Math.min(...groupTimes);
  202. }
  203. }
  204. // 没有组别或组别时间无效,使用项目时间
  205. return project.startTime ? new Date(project.startTime).getTime() : 0;
  206. };
  207. const aTime = getEarliestTime(a);
  208. const bTime = getEarliestTime(b);
  209. return aTime - bTime;
  210. });
  211. } catch (error) {
  212. console.error('加载项目进度失败:', error);
  213. }
  214. };
  215. // 获取项目类型文本
  216. const getProjectTypeText = (projectType: string) => {
  217. if (!game_project_type.value || !projectType) return '未知';
  218. const typeItem = game_project_type.value.find((item: any) => item.value === projectType);
  219. return typeItem ? typeItem.label : '未知';
  220. };
  221. // 格式化时间显示(包含日期)
  222. const formatTime = (timeStr: string) => {
  223. if (!timeStr) return '-';
  224. try {
  225. const date = new Date(timeStr);
  226. // 检查是否是有效日期
  227. if (isNaN(date.getTime())) {
  228. return timeStr;
  229. }
  230. return date.toLocaleString('zh-CN', {
  231. month: '2-digit',
  232. day: '2-digit',
  233. hour: '2-digit',
  234. minute: '2-digit',
  235. hour12: false
  236. });
  237. } catch (error) {
  238. return timeStr;
  239. }
  240. };
  241. // 格式化时间显示(仅时间,用于组别详情)
  242. const formatTimeOnly = (timeStr: string) => {
  243. if (!timeStr) return '-';
  244. try {
  245. const date = new Date(timeStr);
  246. // 检查是否是有效日期
  247. if (isNaN(date.getTime())) {
  248. return timeStr;
  249. }
  250. return date.toLocaleTimeString('zh-CN', {
  251. hour: '2-digit',
  252. minute: '2-digit',
  253. hour12: false
  254. });
  255. } catch (error) {
  256. return timeStr;
  257. }
  258. };
  259. // 获取排名显示文本(支持并列排名,排名数值连续)
  260. const getRankDisplay = (item: any, index: number, list: any[]) => {
  261. // 如果项目有rank字段(如团队排名),直接使用
  262. if (item.rank !== undefined) {
  263. return `第${item.rank}名`;
  264. }
  265. // 个人排名逻辑(排名数值连续)
  266. // 计算当前项目的实际排名
  267. // 排名 = 比当前积分高的不同积分数量 + 1
  268. const currentScore = item.totalScore || 0;
  269. const higherScores = new Set();
  270. for (let i = 0; i < list.length; i++) {
  271. if (list[i].totalScore > currentScore) {
  272. higherScores.add(list[i].totalScore);
  273. }
  274. }
  275. const actualRank = higherScores.size + 1;
  276. return `第${actualRank}名`;
  277. };
  278. onMounted(async () => {
  279. const res = await listScoreRanking(eventId);
  280. // 按照totalScore字段降序排序(分数高的在前面)
  281. athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
  282. // 加载队伍积分排行榜
  283. await loadTeamScores();
  284. // 加载项目进度信息
  285. await loadProjectProgress();
  286. });
  287. </script>
  288. <style scoped>
  289. .ranking-board-page {
  290. padding: 20px;
  291. height: calc(100vh - 120px);
  292. }
  293. /* 新增赛事名称标题样式 */
  294. .event-title {
  295. margin-bottom: 20px;
  296. text-align: center;
  297. }
  298. .event-title h2 {
  299. font-size: 24px;
  300. margin: 0;
  301. }
  302. .event-title p {
  303. font-size: 16px;
  304. color: #666;
  305. margin: 5px 0 0;
  306. }
  307. .ranking-row {
  308. height: calc(100% - 70px); /* 调整高度以适应新增标题区域 */
  309. }
  310. .ranking-card {
  311. width: 100%;
  312. height: 100%;
  313. display: flex;
  314. flex-direction: column;
  315. }
  316. .card-header {
  317. font-weight: bold;
  318. font-size: 16px;
  319. color: black;
  320. text-align: center;
  321. }
  322. .progress-info {
  323. margin-bottom: 15px;
  324. }
  325. .ranking-list {
  326. max-height: 800px;
  327. overflow-y: auto;
  328. }
  329. .ranking-list-full {
  330. flex: 1;
  331. overflow-y: auto;
  332. }
  333. .ranking-item {
  334. padding: 10px 0;
  335. border-bottom: 1px solid #f0f0f0;
  336. }
  337. .ranking-item:last-child {
  338. border-bottom: none;
  339. }
  340. .item-content {
  341. display: flex;
  342. justify-content: space-between;
  343. }
  344. .item-name {
  345. flex: 2;
  346. text-align: left;
  347. }
  348. .item-team,
  349. .item-time,
  350. .item-type,
  351. .item-rank,
  352. .item-team-name,
  353. .item-score {
  354. flex: 1;
  355. text-align: center;
  356. }
  357. /* 组别详情样式 */
  358. .group-details {
  359. margin-top: 8px;
  360. padding-left: 20px;
  361. border-left: 2px solid #e0e0e0;
  362. }
  363. .group-item {
  364. display: flex;
  365. justify-content: space-between;
  366. align-items: center;
  367. padding: 4px 0;
  368. font-size: 12px;
  369. color: #666;
  370. }
  371. .group-name {
  372. flex: 3;
  373. text-align: left;
  374. }
  375. .group-time {
  376. flex: 2;
  377. text-align: center;
  378. }
  379. .group-status {
  380. flex: 1;
  381. text-align: center;
  382. padding: 2px 6px;
  383. border-radius: 10px;
  384. font-size: 10px;
  385. }
  386. .status-0 {
  387. background-color: #f0f0f0;
  388. color: #999;
  389. }
  390. .status-1 {
  391. background-color: #e6f7ff;
  392. color: #1890ff;
  393. }
  394. .status-2 {
  395. background-color: #f6ffed;
  396. color: #52c41a;
  397. }
  398. </style>