feat(admin): 新增用户排行榜功能并优化活动流程
- 新增用户排行榜页面和待发布页面,支持查看和发布排行榜 - 新增排行榜相关API接口,包括获取活动排行列表、修改队伍分数、发布排名和获取用户排名列表 - 新增RankPublishStatus枚举,用于管理排行榜发布状态 - 在活动卡片中新增结束活动按钮,点击后调用结束活动接口并生成排行榜数据 - 优化用户活动首页,只显示已发布和处理中的活动 - 调整游戏页面定时刷新间隔从2秒改为3秒,优化性能 - 修复排行榜详情页分数输入最大值限制问题 - 清理未使用的代码文件,移除results-detail-copy相关组件和路由 - 更新路由配置和类型定义,支持新的排行榜页面
This commit is contained in:
@ -136,9 +136,9 @@ async function fetchGetGameStatisticsData() {
|
||||
}
|
||||
|
||||
// 定时刷新 检查是否可以下一步
|
||||
timer.value = setInterval(checkNextStep, 2000)
|
||||
timer.value = setInterval(checkNextStep, 3000)
|
||||
// 定时刷新 游戏统计数据
|
||||
statisticsTimer.value = setInterval(fetchGetGameStatisticsData, 2000)
|
||||
statisticsTimer.value = setInterval(fetchGetGameStatisticsData, 3000)
|
||||
|
||||
/** 销毁 定时刷新 检查是否可以下一步 */
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchUpdatePublishStatus } from '@/service/api/competition'
|
||||
import { fetchGetQuestionTableByActivityID } from '@/service/api/game'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import { AudioController } from '@/utils/audio'
|
||||
@ -93,7 +95,20 @@ function handleBack() {
|
||||
routerBack()
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
async function handleNext() {
|
||||
// 封面页的下一步操作,更改活动状态
|
||||
// 判断状态是否为已发布或处理中,若已发布或处理中则不能更改状态
|
||||
if (activityInfo.value?.PublishStatus === PublishStatus.Published || activityInfo.value?.PublishStatus === PublishStatus.Processing) {
|
||||
if (activityId.value) {
|
||||
try {
|
||||
await fetchUpdatePublishStatus(Number(activityId.value), PublishStatus.Processing)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('更新活动状态失败', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routerPushByKey('user_rules', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
@ -143,24 +158,20 @@ onMounted(async () => {
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
:show-back="true" :show-next="showNextButton" :title="activityName" :title-bg-type="2"
|
||||
@back="handleBack"
|
||||
@next="handleNext"
|
||||
@back="handleBack" @next="handleNext"
|
||||
>
|
||||
<div class="h-screen w-full overflow-y-auto font-sans">
|
||||
<div class="relative z-10 min-h-full w-full">
|
||||
<div class="min-h-full w-full flex flex-col items-center pb-12 pt-24">
|
||||
<!-- 卡片列表 -->
|
||||
<TransitionGroup
|
||||
tag="div"
|
||||
name="card-anim"
|
||||
tag="div" name="card-anim"
|
||||
class="perspective-container max-w-[1800px] w-full flex flex-wrap justify-center gap-4 px-4 md:gap-10 md:px-12"
|
||||
>
|
||||
<div
|
||||
v-for="(node, index) in showCards ? questions : []"
|
||||
:key="node.id"
|
||||
v-for="(node, index) in showCards ? questions : []" :key="node.id"
|
||||
class="w-[44%] card-wrapper 2xl:w-[14%] lg:w-[21%] md:w-[28%] xl:w-[17%]"
|
||||
:style="{ animationDelay: `${index * 0.2}s` }"
|
||||
@click="toggleFlip(index)"
|
||||
:style="{ animationDelay: `${index * 0.2}s` }" @click="toggleFlip(index)"
|
||||
>
|
||||
<div class="flip-card-inner" :class="{ 'is-flipped': flippedCards.has(index) }">
|
||||
<!-- 正面 (Front) - 显示背面图案 -->
|
||||
@ -216,16 +227,19 @@ onMounted(async () => {
|
||||
transform: translate3d(0, 300px, -800px) rotateY(180deg) rotateX(45deg) scale(0.5);
|
||||
filter: blur(20px); // 深度模糊
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter: blur(0); // 聚焦
|
||||
}
|
||||
|
||||
70% {
|
||||
// [过冲状态]
|
||||
// 飞到眼前一点点 (Z=50px)
|
||||
// 角度稍微回弹 (rotateY -15deg)
|
||||
transform: translate3d(0, -20px, 50px) rotateY(-15deg) rotateX(-10deg) scale(1.05);
|
||||
}
|
||||
|
||||
100% {
|
||||
// [最终归位]
|
||||
transform: translate3d(0, 0, 0) rotateY(0) rotateX(0) scale(1);
|
||||
|
||||
@ -197,7 +197,7 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
try {
|
||||
const { data } = await fetchGetGameStatistics(_params)
|
||||
console.log(data, 'data')
|
||||
// console.log(data, 'data')
|
||||
// 更新图表数据
|
||||
// 假设接口返回格式为: [{ TeamName: '组1', Points: 10, ResultPotins: 5 }, ...]
|
||||
// 需要根据实际接口返回结构调整映射逻辑
|
||||
|
||||
@ -29,13 +29,10 @@ async function getGroups() {
|
||||
if (list && Array.isArray(list)) {
|
||||
groups.value = list.map((item: any) => ({
|
||||
...item,
|
||||
// id: item.Id,
|
||||
// name: item.Name,
|
||||
icon: 'activity', // Default icon since API might not provide one
|
||||
icon: 'activity',
|
||||
}))
|
||||
}
|
||||
isAllEnd.value = list.every((item: Api.Competition.ActivityTeamGroup) => item.IsEnd)
|
||||
// console.log(groups.value, 'groups.value')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@ -49,7 +46,7 @@ function handleBack() {
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
routerPushByKey('user')
|
||||
routerPushByKey('user_rank-list', { query: { activityId } })
|
||||
}
|
||||
|
||||
function selectGroup(item: Api.Competition.ActivityTeamGroup) {
|
||||
|
||||
@ -3,6 +3,7 @@ import { NCarousel, NImage } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import beacon from '@/assets/imgs/user/event-card-bg-icon.svg'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetActivityList } from '@/service/api/competition'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
@ -32,14 +33,16 @@ async function getEvents() {
|
||||
|
||||
const list = data?.data || []
|
||||
if (list && Array.isArray(list)) {
|
||||
events.value = list.map((item: any) => ({
|
||||
...item,
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
subTitle: item.Description,
|
||||
icon: 'activity',
|
||||
status: 'active',
|
||||
}))
|
||||
events.value = list
|
||||
.filter((item: any) => item.PublishStatus === PublishStatus.Published || item.PublishStatus === PublishStatus.Processing)
|
||||
.map((item: any) => ({
|
||||
...item,
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
subTitle: item.Description,
|
||||
icon: 'activity',
|
||||
status: 'active',
|
||||
}))
|
||||
}
|
||||
}
|
||||
finally {
|
||||
@ -185,7 +188,7 @@ onMounted(() => {
|
||||
|
||||
.text-content {
|
||||
.event-title {
|
||||
font-size: 1.4rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: $text-color;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@ -81,7 +81,7 @@ watch(
|
||||
<div class="w-full flex p-6">
|
||||
<!-- <p>template:{{ template }}</p> -->
|
||||
<!-- 模板A: 汉字听写1-提示 (拼音+提示) -->
|
||||
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation1" class="text-center">
|
||||
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation1 || template === QuestionCategoryEnum.ChineseCharacterDictation2" class="text-center">
|
||||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||||
<div class="ptions-container1 mb-2 text-center text-2xl">
|
||||
{{ title }}
|
||||
@ -127,7 +127,7 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- 模板E: 成语-文字要求 -->
|
||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting1" class="text-center">
|
||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting1 || template === QuestionCategoryEnum.IdiomWriting2" class="text-center">
|
||||
<div class="mb-8 text-4xl text-gray-800 font-bold">
|
||||
<!-- {{ content }} -->
|
||||
<div class="rich-content" v-html="content" />
|
||||
|
||||
268
apps/admin/src/views/user/rank-list/index.vue
Normal file
268
apps/admin/src/views/user/rank-list/index.vue
Normal file
@ -0,0 +1,268 @@
|
||||
<script setup lang="tsx">
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NDataTable, NImage, NTag, useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import title from '@/assets/imgs/user/rank-title.svg'
|
||||
import { fetchUserRankList } from '@/service/api/rank'
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const MainID = Number(route.query.activityId)
|
||||
|
||||
// 定义数据结构
|
||||
interface TeamData {
|
||||
id: string
|
||||
rank: number
|
||||
teamName: string
|
||||
[key: string]: number | string // 允许任意数量的题目分数
|
||||
total: number
|
||||
}
|
||||
|
||||
const columns = ref<DataTableColumns<TeamData>>([
|
||||
{
|
||||
title: '排名',
|
||||
key: 'rank',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render(row: TeamData) {
|
||||
const rank = row.rank
|
||||
if (rank === 1) {
|
||||
return <NTag type="warning" size="large" round>🏆</NTag>
|
||||
}
|
||||
if (rank === 2) {
|
||||
return <NTag type="info" size="large" round>🥈</NTag>
|
||||
}
|
||||
if (rank === 3) {
|
||||
return <NTag type="success" size="large" round>🥉</NTag>
|
||||
}
|
||||
return <span>{rank}</span>
|
||||
},
|
||||
},
|
||||
{ title: '队伍名称', key: 'teamName', align: 'left', width: 150, className: 'team-name' },
|
||||
{ title: '总积分', key: 'total', align: 'center', className: 'total-score' },
|
||||
])
|
||||
|
||||
const tableData = ref<TeamData[]>([])
|
||||
|
||||
async function getUserRankList() {
|
||||
const { data, error } = await fetchUserRankList(MainID)
|
||||
if (error) {
|
||||
message.error(error.message || '获取排行榜失败')
|
||||
return
|
||||
}
|
||||
|
||||
if (data?.data && data.data.length > 0) {
|
||||
// 动态生成题目列
|
||||
const firstTeam = data.data[0]
|
||||
const questionColumns = firstTeam.Questions.map(q => ({
|
||||
title: `第${q.QuestionNO}题`,
|
||||
key: `q${q.QuestionNO}`,
|
||||
align: 'center' as const,
|
||||
}))
|
||||
|
||||
// 更新表头
|
||||
columns.value = [
|
||||
{
|
||||
title: '排名',
|
||||
key: 'rank',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render(row: TeamData) {
|
||||
const rank = row.rank
|
||||
if (rank === 1) {
|
||||
return <NTag type="warning" size="large" round>🏆</NTag>
|
||||
}
|
||||
if (rank === 2) {
|
||||
return <NTag type="info" size="large" round>🥈</NTag>
|
||||
}
|
||||
if (rank === 3) {
|
||||
return <NTag type="success" size="large" round>🥉</NTag>
|
||||
}
|
||||
return <span>{rank}</span>
|
||||
},
|
||||
},
|
||||
{ title: '队伍名称', key: 'teamName', align: 'left', width: 150, className: 'team-name' },
|
||||
...questionColumns,
|
||||
{ title: '总积分', key: 'total', align: 'center', className: 'total-score' },
|
||||
]
|
||||
|
||||
tableData.value = data.data.map((item, index) => {
|
||||
const questions: Record<string, number> = {}
|
||||
item.Questions.forEach((q) => {
|
||||
questions[`q${q.QuestionNO}`] = q.ResultPotin
|
||||
})
|
||||
|
||||
return {
|
||||
id: String(item.TeamID),
|
||||
rank: index + 1,
|
||||
teamName: item.Name,
|
||||
total: item.TotalPoint,
|
||||
...questions,
|
||||
} as TeamData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (MainID) {
|
||||
getUserRankList()
|
||||
}
|
||||
})
|
||||
|
||||
// 动态赋予行样式:根据排名和奇偶数设置 class
|
||||
function rowClassName(row: TeamData) {
|
||||
const rank = row.rank
|
||||
const classes = ['rank-row']
|
||||
if (rank >= 1 && rank <= 3) {
|
||||
classes.push(`rank-top-${rank}`)
|
||||
}
|
||||
return classes.join(' ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompetitionLayout :show-back="false" :show-next="false" :show-title="false">
|
||||
<div class="leaderboard-container">
|
||||
<div class="leaderboard-header">
|
||||
<NImage :src="title" alt="logo" preview-disabled class="mt--60px" object-fit="contain" />
|
||||
<p class="mt--20px text-center text-sm text-#6b778d">
|
||||
"勤学如春起之苗,不见其增,日有所长"
|
||||
</p>
|
||||
</div>
|
||||
<div class="leaderboard-content">
|
||||
<NDataTable
|
||||
:columns="columns" :data="tableData" :bordered="false" :bottom-bordered="false"
|
||||
:row-class-name="rowClassName" class="rank-table"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CompetitionLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 1. 整体容器和背景 */
|
||||
.leaderboard-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-image: url('https://raw.githubusercontent.com/0x3f3f3f3f/figure-bed/main/img/20240524150352.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
/* width: 100%; */
|
||||
/* height: 781px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url('@/assets/imgs/user/team-bg.svg') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
transform-origin: center;
|
||||
padding: 40px 20px;
|
||||
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif; */
|
||||
}
|
||||
|
||||
/* 2. 页面标题 */
|
||||
.leaderboard-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.leaderboard-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #1a2a44;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* .leaderboard-header p {
|
||||
font-size: 1rem;
|
||||
color: #6b778d;
|
||||
margin-top: 8px;
|
||||
} */
|
||||
|
||||
/* 3. 表格内容区 */
|
||||
.leaderboard-content {
|
||||
width: 100%;
|
||||
/* max-width: 1200px; */
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(26, 42, 68, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 4. Naive UI 表格深度定制 */
|
||||
:deep(.rank-table .n-data-table-thead) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.rank-table .n-data-table-th) {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #dee2e6 !important;
|
||||
}
|
||||
|
||||
:deep(.rank-table .n-data-table-tr) {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
:deep(.rank-table .n-data-table-tr:hover) {
|
||||
background-color: #f1f3f5 !important;
|
||||
}
|
||||
|
||||
:deep(.rank-table .n-data-table-td) {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e9ecef !important;
|
||||
padding-top: 16px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
|
||||
/* 奇偶行交替色 */
|
||||
:deep(.rank-table .n-data-table-tr:nth-of-type(odd)) {
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
|
||||
/* 队伍名称和总分加粗 */
|
||||
:deep(.team-name) {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.total-score) {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: #e67e22;
|
||||
}
|
||||
|
||||
/* 前三名高亮 */
|
||||
:deep(.rank-top-1 .n-data-table-td) {
|
||||
background-color: #fff9e6 !important;
|
||||
}
|
||||
|
||||
:deep(.rank-top-2 .n-data-table-td) {
|
||||
background-color: #f0f4ff !important;
|
||||
}
|
||||
|
||||
:deep(.rank-top-3 .n-data-table-td) {
|
||||
background-color: #e6f4ea !important;
|
||||
}
|
||||
|
||||
:deep(.rank-top-1 .total-score) {
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
:deep(.rank-top-2 .total-score) {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
:deep(.rank-top-3 .total-score) {
|
||||
color: #cd7f32;
|
||||
}
|
||||
</style>
|
||||
0
apps/admin/src/views/user/rank-pending/index.vue
Normal file
0
apps/admin/src/views/user/rank-pending/index.vue
Normal file
Reference in New Issue
Block a user