From 11fb5481d34a4ad7713a0c6a91d52ce6e3ce614b Mon Sep 17 00:00:00 2001 From: rjl <98n9@163.com> Date: Mon, 9 Mar 2026 17:34:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(competition):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=86=B3=E8=B5=9B=E7=8E=AF=E8=8A=82=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8A=A0=E6=97=B6=E8=B5=9B=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展竞赛环节类型,支持决赛环节(RoundType=3) - 新增加时赛创建功能,支持多分组队伍选择 - 添加题目库禁用/启用功能,支持批量操作 - 优化用户端竞赛流程,区分常规赛和加时赛 - 重构操作按钮组件,提高代码复用性 --- .../components/custom/user/ActionButton.vue | 114 ++++++++++++++ .../custom/user/CompetitionLayout.vue | 149 ++++++------------ apps/admin/src/hooks/common/router.ts | 10 +- apps/admin/src/service/api/competition.ts | 4 +- apps/admin/src/service/api/game.ts | 13 +- apps/admin/src/service/api/question.ts | 10 ++ apps/admin/src/typings/api/Competition.d.ts | 16 +- apps/admin/src/typings/api/question.d.ts | 10 ++ apps/admin/src/typings/components.d.ts | 2 + .../competition/competition-detail/index.vue | 2 +- .../modules/QuestionConfigCard.vue | 10 +- .../question-store/modules/QuestionList.vue | 77 +++++++-- apps/admin/src/views/rank/rank-list/index.vue | 2 +- .../views/results/results-detail/index.vue | 4 +- apps/admin/src/views/user/analysis/index.vue | 3 +- apps/admin/src/views/user/cover/index.vue | 5 +- apps/admin/src/views/user/draw/index.vue | 4 +- apps/admin/src/views/user/groups/index.vue | 26 ++- .../rank-list/components/ExtraTimeModal.vue | 121 ++++++++++++++ apps/admin/src/views/user/rank-list/index.vue | 18 ++- apps/admin/src/views/user/rules/index.vue | 4 +- apps/admin/src/views/user/teams/index.vue | 6 +- 22 files changed, 468 insertions(+), 142 deletions(-) create mode 100644 apps/admin/src/components/custom/user/ActionButton.vue create mode 100644 apps/admin/src/views/user/rank-list/components/ExtraTimeModal.vue diff --git a/apps/admin/src/components/custom/user/ActionButton.vue b/apps/admin/src/components/custom/user/ActionButton.vue new file mode 100644 index 0000000..0fcf5f7 --- /dev/null +++ b/apps/admin/src/components/custom/user/ActionButton.vue @@ -0,0 +1,114 @@ + + + + + {{ text }} + + + + + diff --git a/apps/admin/src/components/custom/user/CompetitionLayout.vue b/apps/admin/src/components/custom/user/CompetitionLayout.vue index 858b8d4..e44db87 100644 --- a/apps/admin/src/components/custom/user/CompetitionLayout.vue +++ b/apps/admin/src/components/custom/user/CompetitionLayout.vue @@ -6,6 +6,8 @@ import titleBg2 from '@/assets/imgs/user/title-bg2.svg' import _defaultTitleBg from '@/assets/imgs/user/title-bg.svg' import { useRouterPush } from '@/hooks/common/router' +import ActionButton from './ActionButton.vue' + interface Props { /** 是否显示返回按钮 */ showBack?: boolean @@ -29,6 +31,10 @@ interface Props { nextDisabled?: boolean /** 标题区域背景图片URL,不传则使用默认 title-bg.svg */ titleBgType?: number + /** 是否显示第二个下一步按钮 */ + showNextBtn2?: boolean + /** 第二个按钮的文本 */ + nextBtnText2?: string } const props = withDefaults(defineProps(), { @@ -43,11 +49,14 @@ const props = withDefaults(defineProps(), { btnTheme: 'primary', nextDisabled: false, titleBgType: 1, + showNextBtn2: false, + nextBtnText2: undefined, }) const emit = defineEmits<{ (e: 'back'): void (e: 'next'): void + (e: 'next2'): void }>() const titleBgUrlMapping: Record = { @@ -89,6 +98,10 @@ function handleNext() { emit('next') } +function handleNext2() { + emit('next2') +} + /** 点击返回首页 */ function handleBackHome() { routerPushByKey('user_home') @@ -139,21 +152,30 @@ onMounted(() => { class="page-footer w-full flex items-center justify-between px-20" :class="[actionPosition, btnTheme]" > - - {{ backBtnText }} - - + + + + + - - {{ nextBtnText || '下一步' }} - - + + + + + + + + + @@ -280,102 +302,21 @@ onMounted(() => { .page-footer { position: absolute; bottom: 40px; - right: 0; // 默认在右下角 + width: 100%; + display: flex; + justify-content: space-between; + padding: 0 20px; z-index: 20; + .right { + display: flex; + gap: 10px; + } + &.top { bottom: auto; top: 40px; } - - .action-btn { - width: 50px; - height: 50px; - border-radius: 50%; - border: none; - color: white; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.1s ease; - background: #eb5e55; - box-shadow: 0 8px 0 #fcc64f; - - .arrow-icon { - font-size: 32px; - } - - &:hover { - transform: translateY(-2px); - filter: brightness(1.05); - } - - &:active { - transform: translateY(4px); - box-shadow: 0 2px 0 #fcc64f; - } - - // 文字按钮通用样式 - &.text-btn { - width: auto; - min-width: 180px; - padding: 0 30px; - border-radius: 40px; - - .btn-text { - font-size: 24px; - font-weight: bold; - letter-spacing: 2px; - } - } - - &.next-btn { - // 保留原有 next-btn 特定逻辑(如果有) - } - - &.back-btn { - // 保持统一风格 - } - - // 禁用状态 - &.is-disabled { - cursor: not-allowed; - // opacity: 0.8; // 移除透明度变化 - // filter: grayscale(0.5); // 移除置灰效果 - // box-shadow: none; // 保持阴影,或者根据需要调整 - - &:hover { - transform: none; // 禁止 hover 位移 - // filter: grayscale(0.5); - } - - &:active { - transform: none; - // box-shadow: none; // 保持阴影 - } - } - } - - // Light Theme Style (White bg, Red text/border, Yellow shadow) - &.light { - .action-btn { - background: white; - color: #eb5e55; - border: 2px solid #eb5e55; - box-shadow: 0 8px 0 #fcc64f; // 还原黄色立体阴影 - - &:hover { - background-color: #fff; // 保持白色 - transform: translateY(-2px); - } - - &:active { - transform: translateY(4px); - box-shadow: 0 2px 0 #fcc64f; - } - } - } } // Transitions diff --git a/apps/admin/src/hooks/common/router.ts b/apps/admin/src/hooks/common/router.ts index 896b677..adda492 100644 --- a/apps/admin/src/hooks/common/router.ts +++ b/apps/admin/src/hooks/common/router.ts @@ -25,7 +25,7 @@ export function useRouterPush(inSetup = true) { * @param key 路由名称 * @param options 路由参数 */ - async function routerPushByKey(key: RouteKey, options?: App.Global.RouterPushOptions) { + async function routerPushByKey(key: RouteKey, options?: App.Global.RouterPushOptions, newWindow?: boolean) { const { query, params } = options || {} const routeLocation: RouteLocationRaw = { @@ -40,7 +40,13 @@ export function useRouterPush(inSetup = true) { routeLocation.params = params } - return routerPush(routeLocation) + if (newWindow) { + const routeData = router.resolve(routeLocation) + window.open(routeData.href, '_blank') + } + else { + return routerPush(routeLocation) + } } /** diff --git a/apps/admin/src/service/api/competition.ts b/apps/admin/src/service/api/competition.ts index 042aa9d..3002904 100644 --- a/apps/admin/src/service/api/competition.ts +++ b/apps/admin/src/service/api/competition.ts @@ -58,9 +58,9 @@ export function fetchCreateQuestion(data: Api.Competition.CreateQuestionRequest[ } /** 根据活动ID获取活动题目列表 */ -export function fetchGetQuestionList(id: number) { +export function fetchGetQuestionList(id: number, roundType = 0) { return request>({ - url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}`, + url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}&RoundType=${roundType}`, method: 'get', }) } diff --git a/apps/admin/src/service/api/game.ts b/apps/admin/src/service/api/game.ts index 776e100..d894b5a 100644 --- a/apps/admin/src/service/api/game.ts +++ b/apps/admin/src/service/api/game.ts @@ -78,9 +78,9 @@ export function fetchCheckNextStep(TeamGroup_QuestionID: number) { } /** 根据活动id查询题目列表 */ -export function fetchGetQuestionTableByActivityID(ActivityID: string) { +export function fetchGetQuestionTableByActivityID(ActivityID: string, RoundType: number = 0) { return request>({ - url: `/Base/ActivityMain/GetActivity_QuestionTableByActivityID/?ID=${ActivityID}`, + url: `/Base/ActivityMain/GetActivity_QuestionTableByActivityID/?ID=${ActivityID}&RoundType=${RoundType}`, method: 'get', }) } @@ -103,3 +103,12 @@ export function fetchGetQuestionDetailByID(QuestionDetailID: number) { method: 'get', }) } + +/** 加时赛选择组小队 */ +export function fetchAddExtraTime(data: Api.Competition.AddExtraTimeParams) { + return request>({ + url: `/Base/ActivityMain/AddOverTimeTeamGroup`, + method: 'post', + data, + }) +} diff --git a/apps/admin/src/service/api/question.ts b/apps/admin/src/service/api/question.ts index 779483e..5e394d1 100644 --- a/apps/admin/src/service/api/question.ts +++ b/apps/admin/src/service/api/question.ts @@ -72,10 +72,20 @@ export function fetchAddQuestionLibrary(params: Api.Question.AddQuestionLibraryP Type: params.type, IsGood: params.IsGood, ImageUrl: params.imageUrl, + IsDisabled: params.IsDisabled, }, }) } +/** 禁用/启用 一条问题库 */ +export function fetchDisableQuestionLibrary(params: Api.Question.DisableQuestionLibraryParams[]) { + return request({ + url: '/Base/ActivityMain/UpdateQuestionListDetailList', + method: 'post', + data: params, + }) +} + /** 更新一条问题库 */ export function fetchUpdateQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) { const formData = new FormData() diff --git a/apps/admin/src/typings/api/Competition.d.ts b/apps/admin/src/typings/api/Competition.d.ts index a05de65..81dcfac 100644 --- a/apps/admin/src/typings/api/Competition.d.ts +++ b/apps/admin/src/typings/api/Competition.d.ts @@ -52,7 +52,7 @@ declare namespace Api { type QuestionScoreType = 0 | 1 /** 固定分数 | 答题个数 */ /** competition round type */ - type CompetitionRoundType = 0 | 1 /** 题包环节 | 加时环节 */ + type CompetitionRoundType = 0 | 1 | 3 /** 题包环节 | 加时环节 | 决赛环节 */ /** * competition publish status @@ -351,6 +351,7 @@ declare namespace Api { id: number /** 分组名称(兼容字段) */ name: string + RoundType: number } interface CreateQuestionRequest { @@ -380,6 +381,19 @@ declare namespace Api { roundType: number } + interface AddExtraTimeParams extends Array {} + + interface ExtraTimeItem { + GroupName: string + ActivityID: number + TeamInfos: TeamInfo[] + } + + interface TeamInfo { + TeamID: number + TeamName: string + } + /** common record */ type CommonRecord = { /** record id */ diff --git a/apps/admin/src/typings/api/question.d.ts b/apps/admin/src/typings/api/question.d.ts index d812287..15a95ca 100644 --- a/apps/admin/src/typings/api/question.d.ts +++ b/apps/admin/src/typings/api/question.d.ts @@ -20,6 +20,14 @@ declare namespace Api { data: T[] } + /** disable question library params */ + interface DisableQuestionLibraryParams { + /** question library id */ + Id: number + /** is disabled 0: no 1: yes */ + IsDisabled: boolean + } + /** get question library list all params */ interface GetQuestionLibraryListAllParams { /** question id */ @@ -60,6 +68,8 @@ declare namespace Api { type: QuestionScoreType /** is priority 0: no 1: yes */ IsGood: number + /** is disabled 0: no 1: yes */ + IsDisabled: number } /** common search params of table */ diff --git a/apps/admin/src/typings/components.d.ts b/apps/admin/src/typings/components.d.ts index c55fc50..c8b51e4 100644 --- a/apps/admin/src/typings/components.d.ts +++ b/apps/admin/src/typings/components.d.ts @@ -12,6 +12,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + ActionButton: typeof import('./../components/custom/user/ActionButton.vue')['default'] AppProvider: typeof import('./../components/common/app-provider.vue')['default'] BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default'] ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] @@ -186,6 +187,7 @@ declare module 'vue' { // For TSX support declare global { + const ActionButton: typeof import('./../components/custom/user/ActionButton.vue')['default'] const AppProvider: typeof import('./../components/common/app-provider.vue')['default'] const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default'] const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] diff --git a/apps/admin/src/views/competition/competition-detail/index.vue b/apps/admin/src/views/competition/competition-detail/index.vue index 992e4be..a48b09c 100644 --- a/apps/admin/src/views/competition/competition-detail/index.vue +++ b/apps/admin/src/views/competition/competition-detail/index.vue @@ -103,7 +103,7 @@ async function fetchTeamListByGroupId(GroupID: number) { /** 获取活动题目列表 */ async function getQuestionList(id: number) { - const { data, error } = await fetchGetQuestionList(id) + const { data, error } = await fetchGetQuestionList(id, 3) if (!error) { questionsList.value = data?.data || [] // eslint-disable-next-line no-console diff --git a/apps/admin/src/views/competition/competition-detail/modules/QuestionConfigCard.vue b/apps/admin/src/views/competition/competition-detail/modules/QuestionConfigCard.vue index aa057b2..8f79be2 100644 --- a/apps/admin/src/views/competition/competition-detail/modules/QuestionConfigCard.vue +++ b/apps/admin/src/views/competition/competition-detail/modules/QuestionConfigCard.vue @@ -1,5 +1,5 @@ + + + + + + + + + removeGroup(index)"> + 删除 + + + + 新增分组 + + + 确定 + + + + + + + diff --git a/apps/admin/src/views/user/rank-list/index.vue b/apps/admin/src/views/user/rank-list/index.vue index fec5f0c..93eed5f 100644 --- a/apps/admin/src/views/user/rank-list/index.vue +++ b/apps/admin/src/views/user/rank-list/index.vue @@ -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>([ const tableData = ref([]) 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) { "勤学如春起之苗,不见其增,日有所长" + + diff --git a/apps/admin/src/views/user/rules/index.vue b/apps/admin/src/views/user/rules/index.vue index 63a32d3..3d139e9 100644 --- a/apps/admin/src/views/user/rules/index.vue +++ b/apps/admin/src/views/user/rules/index.vue @@ -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, }, }) } diff --git a/apps/admin/src/views/user/teams/index.vue b/apps/admin/src/views/user/teams/index.vue index 5f04f51..d037a21 100644 --- a/apps/admin/src/views/user/teams/index.vue +++ b/apps/admin/src/views/user/teams/index.vue @@ -1,5 +1,5 @@
"勤学如春起之苗,不见其增,日有所长"