RankingBoardPage.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090
  1. <template>
  2. <div class="ranking-board-page">
  3. <!-- 右上角倒计时显示 -->
  4. <div class="countdown-display">
  5. <!-- <el-card shadow="never" class="countdown-card"> -->
  6. <el-button size="default" type="primary">
  7. <div class="countdown-content">
  8. <!-- <el-icon class="countdown-icon"><Timer /></el-icon> -->
  9. <span>{{ countdownSeconds }}秒后刷新</span>
  10. </div>
  11. </el-button>
  12. <!-- </el-card> -->
  13. </div>
  14. <!-- 赛事名称标题 -->
  15. <div class="event-title">
  16. <h2>{{defaultEventInfo ? defaultEventInfo.eventName : '赛事排行榜'}}</h2>
  17. <p>随时掌握运动会情况</p>
  18. </div>
  19. <el-row :gutter="20" class="ranking-row">
  20. <el-col :span="8">
  21. <el-card shadow="hover" class="ranking-card">
  22. <template #header>
  23. <div class="card-header header-one">
  24. <div class="personal-ranking-header">
  25. <!-- 标题单独一行,居中对齐 -->
  26. <div class="header-title">
  27. <span>个人项目排行榜</span>
  28. </div>
  29. <!-- 控件在一行,均匀分布 -->
  30. <div class="header-controls">
  31. <div class="display-count-control">
  32. <span class="control-label">前</span>
  33. <el-input-number
  34. v-model="personalDisplayCount"
  35. :min="1"
  36. :max="100"
  37. size="small"
  38. controls-position="right"
  39. style="width: 80px; margin: 0 5px;"
  40. @change="handlePersonalCountChange"
  41. />
  42. <span class="control-label">名</span>
  43. </div>
  44. <el-button
  45. type="primary"
  46. size="small"
  47. :icon="Refresh"
  48. :loading="personalRefreshing"
  49. @click="refreshPersonalRanking"
  50. class="refresh-btn"
  51. >
  52. 刷新
  53. </el-button>
  54. </div>
  55. </div>
  56. </div>
  57. </template>
  58. <div class="ranking-list ranking-list-full">
  59. <!-- 添加标题行 -->
  60. <div class="ranking-header">
  61. <div class="header-content">
  62. <span class="header-rank">名次</span>
  63. <span class="header-name">姓名</span>
  64. <span class="header-team">队伍名称</span>
  65. <span class="header-score">积分</span>
  66. </div>
  67. </div>
  68. <div v-for="(item, index) in displayedAthleteList" :key="index" class="ranking-item">
  69. <div class="item-content">
  70. <span class="item-rank">{{ getRankDisplay(item, index, athleteScoreList) }}</span>
  71. <span class="item-name">{{ item.athleteName }}</span>
  72. <span class="item-team">{{ item.teamName }}</span>
  73. <span class="item-time">{{ item.totalScore }}</span>
  74. </div>
  75. </div>
  76. </div>
  77. </el-card>
  78. </el-col>
  79. <el-col :span="8">
  80. <el-card shadow="hover" class="ranking-card">
  81. <template #header>
  82. <div class="card-header header-three">
  83. <div class="team-ranking-header">
  84. <!-- 标题单独一行,居中对齐 -->
  85. <div class="header-title">
  86. <span>团队积分排行榜</span>
  87. </div>
  88. <!-- 控件在一行,均匀分布 -->
  89. <div class="header-controls">
  90. <el-select
  91. v-model="selectedRankGroupId"
  92. placeholder="选择分组"
  93. clearable
  94. size="small"
  95. @change="handleRankGroupChange"
  96. class="group-select"
  97. >
  98. <el-option label="全部队伍" :value="ALL_GROUPS_VALUE"></el-option>
  99. <el-option
  100. v-for="group in rankGroupOptions"
  101. :key="group.rgId"
  102. :label="group.rgName"
  103. :value="group.rgId"
  104. />
  105. </el-select>
  106. <div class="display-count-control">
  107. <span class="control-label">前</span>
  108. <el-input-number
  109. v-model="teamDisplayCount"
  110. :min="1"
  111. :max="100"
  112. size="small"
  113. controls-position="right"
  114. style="width: 80px; margin: 0 5px;"
  115. @change="handleTeamCountChange"
  116. />
  117. <span class="control-label">名</span>
  118. </div>
  119. <el-button
  120. type="primary"
  121. size="small"
  122. :icon="Refresh"
  123. :loading="teamRefreshing"
  124. @click="refreshTeamRanking"
  125. class="refresh-btn"
  126. >
  127. 刷新
  128. </el-button>
  129. </div>
  130. </div>
  131. </div>
  132. </template>
  133. <div class="ranking-list ranking-list-full">
  134. <!-- 添加标题行 -->
  135. <div class="ranking-header">
  136. <div class="header-content">
  137. <span class="header-rank">名次</span>
  138. <span class="header-team-name">队伍名称</span>
  139. <span class="header-group">组别</span>
  140. <span class="header-score">总分</span>
  141. </div>
  142. </div>
  143. <div v-for="(item, index) in displayedTeamList" :key="index" class="ranking-item">
  144. <div class="item-content">
  145. <span class="item-rank">{{ getRankDisplay(item, index, filteredTeamScores) }}</span>
  146. <span class="item-team-name">{{ item.teamName }}</span>
  147. <span class="item-group">{{ item.rgName }}</span>
  148. <span class="item-score">{{ item.score }}分</span>
  149. </div>
  150. </div>
  151. </div>
  152. </el-card>
  153. </el-col>
  154. <el-col :span="8">
  155. <el-card shadow="hover" class="ranking-card">
  156. <template #header>
  157. <div class="card-header header-two">
  158. <span>项目进度</span>
  159. </div>
  160. </template>
  161. <div class="progress-info">
  162. <p>{{ completedTasks }} / {{ totalTasks }}</p>
  163. <el-progress :percentage="progressPercentage" />
  164. </div>
  165. <div class="ranking-list ranking-list-full">
  166. <div v-for="(item, index) in projectProgress" :key="index" class="ranking-item">
  167. <div class="item-content">
  168. <span class="item-name">{{ item.projectName }}</span>
  169. <span class="item-type">{{ getProjectTypeText(item.projectType) }} {{ item.classification === '0' ? '个人' : '团体' }}</span>
  170. <span class="item-time">{{ formatTime(item.startTime) }}</span>
  171. </div>
  172. <!-- 如果有组别信息,显示组别详情 -->
  173. <div v-if="item.groups && item.groups.length > 0" class="group-details">
  174. <div v-for="group in item.groups" :key="group.groupId" class="group-item">
  175. <span class="group-name">{{ group.groupName }}</span>
  176. <span class="group-time">{{ formatTimeOnly(group.beginTime) }}</span>
  177. <span class="group-status" :class="'status-' + group.status">{{ group.statusText }}</span>
  178. </div>
  179. </div>
  180. </div>
  181. </div>
  182. </el-card>
  183. </el-col>
  184. </el-row>
  185. </div>
  186. </template>
  187. 8
  188. <script setup lang="ts">
  189. import { ref, computed, onMounted, onUnmounted, getCurrentInstance, toRefs } from 'vue';
  190. import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
  191. import { listGameScore } from '@/api/system/gameScore';
  192. import { listGameTeam } from '@/api/system/gameTeam';
  193. import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
  194. import { useRouter,useRoute } from 'vue-router';
  195. import { GameTeamVO } from '@/api/system/gameTeam/types';
  196. import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
  197. import { useGameEventStore } from '@/store/modules/gameEvent';
  198. import { storeToRefs } from 'pinia';
  199. import { listRankGroup } from '@/api/system/rankGroup';
  200. import { RankGroupVO } from '@/api/system/rankGroup/types';
  201. import { Refresh } from '@element-plus/icons-vue';
  202. import { Timer, Close } from '@element-plus/icons-vue';
  203. // 定义队伍积分排行榜的数据结构
  204. interface TeamScore {
  205. rgId: string | number;
  206. teamId: string | number;
  207. teamName: string;
  208. score: number;
  209. rank: number;
  210. rgName?: string;
  211. }
  212. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  213. const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
  214. // const route = useRoute();
  215. // const eventId = route.params.eventId as string;
  216. //从pinia中获取默认赛事信息
  217. const gameEventStore = useGameEventStore();
  218. const { defaultEventInfo } = storeToRefs(gameEventStore);
  219. const athleteScoreList = ref([]);
  220. const completedTasks = ref(0);
  221. const totalTasks = ref(0);
  222. const progressPercentage = computed(() => totalTasks.value > 0 ? Math.round((completedTasks.value / totalTasks.value) * 100) : 0);
  223. const projectProgress = ref<ProjectProgressVo[]>([]);
  224. const teamScores = ref<TeamScore[]>([]);
  225. const filteredTeamScores = ref<TeamScore[]>([]); // 用于显示筛选后的队伍积分
  226. const ALL_GROUPS_VALUE = 'all';
  227. // 分组相关数据
  228. const rankGroupOptions = ref<RankGroupVO[]>([]);
  229. const selectedRankGroupId = ref<string | number | null>(null);
  230. // 显示数量控制
  231. const personalDisplayCount = ref(10); // 个人排行榜显示数量,默认10
  232. const teamDisplayCount = ref(10); // 团队排行榜显示数量,默认10
  233. // 添加刷新状态控制
  234. const personalRefreshing = ref(false); // 个人排行榜刷新状态
  235. const teamRefreshing = ref(false); // 团队排行榜刷新状态
  236. // 自动刷新相关
  237. const autoRefreshInterval = ref<NodeJS.Timeout | null>(null);
  238. const autoRefreshSeconds = ref(300); // 默认300秒自动刷新
  239. // 添加倒计时相关状态
  240. const countdownSeconds = ref(0); // 倒计时秒数
  241. const countdownInterval = ref<NodeJS.Timeout | null>(null); // 倒计时定时器
  242. // 启动倒计时
  243. const startCountdown = () => {
  244. // stopCountdown();
  245. countdownSeconds.value = autoRefreshSeconds.value;
  246. countdownInterval.value = setInterval(() => {
  247. countdownSeconds.value--;
  248. if (countdownSeconds.value <= 0) {
  249. countdownSeconds.value = autoRefreshSeconds.value;
  250. }
  251. }, 1000);
  252. };
  253. // 停止倒计时
  254. const stopCountdown = () => {
  255. if (countdownInterval.value) {
  256. clearInterval(countdownInterval.value);
  257. countdownInterval.value = null;
  258. }
  259. countdownSeconds.value = 0;
  260. };
  261. // 根据显示数量过滤数据
  262. const displayedAthleteList = computed(() => {
  263. return athleteScoreList.value.slice(0, personalDisplayCount.value);
  264. });
  265. const displayedTeamList = computed(() => {
  266. return filteredTeamScores.value.slice(0, teamDisplayCount.value);
  267. });
  268. // 处理个人排行榜显示数量变化
  269. const handlePersonalCountChange = (value: number) => {
  270. personalDisplayCount.value = value;
  271. };
  272. // 处理团队排行榜显示数量变化
  273. const handleTeamCountChange = (value: number) => {
  274. teamDisplayCount.value = value;
  275. };
  276. // 获取队伍积分排行榜
  277. const loadTeamScores = async () => {
  278. // 获取所有成绩数据
  279. const scoreRes = await listGameScore({
  280. eventId: defaultEventInfo.value.eventId,
  281. pageNum: 1,
  282. pageSize: 1000,
  283. orderByColumn: '',
  284. isAsc: ''
  285. });
  286. const scores = scoreRes.rows;
  287. // 获取所有队伍信息
  288. const teamRes = await listGameTeam({
  289. eventId: defaultEventInfo.value.eventId,
  290. pageNum: 1,
  291. pageSize: 1000,
  292. orderByColumn: '',
  293. isAsc: ''
  294. });
  295. const teams = teamRes.rows;
  296. // 获取分组信息(如果还没有加载的话)
  297. if (rankGroupOptions.value.length === 0) {
  298. await loadRankGroupOptions();
  299. }
  300. // 创建分组ID到分组名称的映射
  301. const groupMap = new Map();
  302. rankGroupOptions.value.forEach(group => {
  303. groupMap.set(group.rgId, group.rgName);
  304. });
  305. // 计算每个队伍的总积分
  306. const teamScoreMap = new Map();
  307. scores.forEach(score => {
  308. const teamId = score.teamId;
  309. if (teamId) {
  310. if (teamScoreMap.has(teamId)) {
  311. teamScoreMap.set(teamId, teamScoreMap.get(teamId) + score.scorePoint);
  312. } else {
  313. teamScoreMap.set(teamId, score.scorePoint);
  314. }
  315. }
  316. });
  317. // 将队伍信息和积分结合
  318. const teamScoreList = teams.map(team => {
  319. return {
  320. teamId: team.teamId,
  321. teamName: team.teamName,
  322. score: teamScoreMap.get(team.teamId) || 0,
  323. rank: 0, // 占位符,稍后设置
  324. rgId: team.rgId, // 添加分组ID
  325. rgName: groupMap.get(team.rgId) || '-' // 添加分组名称
  326. };
  327. });
  328. // 按积分从高到低排序
  329. teamScoreList.sort((a, b) => b.score - a.score);
  330. // 添加排名(支持并列排名)
  331. let currentRank = 1;
  332. for (let i = 0; i < teamScoreList.length; i++) {
  333. const team = teamScoreList[i];
  334. const currentScore = team.score || 0;
  335. // 如果不是第一个,检查是否与前一个积分相同
  336. if (i > 0) {
  337. const previousScore = teamScoreList[i - 1].score || 0;
  338. if (currentScore !== previousScore) {
  339. currentRank = i + 1;
  340. }
  341. }
  342. team.rank = currentRank;
  343. }
  344. teamScores.value = teamScoreList;
  345. // 默认显示所有队伍
  346. filteredTeamScores.value = teamScoreList;
  347. };
  348. // 处理分组筛选变化
  349. const handleRankGroupChange = (rgId: string | number | null) => {
  350. if (rgId === null || rgId === ALL_GROUPS_VALUE) {
  351. // 显示所有队伍
  352. filteredTeamScores.value = teamScores.value;
  353. } else {
  354. // 筛选指定分组的队伍
  355. filteredTeamScores.value = teamScores.value.filter(team => team.rgId === rgId);
  356. }
  357. // 重新计算排名
  358. let currentRank = 1;
  359. for (let i = 0; i < filteredTeamScores.value.length; i++) {
  360. const team = filteredTeamScores.value[i];
  361. const currentScore = team.score || 0;
  362. if (i > 0) {
  363. const previousScore = filteredTeamScores.value[i - 1].score || 0;
  364. if (currentScore !== previousScore) {
  365. currentRank = i + 1;
  366. }
  367. }
  368. team.rank = currentRank;
  369. }
  370. };
  371. // 获取分组选项
  372. const loadRankGroupOptions = async () => {
  373. try {
  374. const res = await listRankGroup({
  375. eventId: defaultEventInfo.value.eventId,
  376. pageNum: 1,
  377. pageSize: 1000,
  378. orderByColumn: undefined,
  379. isAsc: undefined,
  380. status: '0'
  381. });
  382. rankGroupOptions.value = res.rows;
  383. } catch (error) {
  384. console.error('获取分组列表失败:', error);
  385. }
  386. };
  387. // 加载项目进度信息
  388. const loadProjectProgress = async () => {
  389. try {
  390. const res = await getProjectProgress(defaultEventInfo.value.eventId);
  391. projectProgress.value = res.data || [];
  392. // 计算已完成和总任务数
  393. let completed = 0;
  394. let total = 0;
  395. projectProgress.value.forEach(project => {
  396. if (project.groups && project.groups.length > 0) {
  397. // 有组别的项目,统计组别
  398. project.groups.forEach(group => {
  399. total++;
  400. if (group.status === '2') { // 已完成
  401. completed++;
  402. }
  403. });
  404. } else {
  405. // 没有组别的项目,直接统计项目
  406. total++;
  407. if (project.status === '2') { // 已完成
  408. completed++;
  409. }
  410. }
  411. });
  412. completedTasks.value = completed;
  413. totalTasks.value = total;
  414. // 前端也可以做一次排序确保,虽然后端已经排序了
  415. // 按完整时间排序(项目时间或最早组别时间)
  416. projectProgress.value.sort((a, b) => {
  417. const getEarliestTime = (project: ProjectProgressVo) => {
  418. if (project.groups && project.groups.length > 0) {
  419. // 有组别,找到最早的组别时间
  420. const groupTimes = project.groups
  421. .map(g => g.beginTime ? new Date(g.beginTime).getTime() : 0)
  422. .filter(time => time > 0);
  423. if (groupTimes.length > 0) {
  424. return Math.min(...groupTimes);
  425. }
  426. }
  427. // 没有组别或组别时间无效,使用项目时间
  428. return project.startTime ? new Date(project.startTime).getTime() : 0;
  429. };
  430. const aTime = getEarliestTime(a);
  431. const bTime = getEarliestTime(b);
  432. return aTime - bTime;
  433. });
  434. } catch (error) {
  435. console.error('加载项目进度失败:', error);
  436. }
  437. };
  438. // 获取项目类型文本
  439. const getProjectTypeText = (projectType: string) => {
  440. if (!game_project_type.value || !projectType) return '未知';
  441. const typeItem = game_project_type.value.find((item: any) => item.value === projectType);
  442. return typeItem ? typeItem.label : '未知';
  443. };
  444. // 格式化时间显示(包含日期)
  445. const formatTime = (timeStr: string) => {
  446. if (!timeStr) return '-';
  447. try {
  448. const date = new Date(timeStr);
  449. // 检查是否是有效日期
  450. if (isNaN(date.getTime())) {
  451. return timeStr;
  452. }
  453. return date.toLocaleString('zh-CN', {
  454. month: '2-digit',
  455. day: '2-digit',
  456. hour: '2-digit',
  457. minute: '2-digit',
  458. hour12: false
  459. });
  460. } catch (error) {
  461. return timeStr;
  462. }
  463. };
  464. // 格式化时间显示(仅时间,用于组别详情)
  465. const formatTimeOnly = (timeStr: string) => {
  466. if (!timeStr) return '-';
  467. try {
  468. const date = new Date(timeStr);
  469. // 检查是否是有效日期
  470. if (isNaN(date.getTime())) {
  471. return timeStr;
  472. }
  473. return date.toLocaleTimeString('zh-CN', {
  474. hour: '2-digit',
  475. minute: '2-digit',
  476. hour12: false
  477. });
  478. } catch (error) {
  479. return timeStr;
  480. }
  481. };
  482. // 获取排名显示文本(支持并列排名,排名数值连续)
  483. const getRankDisplay = (item: any, index: number, list: any[]) => {
  484. // 如果项目有rank字段(如团队排名),直接使用
  485. if (item.rank !== undefined) {
  486. return `第${item.rank}名`;
  487. }
  488. // 个人排名逻辑(排名数值连续)
  489. // 计算当前项目的实际排名
  490. // 排名 = 比当前积分高的不同积分数量 + 1
  491. const currentScore = item.totalScore || 0;
  492. const higherScores = new Set();
  493. for (let i = 0; i < list.length; i++) {
  494. if (list[i].totalScore > currentScore) {
  495. higherScores.add(list[i].totalScore);
  496. }
  497. }
  498. const actualRank = higherScores.size + 1;
  499. return `第${actualRank}名`;
  500. };
  501. // 刷新个人排行榜数据
  502. const refreshPersonalRanking = async () => {
  503. personalRefreshing.value = true;
  504. try {
  505. const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
  506. // 按照totalScore字段降序排序(分数高的在前面)
  507. athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
  508. // 手动刷新时重置倒计时
  509. startCountdown();
  510. // 显示成功消息
  511. ElMessage.success('个人排行榜数据已刷新');
  512. } catch (error) {
  513. console.error('刷新个人排行榜失败:', error);
  514. ElMessage.error('刷新个人排行榜失败');
  515. } finally {
  516. personalRefreshing.value = false;
  517. }
  518. };
  519. // 刷新团队排行榜数据
  520. const refreshTeamRanking = async () => {
  521. teamRefreshing.value = true;
  522. try {
  523. // 重新加载队伍积分排行榜
  524. await loadTeamScores();
  525. // 手动刷新时重置倒计时
  526. startCountdown();
  527. // 显示成功消息
  528. ElMessage.success('团队排行榜数据已刷新');
  529. } catch (error) {
  530. console.error('刷新团队排行榜失败:', error);
  531. ElMessage.error('刷新团队排行榜失败');
  532. } finally {
  533. teamRefreshing.value = false;
  534. }
  535. };
  536. // 刷新所有数据
  537. const refreshAllData = async () => {
  538. personalRefreshing.value = true;
  539. teamRefreshing.value = true;
  540. try {
  541. // 并行刷新所有数据
  542. await Promise.all([
  543. refreshPersonalRanking(),
  544. refreshTeamRanking(),
  545. loadProjectProgress()
  546. ]);
  547. ElMessage.success('所有数据已刷新');
  548. } catch (error) {
  549. console.error('刷新数据失败:', error);
  550. ElMessage.error('刷新数据失败');
  551. } finally {
  552. personalRefreshing.value = false;
  553. teamRefreshing.value = false;
  554. }
  555. };
  556. // 开启自动刷新
  557. const startAutoRefresh = () => {
  558. if (autoRefreshInterval.value) {
  559. clearInterval(autoRefreshInterval.value);
  560. }
  561. autoRefreshInterval.value = setInterval(() => {
  562. // refreshAllData();
  563. }, autoRefreshSeconds.value * 1000);
  564. // 开启倒计时
  565. startCountdown();
  566. ElMessage.success(`已开启自动刷新,每${autoRefreshSeconds.value}秒刷新一次`);
  567. };
  568. // 组件卸载时清理定时器
  569. onUnmounted(() => {
  570. if (autoRefreshInterval.value) {
  571. clearInterval(autoRefreshInterval.value);
  572. }
  573. // 停止倒计时
  574. stopCountdown();
  575. });
  576. onMounted(async () => {
  577. try{
  578. startAutoRefresh();
  579. await gameEventStore.fetchDefaultEvent();
  580. const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
  581. // 按照totalScore字段降序排序(分数高的在前面)
  582. athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
  583. // 并行加载其他数据
  584. await Promise.all([
  585. // 加载队伍积分排行榜
  586. loadTeamScores(),
  587. // 加载分组选项
  588. loadRankGroupOptions(),
  589. // 加载项目进度信息
  590. loadProjectProgress()
  591. ]);
  592. } catch (error) {
  593. console.error('加载数据失败:', error);
  594. }
  595. });
  596. </script>
  597. <style scoped>
  598. .ranking-board-page {
  599. padding: 20px;
  600. height: calc(100vh - 120px);
  601. position: relative;
  602. }
  603. /* 赛事名称标题样式 */
  604. .event-title {
  605. margin-bottom: 20px;
  606. text-align: center;
  607. }
  608. .event-title h2 {
  609. font-size: 24px;
  610. margin: 0;
  611. }
  612. .event-title p {
  613. font-size: 16px;
  614. color: #666;
  615. margin: 5px 0 0;
  616. }
  617. .ranking-row {
  618. height: calc(100% - 70px); /* 调整高度以适应新增标题区域 */
  619. }
  620. .ranking-card {
  621. width: 100%;
  622. height: 100%;
  623. display: flex;
  624. flex-direction: column;
  625. }
  626. .card-header {
  627. font-weight: bold;
  628. font-size: 16px;
  629. color: black;
  630. text-align: center;
  631. }
  632. /* 团队排行榜头部样式 */
  633. .team-ranking-header {
  634. display: flex;
  635. justify-content: center;
  636. align-items: center;
  637. }
  638. .progress-info {
  639. margin-bottom: 15px;
  640. }
  641. .ranking-list {
  642. max-height: 800px;
  643. overflow-y: auto;
  644. }
  645. .ranking-list-full {
  646. flex: 1;
  647. overflow-y: auto;
  648. }
  649. .ranking-item {
  650. padding: 10px 0;
  651. border-bottom: 1px solid #f0f0f0;
  652. }
  653. .ranking-item:last-child {
  654. border-bottom: none;
  655. }
  656. .item-content {
  657. display: flex;
  658. justify-content: space-between;
  659. }
  660. .item-name {
  661. flex: 2;
  662. text-align: left;
  663. }
  664. .item-team,
  665. .item-time,
  666. .item-type,
  667. .item-rank,
  668. .item-team-name,
  669. .item-group,
  670. .item-score {
  671. flex: 1;
  672. text-align: center;
  673. }
  674. /* 组别详情样式 */
  675. .group-details {
  676. margin-top: 8px;
  677. padding-left: 20px;
  678. border-left: 2px solid #e0e0e0;
  679. }
  680. .group-item {
  681. display: flex;
  682. justify-content: space-between;
  683. align-items: center;
  684. padding: 4px 0;
  685. font-size: 12px;
  686. color: #666;
  687. }
  688. .group-name {
  689. flex: 3;
  690. text-align: left;
  691. }
  692. .group-time {
  693. flex: 2;
  694. text-align: center;
  695. }
  696. .group-status {
  697. flex: 1;
  698. text-align: center;
  699. padding: 2px 6px;
  700. border-radius: 10px;
  701. font-size: 10px;
  702. }
  703. .status-0 {
  704. background-color: #f0f0f0;
  705. color: #999;
  706. }
  707. .status-1 {
  708. background-color: #e6f7ff;
  709. color: #1890ff;
  710. }
  711. .status-2 {
  712. background-color: #f6ffed;
  713. color: #52c41a;
  714. }
  715. /* 标题行样式 */
  716. .ranking-header {
  717. padding: 12px 0;
  718. border-bottom: 2px solid #e0e0e0;
  719. background-color: #f8f9fa;
  720. font-weight: bold;
  721. color: #333;
  722. position: sticky;
  723. top: 0;
  724. z-index: 10;
  725. }
  726. .header-content {
  727. display: flex;
  728. justify-content: space-between;
  729. }
  730. /* 个人积分排行榜标题样式 */
  731. .header-rank,
  732. .header-team,
  733. .header-score {
  734. flex: 1;
  735. text-align: center;
  736. font-size: 14px;
  737. }
  738. .header-name {
  739. flex: 2;
  740. text-align: left;
  741. font-size: 14px;
  742. }
  743. /* 团队积分排行榜标题样式 */
  744. .header-team-name {
  745. flex: 2;
  746. text-align: left;
  747. font-size: 14px;
  748. }
  749. .header-group {
  750. flex: 1;
  751. text-align: center;
  752. font-size: 14px;
  753. }
  754. /* 数据行样式 */
  755. .item-content {
  756. display: flex;
  757. justify-content: space-between;
  758. }
  759. /* 个人积分排行榜数据样式 */
  760. .item-name {
  761. flex: 2;
  762. text-align: left;
  763. }
  764. .item-team,
  765. .item-time,
  766. .item-type,
  767. .item-rank {
  768. flex: 1;
  769. text-align: center;
  770. }
  771. /* 团队积分排行榜数据样式 */
  772. .item-team-name {
  773. flex: 2;
  774. text-align: left;
  775. }
  776. .item-rank,
  777. .item-group,
  778. .item-score {
  779. flex: 1;
  780. text-align: center;
  781. }
  782. /* 团队积分排行榜特殊样式优化 */
  783. .ranking-item {
  784. padding: 10px 0;
  785. border-bottom: 1px solid #f0f0f0;
  786. transition: background-color 0.2s;
  787. }
  788. .ranking-item:hover {
  789. background-color: #f8f9fa;
  790. }
  791. .ranking-item:last-child {
  792. border-bottom: none;
  793. }
  794. /* 组别列样式优化 */
  795. .item-group {
  796. color: #666;
  797. font-size: 13px;
  798. }
  799. /* 总分列样式优化 */
  800. .item-score {
  801. font-weight: 500;
  802. /* color: #1890ff; */
  803. }
  804. /* 个人排行榜头部样式 */
  805. .personal-ranking-header {
  806. display: flex;
  807. flex-direction: column;
  808. gap: 12px;
  809. padding: 8px 0;
  810. }
  811. /* 团队排行榜头部样式 */
  812. .team-ranking-header {
  813. display: flex;
  814. flex-direction: column;
  815. gap: 12px;
  816. padding: 8px 0;
  817. }
  818. /* 标题样式 */
  819. .header-title {
  820. text-align: center;
  821. font-weight: bold;
  822. font-size: 16px;
  823. color: black;
  824. margin-bottom: 4px;
  825. }
  826. /* 控件区域样式 */
  827. .header-controls {
  828. display: flex;
  829. justify-content: space-between;
  830. align-items: center;
  831. gap: 15px;
  832. flex-wrap: wrap;
  833. min-height: 32px;
  834. }
  835. /* 显示数量控制样式 */
  836. .display-count-control {
  837. display: flex;
  838. align-items: center;
  839. white-space: nowrap;
  840. background-color: #f8f9fa;
  841. padding: 4px 8px;
  842. border-radius: 4px;
  843. border: 1px solid #e9ecef;
  844. }
  845. /* 分组选择框样式 */
  846. .group-select {
  847. width: 120px;
  848. }
  849. /* 刷新按钮样式 */
  850. .refresh-btn {
  851. min-width: 60px;
  852. height: 28px;
  853. }
  854. .refresh-btn .el-icon {
  855. margin-right: 4px;
  856. }
  857. .control-label {
  858. font-size: 12px;
  859. color: #666;
  860. margin: 0 2px;
  861. font-weight: 500;
  862. }
  863. /* 响应式设计 */
  864. @media (max-width: 1200px) {
  865. .header-controls {
  866. flex-direction: column;
  867. gap: 10px;
  868. align-items: center;
  869. }
  870. .display-count-control {
  871. order: 1;
  872. }
  873. .group-select {
  874. order: 2;
  875. }
  876. .refresh-btn {
  877. order: 3;
  878. }
  879. }
  880. @media (max-width: 768px) {
  881. .header-title {
  882. font-size: 14px;
  883. }
  884. .header-controls {
  885. gap: 8px;
  886. }
  887. .group-select {
  888. width: 100%;
  889. max-width: 200px;
  890. }
  891. }
  892. /* 输入框样式优化 */
  893. .display-count-control .el-input-number {
  894. --el-input-number-controls-width: 20px;
  895. }
  896. .display-count-control .el-input-number .el-input__inner {
  897. text-align: center;
  898. padding: 0 5px;
  899. }
  900. /* 右上角倒计时样式 */
  901. .countdown-display {
  902. position: absolute;
  903. top: 20px;
  904. right: 20px;
  905. z-index: 1000;
  906. }
  907. .countdown-card {
  908. background: rgba(255, 255, 255, 0.95);
  909. backdrop-filter: blur(10px);
  910. border: 1px solid #e4e7ed;
  911. }
  912. .countdown-content {
  913. display: flex;
  914. align-items: center;
  915. gap: 8px;
  916. padding: 4px 8px;
  917. }
  918. .countdown-icon {
  919. color: #409eff;
  920. font-size: 16px;
  921. }
  922. .countdown-text {
  923. font-weight: bold;
  924. color: #409eff;
  925. font-size: 14px;
  926. min-width: 30px;
  927. text-align: center;
  928. }
  929. /* .stop-btn {
  930. padding: 2px;
  931. color: #f56c6c;
  932. }
  933. .stop-btn:hover {
  934. background-color: #fef0f0;
  935. } */
  936. @keyframes pulse {
  937. 0% {
  938. transform: scale(1);
  939. }
  940. 50% {
  941. transform: scale(1.05);
  942. }
  943. 100% {
  944. transform: scale(1);
  945. }
  946. }
  947. /* 响应式设计 */
  948. @media (max-width: 768px) {
  949. .countdown-display {
  950. top: 10px;
  951. right: 10px;
  952. }
  953. .countdown-content {
  954. padding: 2px 6px;
  955. }
  956. .countdown-text {
  957. font-size: 12px;
  958. min-width: 25px;
  959. }
  960. }
  961. </style>