feat(competition): 支持决赛环节并添加加时赛管理功能

- 扩展竞赛环节类型,支持决赛环节(RoundType=3)
- 新增加时赛创建功能,支持多分组队伍选择
- 添加题目库禁用/启用功能,支持批量操作
- 优化用户端竞赛流程,区分常规赛和加时赛
- 重构操作按钮组件,提高代码复用性
This commit is contained in:
2026-03-09 17:34:40 +08:00
parent 8469e16638
commit 11fb5481d3
22 changed files with 468 additions and 142 deletions

View File

@ -55,6 +55,7 @@ function handleNext() {
groupsId: groupsId.value,
teamId: teamId.value,
teamIds: teamIds.value,
RoundType: Number(currentQuestionMainInfo.value?.Activity_Question.RoundType) || 0,
},
})
// 清空当前题目信息
@ -73,7 +74,7 @@ async function initQuestions() {
const { data: outlineData, error } = await fetchGetCurrentQuestion({
ActivityID: Number(activityId.value),
GroupID: Number(groupsId.value),
RoundType: 0,
RoundType: Number(currentQuestionMainInfo.value?.Activity_Question.RoundType) || 0,
})
// eslint-disable-next-line no-console
console.log(outlineData, 'outlineData')

View File

@ -45,6 +45,8 @@ const groupsId = computed(() => Number(route.query?.groupsId)) || activityInfoSt
const teamId = computed(() => activityInfoStore.teamId || Number(route.query?.teamId))
const teamIds = computed(() => route.query?.teamIds as string)
const RoundType = route.query?.RoundType || 0
async function getQuestions() {
if (!activityId.value || !groupsId.value || !teamId.value) {
window?.$message?.error('参数错误')
@ -53,7 +55,7 @@ async function getQuestions() {
loading.value = true
try {
const { data, error } = await fetchGetQuestionTableByActivityID(String(activityId.value))
const { data, error } = await fetchGetQuestionTableByActivityID(String(activityId.value), Number(RoundType))
if (error) {
window?.$message?.error(error.message)
return
@ -115,6 +117,7 @@ async function handleNext() {
groupsId: groupsId.value,
teamId: teamId.value,
teamIds: teamIds.value,
RoundType: Number(RoundType),
},
})
}

View File

@ -17,6 +17,7 @@ const activityId = computed(() => activityInfoStore.activityId || route.query?.a
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
const teamIds = computed(() => route.query?.teamIds as string)
const RoundType = computed(() => Number(route.query?.RoundType || 0) as Api.Competition.CompetitionRoundType)
const store = useCompetitionStore()
@ -39,7 +40,7 @@ async function initQuestions() {
const { data: outlineData, error } = await fetchGetCurrentQuestion({
ActivityID: Number(activityId.value),
GroupID: Number(groupsId.value),
RoundType: 0,
RoundType: RoundType.value,
})
// eslint-disable-next-line no-console
console.log(outlineData, 'outlineData')
@ -78,6 +79,7 @@ async function handleCardClick(item: Api.Competition.CurrentQuestionResponse) {
routerPushByKey('user_groups', {
query: {
activityId: activityId.value,
RoundType: RoundType.value,
},
})
return

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
@ -13,6 +13,12 @@ const loading = ref(false)
const activityId = route.query.activityId as string
const isAllEnd = ref(false)
// 是否有加时赛
const hasExtraTime = computed(() => {
const extraTimeGroups = groups.value.filter(item => item.RoundType === 1)
return extraTimeGroups.length > 0 && extraTimeGroups.every(item => item.IsEnd)
})
async function getGroups() {
if (!activityId)
return
@ -32,7 +38,7 @@ async function getGroups() {
icon: 'activity',
}))
}
isAllEnd.value = list.every((item: Api.Competition.ActivityTeamGroup) => item.IsEnd)
isAllEnd.value = list.filter(item => item.RoundType === 0).every((item: Api.Competition.ActivityTeamGroup) => item.IsEnd)
}
finally {
loading.value = false
@ -46,7 +52,7 @@ function handleBack() {
}
function handleNext() {
routerPushByKey('user_rank-list', { query: { activityId } })
routerPushByKey('user_rank-list', { query: { activityId, RoundType: 0 } })
}
function selectGroup(item: Api.Competition.ActivityTeamGroup) {
@ -54,7 +60,14 @@ function selectGroup(item: Api.Competition.ActivityTeamGroup) {
return
// 选择组别逻辑
routerPushByKey('user_teams', { query: { activityId, groupsId: item.Id } })
routerPushByKey('user_teams', { query: { activityId, groupsId: item.Id, RoundType: item.RoundType } })
}
/**
* 查看加时赛结果
*/
function handleNext2() {
routerPushByKey('user_rank-list', { query: { activityId, RoundType: 1 } }, true)
}
onMounted(() => {
@ -67,8 +80,9 @@ onMounted(() => {
<template>
<CompetitionLayout
:show-back="true" :show-next="isAllEnd" next-btn-text="查看队伍结果" :title-bg-type="2"
title="展示组别" @back="handleBack" @next="handleNext"
:show-back="true" :show-next="isAllEnd" next-btn-text="查看常规赛结果" next-btn-text2="查看加时赛结果"
:show-next-btn2="hasExtraTime && isAllEnd" :title-bg-type="2" title="展示组别" @back="handleBack" @next="handleNext"
@next2="handleNext2"
>
<!-- Groups Grid -->
<div class="groups-container">

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import { NButton, NCard, NInput, NModal, NSelect, NSpace, useMessage } from 'naive-ui'
import { computed, ref } from 'vue'
import { useRouterPush } from '@/hooks/common/router'
import { fetchAddExtraTime } from '@/service/api/game'
const props = defineProps<{
show: boolean
activityId: number
teamOptions: { label: string, value: number }[]
}>()
const emit = defineEmits(['update:show', 'submit'])
const { routerPushByKey } = useRouterPush()
const message = useMessage()
const extraTimeGroups = ref<{
GroupName: string
TeamInfos: number[]
}[]>([{ GroupName: '', TeamInfos: [] }])
const selectedTeams = computed(() => extraTimeGroups.value.flatMap(g => g.TeamInfos))
function getFilteredOptions(currentGroupIndex: number) {
const currentGroupTeams = extraTimeGroups.value[currentGroupIndex].TeamInfos
return props.teamOptions.filter(option =>
!selectedTeams.value.includes(option.value) || currentGroupTeams.includes(option.value),
)
}
function addGroup() {
extraTimeGroups.value.push({ GroupName: '', TeamInfos: [] })
}
function removeGroup(index: number) {
extraTimeGroups.value.splice(index, 1)
}
async function handleSubmit() {
const allSelectedTeams = new Set<number>()
for (const group of extraTimeGroups.value) {
if (!group.GroupName) {
message.warning('请输入所有分组的名称')
return
}
if (group.TeamInfos.length < 2) {
message.warning(`分组 "${group.GroupName}" 至少需要选择两个队伍`)
return
}
for (const teamId of group.TeamInfos) {
if (allSelectedTeams.has(teamId)) {
message.warning('同一个队伍不能出现在多个分组中')
return
}
allSelectedTeams.add(teamId)
}
}
const params: Api.Competition.AddExtraTimeParams = extraTimeGroups.value.map(group => ({
...group,
ActivityID: props.activityId,
TeamInfos: group.TeamInfos.map(teamId => ({
TeamID: teamId,
TeamName: props.teamOptions.find(opt => opt.value === teamId)?.label || '',
})),
}))
const { data, error } = await fetchAddExtraTime(params)
if (error) {
message.error(error.message || '创建加时赛失败')
return
}
message.success('加时赛已创建')
emit('submit', data)
emit('update:show', false)
if (data) {
routerPushByKey('user_cover', { query: { activityId: props.activityId, groupsId: data.data, RoundType: 1 } })
}
}
function handleClose() {
emit('update:show', false)
}
</script>
<template>
<NModal :show="show" @update:show="handleClose">
<NCard style="width: 600px" title="创建加时赛" :bordered="false" size="huge" role="dialog" aria-modal="true">
<NSpace vertical>
<div v-for="(group, index) in extraTimeGroups" :key="index" class="group-item">
<NInput v-model:value="group.GroupName" placeholder="请输入分组名称" />
<NSelect
v-model:value="group.TeamInfos"
multiple
:options="getFilteredOptions(index)"
placeholder="请选择队伍"
/>
<NButton v-if="extraTimeGroups.length > 1" text @click="() => removeGroup(index)">
删除
</NButton>
</div>
<NButton type="dashed" block @click="addGroup">
新增分组
</NButton>
<NButton type="primary" block @click="handleSubmit">
确定
</NButton>
</NSpace>
</NCard>
</NModal>
</template>
<style scoped>
.group-item {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
</style>

View File

@ -4,11 +4,23 @@ 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 ActionButton from '@/components/custom/user/ActionButton.vue'
import { fetchUserRankList } from '@/service/api/rank'
import ExtraTimeModal from './components/ExtraTimeModal.vue'
const route = useRoute()
const message = useMessage()
const MainID = Number(route.query.activityId)
const RoundType = Number(route.query.RoundType) || 0
const hasExtraTimeBtn = RoundType === 0
const showExtraTimeModal = ref(false)
const teamOptions = ref<{ label: string, value: number }[]>([])
function handleAddTime() {
showExtraTimeModal.value = true
}
// 定义数据结构
interface TeamData {
@ -46,7 +58,7 @@ const columns = ref<DataTableColumns<TeamData>>([
const tableData = ref<TeamData[]>([])
async function getUserRankList() {
const { data, error } = await fetchUserRankList(MainID)
const { data, error } = await fetchUserRankList(MainID, RoundType)
if (error) {
message.error(error.message || '获取排行榜失败')
return
@ -101,6 +113,8 @@ async function getUserRankList() {
...questions,
} as TeamData
})
teamOptions.value = data.data.map(item => ({ label: item.Name, value: item.TeamID }))
}
}
@ -129,6 +143,7 @@ function rowClassName(row: TeamData) {
<p class="mt--20px text-center text-sm text-#6b778d">
"勤学如春起之苗,不见其增,日有所长"
</p>
<ActionButton v-if="hasExtraTimeBtn" class="position-absolute bottom-20px right-20px" type="text" text="加时赛" @click="handleAddTime" />
</div>
<div class="leaderboard-content">
<NDataTable
@ -137,6 +152,7 @@ function rowClassName(row: TeamData) {
/>
</div>
</div>
<ExtraTimeModal v-model:show="showExtraTimeModal" :activity-id="MainID" :team-options="teamOptions" />
</CompetitionLayout>
</template>

View File

@ -14,13 +14,14 @@ const activityId = computed(() => activityInfoStore.activityId || route.query?.a
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
const teamIds = computed(() => route.query?.teamIds as string)
const RoundType = computed(() => Number(route.query?.RoundType || 0))
const { routerPushByKey, routerBack } = useRouterPush()
const activityName = ref('规则介绍')
const rulesContent = ref('')
const mockContent = computed(() => activityInfo.value?.ActivityContent || '')
const mockContent = computed(() => RoundType.value === 0 ? activityInfo.value?.ActivityContent || '' : activityInfo.value?.ExtraTimeContent || '')
onMounted(() => {
setTimeout(() => {
@ -40,6 +41,7 @@ function handleNext() {
groupsId: groupsId.value,
teamId: teamId.value,
teamIds: teamIds.value,
RoundType: RoundType.value,
},
})
}

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
@ -20,6 +20,8 @@ const activityId = (route.query?.activityId) as string || ''
const groupsId = (route.query?.groupsId) as string || ''
const teamIds = ref<number[]>([])
const RoundType = computed(() => Number(route.query?.RoundType || 0))
async function getTeams() {
if (!groupsId)
return
@ -61,7 +63,7 @@ function handleNext() {
function selectTeam(_id: number) {
// 选择队伍逻辑
routerPushByKey('user_cover', { query: { activityId, groupsId, teamId: _id, teamIds: teamIds.value.join(',') } })
routerPushByKey('user_cover', { query: { activityId, groupsId, teamId: _id, teamIds: teamIds.value.join(','), RoundType: RoundType.value } })
}
onMounted(() => {