RankingBoardPage.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  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. // refreshAllData();
  579. startAutoRefresh();
  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. // // 开启自动刷新
  593. // startAutoRefresh();
  594. } catch (error) {
  595. console.error('加载数据失败:', error);
  596. }
  597. });
  598. </script>
  599. <style scoped>
  600. .ranking-board-page {
  601. padding: 20px;
  602. height: calc(100vh - 120px);
  603. position: relative;
  604. }
  605. /* 赛事名称标题样式 */
  606. .event-title {
  607. margin-bottom: 20px;
  608. text-align: center;
  609. }
  610. .event-title h2 {
  611. font-size: 24px;
  612. margin: 0;
  613. }
  614. .event-title p {
  615. font-size: 16px;
  616. color: #666;
  617. margin: 5px 0 0;
  618. }
  619. .ranking-row {
  620. height: calc(100% - 70px); /* 调整高度以适应新增标题区域 */
  621. }
  622. .ranking-card {
  623. width: 100%;
  624. height: 100%;
  625. display: flex;
  626. flex-direction: column;
  627. }
  628. .card-header {
  629. font-weight: bold;
  630. font-size: 16px;
  631. color: black;
  632. text-align: center;
  633. }
  634. /* 团队排行榜头部样式 */
  635. .team-ranking-header {
  636. display: flex;
  637. justify-content: center;
  638. align-items: center;
  639. }
  640. .progress-info {
  641. margin-bottom: 15px;
  642. }
  643. .ranking-list {
  644. max-height: 800px;
  645. overflow-y: auto;
  646. }
  647. .ranking-list-full {
  648. flex: 1;
  649. overflow-y: auto;
  650. }
  651. .ranking-item {
  652. padding: 10px 0;
  653. border-bottom: 1px solid #f0f0f0;
  654. }
  655. .ranking-item:last-child {
  656. border-bottom: none;
  657. }
  658. .item-content {
  659. display: flex;
  660. justify-content: space-between;
  661. }
  662. .item-name {
  663. flex: 2;
  664. text-align: left;
  665. }
  666. .item-team,
  667. .item-time,
  668. .item-type,
  669. .item-rank,
  670. .item-team-name,
  671. .item-group,
  672. .item-score {
  673. flex: 1;
  674. text-align: center;
  675. }
  676. /* 组别详情样式 */
  677. .group-details {
  678. margin-top: 8px;
  679. padding-left: 20px;
  680. border-left: 2px solid #e0e0e0;
  681. }
  682. .group-item {
  683. display: flex;
  684. justify-content: space-between;
  685. align-items: center;
  686. padding: 4px 0;
  687. font-size: 12px;
  688. color: #666;
  689. }
  690. .group-name {
  691. flex: 3;
  692. text-align: left;
  693. }
  694. .group-time {
  695. flex: 2;
  696. text-align: center;
  697. }
  698. .group-status {
  699. flex: 1;
  700. text-align: center;
  701. padding: 2px 6px;
  702. border-radius: 10px;
  703. font-size: 10px;
  704. }
  705. .status-0 {
  706. background-color: #f0f0f0;
  707. color: #999;
  708. }
  709. .status-1 {
  710. background-color: #e6f7ff;
  711. color: #1890ff;
  712. }
  713. .status-2 {
  714. background-color: #f6ffed;
  715. color: #52c41a;
  716. }
  717. /* 标题行样式 */
  718. .ranking-header {
  719. padding: 12px 0;
  720. border-bottom: 2px solid #e0e0e0;
  721. background-color: #f8f9fa;
  722. font-weight: bold;
  723. color: #333;
  724. position: sticky;
  725. top: 0;
  726. z-index: 10;
  727. }
  728. .header-content {
  729. display: flex;
  730. justify-content: space-between;
  731. }
  732. /* 个人积分排行榜标题样式 */
  733. .header-rank,
  734. .header-team,
  735. .header-score {
  736. flex: 1;
  737. text-align: center;
  738. font-size: 14px;
  739. }
  740. .header-name {
  741. flex: 2;
  742. text-align: left;
  743. font-size: 14px;
  744. }
  745. /* 团队积分排行榜标题样式 */
  746. .header-team-name {
  747. flex: 2;
  748. text-align: left;
  749. font-size: 14px;
  750. }
  751. .header-group {
  752. flex: 1;
  753. text-align: center;
  754. font-size: 14px;
  755. }
  756. /* 数据行样式 */
  757. .item-content {
  758. display: flex;
  759. justify-content: space-between;
  760. }
  761. /* 个人积分排行榜数据样式 */
  762. .item-name {
  763. flex: 2;
  764. text-align: left;
  765. }
  766. .item-team,
  767. .item-time,
  768. .item-type,
  769. .item-rank {
  770. flex: 1;
  771. text-align: center;
  772. }
  773. /* 团队积分排行榜数据样式 */
  774. .item-team-name {
  775. flex: 2;
  776. text-align: left;
  777. }
  778. .item-rank,
  779. .item-group,
  780. .item-score {
  781. flex: 1;
  782. text-align: center;
  783. }
  784. /* 团队积分排行榜特殊样式优化 */
  785. .ranking-item {
  786. padding: 10px 0;
  787. border-bottom: 1px solid #f0f0f0;
  788. transition: background-color 0.2s;
  789. }
  790. .ranking-item:hover {
  791. background-color: #f8f9fa;
  792. }
  793. .ranking-item:last-child {
  794. border-bottom: none;
  795. }
  796. /* 组别列样式优化 */
  797. .item-group {
  798. color: #666;
  799. font-size: 13px;
  800. }
  801. /* 总分列样式优化 */
  802. .item-score {
  803. font-weight: 500;
  804. /* color: #1890ff; */
  805. }
  806. /* 个人排行榜头部样式 */
  807. .personal-ranking-header {
  808. display: flex;
  809. flex-direction: column;
  810. gap: 12px;
  811. padding: 8px 0;
  812. }
  813. /* 团队排行榜头部样式 */
  814. .team-ranking-header {
  815. display: flex;
  816. flex-direction: column;
  817. gap: 12px;
  818. padding: 8px 0;
  819. }
  820. /* 标题样式 */
  821. .header-title {
  822. text-align: center;
  823. font-weight: bold;
  824. font-size: 16px;
  825. color: black;
  826. margin-bottom: 4px;
  827. }
  828. /* 控件区域样式 */
  829. .header-controls {
  830. display: flex;
  831. justify-content: space-between;
  832. align-items: center;
  833. gap: 15px;
  834. flex-wrap: wrap;
  835. min-height: 32px;
  836. }
  837. /* 显示数量控制样式 */
  838. .display-count-control {
  839. display: flex;
  840. align-items: center;
  841. white-space: nowrap;
  842. background-color: #f8f9fa;
  843. padding: 4px 8px;
  844. border-radius: 4px;
  845. border: 1px solid #e9ecef;
  846. }
  847. /* 分组选择框样式 */
  848. .group-select {
  849. width: 120px;
  850. }
  851. /* 刷新按钮样式 */
  852. .refresh-btn {
  853. min-width: 60px;
  854. height: 28px;
  855. }
  856. .refresh-btn .el-icon {
  857. margin-right: 4px;
  858. }
  859. .control-label {
  860. font-size: 12px;
  861. color: #666;
  862. margin: 0 2px;
  863. font-weight: 500;
  864. }
  865. /* 响应式设计 */
  866. @media (max-width: 1200px) {
  867. .header-controls {
  868. flex-direction: column;
  869. gap: 10px;
  870. align-items: center;
  871. }
  872. .display-count-control {
  873. order: 1;
  874. }
  875. .group-select {
  876. order: 2;
  877. }
  878. .refresh-btn {
  879. order: 3;
  880. }
  881. }
  882. @media (max-width: 768px) {
  883. .header-title {
  884. font-size: 14px;
  885. }
  886. .header-controls {
  887. gap: 8px;
  888. }
  889. .group-select {
  890. width: 100%;
  891. max-width: 200px;
  892. }
  893. }
  894. /* 输入框样式优化 */
  895. .display-count-control .el-input-number {
  896. --el-input-number-controls-width: 20px;
  897. }
  898. .display-count-control .el-input-number .el-input__inner {
  899. text-align: center;
  900. padding: 0 5px;
  901. }
  902. /* 右上角倒计时样式 */
  903. .countdown-display {
  904. position: absolute;
  905. top: 20px;
  906. right: 20px;
  907. z-index: 1000;
  908. }
  909. .countdown-card {
  910. background: rgba(255, 255, 255, 0.95);
  911. backdrop-filter: blur(10px);
  912. border: 1px solid #e4e7ed;
  913. }
  914. .countdown-content {
  915. display: flex;
  916. align-items: center;
  917. gap: 8px;
  918. padding: 4px 8px;
  919. }
  920. .countdown-icon {
  921. color: #409eff;
  922. font-size: 16px;
  923. }
  924. .countdown-text {
  925. font-weight: bold;
  926. color: #409eff;
  927. font-size: 14px;
  928. min-width: 30px;
  929. text-align: center;
  930. }
  931. /* .stop-btn {
  932. padding: 2px;
  933. color: #f56c6c;
  934. }
  935. .stop-btn:hover {
  936. background-color: #fef0f0;
  937. } */
  938. @keyframes pulse {
  939. 0% {
  940. transform: scale(1);
  941. }
  942. 50% {
  943. transform: scale(1.05);
  944. }
  945. 100% {
  946. transform: scale(1);
  947. }
  948. }
  949. /* 响应式设计 */
  950. @media (max-width: 768px) {
  951. .countdown-display {
  952. top: 10px;
  953. right: 10px;
  954. }
  955. .countdown-content {
  956. padding: 2px 6px;
  957. }
  958. .countdown-text {
  959. font-size: 12px;
  960. min-width: 25px;
  961. }
  962. }
  963. </style>