- 新增用户排行榜页面和待发布页面,支持查看和发布排行榜 - 新增排行榜相关API接口,包括获取活动排行列表、修改队伍分数、发布排名和获取用户排名列表 - 新增RankPublishStatus枚举,用于管理排行榜发布状态 - 在活动卡片中新增结束活动按钮,点击后调用结束活动接口并生成排行榜数据 - 优化用户活动首页,只显示已发布和处理中的活动 - 调整游戏页面定时刷新间隔从2秒改为3秒,优化性能 - 修复排行榜详情页分数输入最大值限制问题 - 清理未使用的代码文件,移除results-detail-copy相关组件和路由 - 更新路由配置和类型定义,支持新的排行榜页面
274 lines
7.9 KiB
Vue
274 lines
7.9 KiB
Vue
<script setup lang="tsx">
|
||
import { NCard, NDivider, useMessage } from 'naive-ui'
|
||
import { onMounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { RankPublishStatus } from '@/enum/business'
|
||
import { fetchRankDetail, fetchTeamQuestionScore, publishRank, updateTeamScore } from '@/service/api/rank'
|
||
import RankHeader from './modules/rank-header.vue'
|
||
import TeamItem from './modules/team-item.vue'
|
||
|
||
import TeamListHeader from './modules/team-list-header.vue'
|
||
|
||
const route = useRoute()
|
||
const message = useMessage()
|
||
const activityId = route.query.activityId as string
|
||
const id = route.query.id as string
|
||
|
||
const Status = ref(Number(route.query.Status))
|
||
|
||
const competitionInfo = ref({
|
||
name: route.query.name as string || '阅读之星-辞海遨游环节 (2026)',
|
||
date: route.query.date as string || '2026.01.12',
|
||
})
|
||
|
||
interface TeamData {
|
||
id: number
|
||
rank: number
|
||
name: string
|
||
group: string
|
||
correctCount: string
|
||
totalScore: number
|
||
updateTime: string
|
||
isExpanded: boolean
|
||
scores: Record<string, { score: number, questionId: number, teamQuestionUseId: number, questionDetailId: number }>
|
||
}
|
||
|
||
const teamList = ref<TeamData[]>([])
|
||
|
||
/** 获取排行榜详情 */
|
||
async function getRankDetail() {
|
||
if (!activityId)
|
||
return
|
||
try {
|
||
const { data, error } = await fetchRankDetail(Number(activityId))
|
||
if (error) {
|
||
message.error(error.message)
|
||
return
|
||
}
|
||
|
||
// console.log(data, 'data')
|
||
// 假设后端返回的数据结构需要映射
|
||
teamList.value = (data?.data || []).map((item: any, index: number) => ({
|
||
id: item.TeamID,
|
||
rank: index + 1,
|
||
name: item.Name,
|
||
group: '默认组', // 接口没有组名,暂时用默认
|
||
correctCount: '0 / 0',
|
||
totalScore: item.TotalPoint || 0,
|
||
updateTime: '', // 暂时用当前时间
|
||
isExpanded: false,
|
||
scores: {},
|
||
}))
|
||
}
|
||
catch (err: any) {
|
||
message.error(err.message || '获取排行榜详情失败')
|
||
}
|
||
}
|
||
|
||
/** 获取队伍具体的题目分数 */
|
||
async function getTeamQuestionScore(team: TeamData) {
|
||
try {
|
||
const { data, error } = await fetchTeamQuestionScore(team.id)
|
||
|
||
if (error) {
|
||
message.error(error.message)
|
||
return
|
||
}
|
||
|
||
// 映射接口数据到 scores
|
||
// 接口返回的是一个数组,每个元素对应一个题目
|
||
// 我们使用 QuestionId 或者 index 作为 key
|
||
const scores: Record<string, { score: number, questionId: number, teamQuestionUseId: number, questionDetailId: number }> = {}
|
||
let totalScore = 0
|
||
let correctCount = 0
|
||
let questionCount = 0
|
||
|
||
const list = data?.data || []
|
||
|
||
list.forEach((item: any, index: number) => {
|
||
// 使用 q + (index + 1) 作为 key,例如 q1, q2...
|
||
const key = `q${index + 1}`
|
||
// ResultPotins 是最终得分
|
||
const score = item.ResultPotins || 0
|
||
scores[key] = {
|
||
score,
|
||
questionId: item.QuestionId || 0,
|
||
teamQuestionUseId: item.Id || 0, // 使用接口返回的 Id 作为 TeamQuestionUseID
|
||
questionDetailId: item.QuestionDetaiID || 0, // 注意后端字段拼写 QuestionDetaiID
|
||
}
|
||
totalScore += score
|
||
|
||
// 统计正确数:如果分数 > 0 视为正确,或者根据 Status 判断
|
||
// 这里暂时以分数 > 0 为准,或者 ResultPotins === Points (满分)
|
||
// 根据示例数据,Points是题目分值,ResultPotins是得分。
|
||
// 只要得分 > 0 就算对?或者得分 == Points?
|
||
// 简单起见,得分 > 0 算对
|
||
if (score > 0) {
|
||
correctCount++
|
||
}
|
||
questionCount++
|
||
})
|
||
|
||
team.scores = scores
|
||
team.totalScore = totalScore
|
||
team.correctCount = `${correctCount} / ${questionCount}`
|
||
}
|
||
catch (err: any) {
|
||
message.error(err.message || '获取队伍题目分数失败')
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
getRankDetail()
|
||
})
|
||
|
||
async function toggleExpand(team: TeamData) {
|
||
team.isExpanded = !team.isExpanded
|
||
|
||
if (team.isExpanded && Object.keys(team.scores).length === 0) {
|
||
await getTeamQuestionScore(team)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存队伍的分数
|
||
* @param team - 队伍数据
|
||
*/
|
||
async function saveScores(team: TeamData) {
|
||
// window.$message?.success(`已保存 ${team.name} 的分数变动`)
|
||
// console.log(team.scores, 'team.scores');
|
||
|
||
// 构建提交数据
|
||
// 我们只提交有 teamQuestionUseId 的分数
|
||
// 假设所有分数都需要提交,或者可以只提交变化的分数(如果能追踪变化)
|
||
// 这里简单起见,提交该队伍所有题目的当前分数
|
||
const scoreList = Object.values(team.scores)
|
||
.filter(item => item.teamQuestionUseId)
|
||
.map(item => ({
|
||
Id: item.teamQuestionUseId,
|
||
ResultPotins: item.score,
|
||
ActivityID: Number(activityId),
|
||
}))
|
||
|
||
if (scoreList.length === 0) {
|
||
message.warning('没有可保存的分数')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const { error } = await updateTeamScore(scoreList, Number(activityId))
|
||
if (error) {
|
||
message.error(error.message || '保存分数失败')
|
||
return
|
||
}
|
||
message.success(`已保存 ${team.name} 的分数变动`)
|
||
team.isExpanded = false
|
||
team.updateTime = new Date().toLocaleString()
|
||
await getRankDetail()
|
||
}
|
||
catch (e: any) {
|
||
message.error(e.message || '保存分数失败')
|
||
}
|
||
}
|
||
|
||
async function handlePublish() {
|
||
if (Status.value === RankPublishStatus.Published) {
|
||
message.warning('本场榜单已发布,请勿重复发布')
|
||
return
|
||
}
|
||
|
||
const d = window.$dialog?.warning({
|
||
title: '确认发布',
|
||
content: '确定要发布本场榜单吗?发布后终端将可见结果。',
|
||
positiveText: '确认发布',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
if (d) {
|
||
d.loading = true
|
||
}
|
||
try {
|
||
const { error } = await publishRank(Number(id))
|
||
if (error) {
|
||
message.error(error.message || '发布失败')
|
||
return false
|
||
}
|
||
message.success('本场榜单发布成功')
|
||
Status.value = RankPublishStatus.Published
|
||
return true
|
||
}
|
||
catch (e: any) {
|
||
message.error(e.message || '发布失败')
|
||
return false
|
||
}
|
||
finally {
|
||
if (d) {
|
||
d.loading = false
|
||
}
|
||
}
|
||
},
|
||
})
|
||
}
|
||
|
||
function updateScore(team: TeamData, key: string, value: number | null) {
|
||
if (value !== null) {
|
||
if (team.scores[key]) {
|
||
team.scores[key].score = value
|
||
}
|
||
else {
|
||
// 这是一个新key的情况,应该不太可能发生,但为了类型安全
|
||
// 这里我们无法得知 questionId,所以暂时设为 0 或者需要修改逻辑传递 questionId
|
||
// 实际上 updateScore 是由 UI 触发的,UI 遍历的是现有的 keys
|
||
team.scores[key] = { score: value, questionId: 0, teamQuestionUseId: 0, questionDetailId: 0 }
|
||
}
|
||
|
||
// 重新计算总分和正确题数
|
||
let totalScore = 0
|
||
let correctCount = 0
|
||
const keys = Object.keys(team.scores)
|
||
const questionCount = keys.length
|
||
|
||
keys.forEach((k) => {
|
||
const s = team.scores[k].score
|
||
totalScore += s
|
||
if (s > 0) {
|
||
correctCount++
|
||
}
|
||
})
|
||
|
||
team.totalScore = totalScore
|
||
team.correctCount = `${correctCount} / ${questionCount}`
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||
<NCard :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
||
<!-- Header -->
|
||
<RankHeader :title="competitionInfo.name" :date="competitionInfo.date" @publish="handlePublish" />
|
||
|
||
<NDivider />
|
||
|
||
<!-- List Header -->
|
||
<TeamListHeader />
|
||
|
||
<!-- Custom Table / List -->
|
||
<div class="flex flex-col gap-4">
|
||
<!-- Data Rows -->
|
||
<TeamItem
|
||
v-for="team in teamList" :key="team.id" :team="team" @toggle-expand="toggleExpand"
|
||
@save-scores="saveScores" @update-score="updateScore"
|
||
/>
|
||
</div>
|
||
</NCard>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.n-input-number .n-input__input-el) {
|
||
text-align: center;
|
||
font-weight: bold;
|
||
color: var(--primary-color);
|
||
}
|
||
</style>
|