Files
reader-star/apps/admin/src/views/rank/rank-detail/index.vue
rjl 8469e16638 feat(admin): 新增用户排行榜功能并优化活动流程
- 新增用户排行榜页面和待发布页面,支持查看和发布排行榜
- 新增排行榜相关API接口,包括获取活动排行列表、修改队伍分数、发布排名和获取用户排名列表
- 新增RankPublishStatus枚举,用于管理排行榜发布状态
- 在活动卡片中新增结束活动按钮,点击后调用结束活动接口并生成排行榜数据
- 优化用户活动首页,只显示已发布和处理中的活动
- 调整游戏页面定时刷新间隔从2秒改为3秒,优化性能
- 修复排行榜详情页分数输入最大值限制问题
- 清理未使用的代码文件,移除results-detail-copy相关组件和路由
- 更新路由配置和类型定义,支持新的排行榜页面
2026-03-09 08:37:32 +08:00

274 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>