feat(competition): 支持决赛环节并添加加时赛管理功能
- 扩展竞赛环节类型,支持决赛环节(RoundType=3) - 新增加时赛创建功能,支持多分组队伍选择 - 添加题目库禁用/启用功能,支持批量操作 - 优化用户端竞赛流程,区分常规赛和加时赛 - 重构操作按钮组件,提高代码复用性
This commit is contained in:
114
apps/admin/src/components/custom/user/ActionButton.vue
Normal file
114
apps/admin/src/components/custom/user/ActionButton.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: '下一步',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String as () => 'primary' | 'light',
|
||||||
|
default: 'primary',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String as () => 'button' | 'text',
|
||||||
|
default: 'button',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['click'])
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!props.disabled) {
|
||||||
|
emits('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
:class="[theme, { 'is-disabled': disabled, 'text-btn': type === 'text' }]"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<span v-if="type === 'text'" class="btn-text">{{ text }}</span>
|
||||||
|
<slot v-else />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -6,6 +6,8 @@ import titleBg2 from '@/assets/imgs/user/title-bg2.svg'
|
|||||||
import _defaultTitleBg from '@/assets/imgs/user/title-bg.svg'
|
import _defaultTitleBg from '@/assets/imgs/user/title-bg.svg'
|
||||||
import { useRouterPush } from '@/hooks/common/router'
|
import { useRouterPush } from '@/hooks/common/router'
|
||||||
|
|
||||||
|
import ActionButton from './ActionButton.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** 是否显示返回按钮 */
|
/** 是否显示返回按钮 */
|
||||||
showBack?: boolean
|
showBack?: boolean
|
||||||
@ -29,6 +31,10 @@ interface Props {
|
|||||||
nextDisabled?: boolean
|
nextDisabled?: boolean
|
||||||
/** 标题区域背景图片URL,不传则使用默认 title-bg.svg */
|
/** 标题区域背景图片URL,不传则使用默认 title-bg.svg */
|
||||||
titleBgType?: number
|
titleBgType?: number
|
||||||
|
/** 是否显示第二个下一步按钮 */
|
||||||
|
showNextBtn2?: boolean
|
||||||
|
/** 第二个按钮的文本 */
|
||||||
|
nextBtnText2?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@ -43,11 +49,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
btnTheme: 'primary',
|
btnTheme: 'primary',
|
||||||
nextDisabled: false,
|
nextDisabled: false,
|
||||||
titleBgType: 1,
|
titleBgType: 1,
|
||||||
|
showNextBtn2: false,
|
||||||
|
nextBtnText2: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'back'): void
|
(e: 'back'): void
|
||||||
(e: 'next'): void
|
(e: 'next'): void
|
||||||
|
(e: 'next2'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const titleBgUrlMapping: Record<number, string> = {
|
const titleBgUrlMapping: Record<number, string> = {
|
||||||
@ -89,6 +98,10 @@ function handleNext() {
|
|||||||
emit('next')
|
emit('next')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNext2() {
|
||||||
|
emit('next2')
|
||||||
|
}
|
||||||
|
|
||||||
/** 点击返回首页 */
|
/** 点击返回首页 */
|
||||||
function handleBackHome() {
|
function handleBackHome() {
|
||||||
routerPushByKey('user_home')
|
routerPushByKey('user_home')
|
||||||
@ -139,21 +152,30 @@ onMounted(() => {
|
|||||||
class="page-footer w-full flex items-center justify-between px-20"
|
class="page-footer w-full flex items-center justify-between px-20"
|
||||||
:class="[actionPosition, btnTheme]"
|
:class="[actionPosition, btnTheme]"
|
||||||
>
|
>
|
||||||
<button
|
<div class="left">
|
||||||
class="action-btn back-btn" :class="{ 'text-btn': backBtnText }"
|
<ActionButton
|
||||||
:style="{ visibility: showBack ? 'visible' : 'hidden' }" @click="handleBack"
|
class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }"
|
||||||
>
|
:type="backBtnText ? 'text' : 'button'" :text="backBtnText || ''" :theme="btnTheme" @click="handleBack"
|
||||||
<span v-if="backBtnText" class="btn-text">{{ backBtnText }}</span>
|
>
|
||||||
<SvgIcon v-else icon="mdi:arrow-left" class="arrow-icon" />
|
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" class="arrow-icon" />
|
||||||
</button>
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<div class="right">
|
||||||
v-if="showNext" class="action-btn next-btn text-btn" :class="{ 'is-disabled': nextDisabled }"
|
<ActionButton
|
||||||
@click="handleNext"
|
v-if="showNext" :type="nextBtnText ? 'text' : 'button'" :text="nextBtnText || '下一步'"
|
||||||
>
|
:theme="btnTheme" :disabled="nextDisabled" @click="handleNext"
|
||||||
<span class="btn-text">{{ nextBtnText || '下一步' }}</span>
|
>
|
||||||
<!-- <SvgIcon v-else icon="mdi:arrow-right" class="arrow-icon" /> -->
|
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" />
|
||||||
</button>
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
v-if="showNextBtn2" :type="nextBtnText2 ? 'text' : 'button'" :text="nextBtnText2 || '下一步'"
|
||||||
|
:theme="btnTheme" :disabled="nextDisabled" @click="handleNext2"
|
||||||
|
>
|
||||||
|
<SvgIcon v-if="!nextBtnText2" icon="mdi:arrow-right" class="arrow-icon" />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -280,102 +302,21 @@ onMounted(() => {
|
|||||||
.page-footer {
|
.page-footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
right: 0; // 默认在右下角
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&.top {
|
&.top {
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
top: 40px;
|
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
|
// Transitions
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export function useRouterPush(inSetup = true) {
|
|||||||
* @param key 路由名称
|
* @param key 路由名称
|
||||||
* @param options 路由参数
|
* @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 { query, params } = options || {}
|
||||||
|
|
||||||
const routeLocation: RouteLocationRaw = {
|
const routeLocation: RouteLocationRaw = {
|
||||||
@ -40,7 +40,13 @@ export function useRouterPush(inSetup = true) {
|
|||||||
routeLocation.params = params
|
routeLocation.params = params
|
||||||
}
|
}
|
||||||
|
|
||||||
return routerPush(routeLocation)
|
if (newWindow) {
|
||||||
|
const routeData = router.resolve(routeLocation)
|
||||||
|
window.open(routeData.href, '_blank')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return routerPush(routeLocation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -58,9 +58,9 @@ export function fetchCreateQuestion(data: Api.Competition.CreateQuestionRequest[
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 根据活动ID获取活动题目列表 */
|
/** 根据活动ID获取活动题目列表 */
|
||||||
export function fetchGetQuestionList(id: number) {
|
export function fetchGetQuestionList(id: number, roundType = 0) {
|
||||||
return request<App.Service.Response<Api.Competition.QuestionListRecord[]>>({
|
return request<App.Service.Response<Api.Competition.QuestionListRecord[]>>({
|
||||||
url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}`,
|
url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}&RoundType=${roundType}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,9 +78,9 @@ export function fetchCheckNextStep(TeamGroup_QuestionID: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 根据活动id查询题目列表 */
|
/** 根据活动id查询题目列表 */
|
||||||
export function fetchGetQuestionTableByActivityID(ActivityID: string) {
|
export function fetchGetQuestionTableByActivityID(ActivityID: string, RoundType: number = 0) {
|
||||||
return request<App.Service.Response<Api.Competition.QuestionListDetailRound[]>>({
|
return request<App.Service.Response<Api.Competition.QuestionListDetailRound[]>>({
|
||||||
url: `/Base/ActivityMain/GetActivity_QuestionTableByActivityID/?ID=${ActivityID}`,
|
url: `/Base/ActivityMain/GetActivity_QuestionTableByActivityID/?ID=${ActivityID}&RoundType=${RoundType}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -103,3 +103,12 @@ export function fetchGetQuestionDetailByID(QuestionDetailID: number) {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 加时赛选择组小队 */
|
||||||
|
export function fetchAddExtraTime(data: Api.Competition.AddExtraTimeParams) {
|
||||||
|
return request<App.Service.Response<any>>({
|
||||||
|
url: `/Base/ActivityMain/AddOverTimeTeamGroup`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -72,10 +72,20 @@ export function fetchAddQuestionLibrary(params: Api.Question.AddQuestionLibraryP
|
|||||||
Type: params.type,
|
Type: params.type,
|
||||||
IsGood: params.IsGood,
|
IsGood: params.IsGood,
|
||||||
ImageUrl: params.imageUrl,
|
ImageUrl: params.imageUrl,
|
||||||
|
IsDisabled: params.IsDisabled,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 禁用/启用 一条问题库 */
|
||||||
|
export function fetchDisableQuestionLibrary(params: Api.Question.DisableQuestionLibraryParams[]) {
|
||||||
|
return request<Api.Question.CommonRecord>({
|
||||||
|
url: '/Base/ActivityMain/UpdateQuestionListDetailList',
|
||||||
|
method: 'post',
|
||||||
|
data: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** 更新一条问题库 */
|
/** 更新一条问题库 */
|
||||||
export function fetchUpdateQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) {
|
export function fetchUpdateQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|||||||
16
apps/admin/src/typings/api/Competition.d.ts
vendored
16
apps/admin/src/typings/api/Competition.d.ts
vendored
@ -52,7 +52,7 @@ declare namespace Api {
|
|||||||
type QuestionScoreType = 0 | 1 /** 固定分数 | 答题个数 */
|
type QuestionScoreType = 0 | 1 /** 固定分数 | 答题个数 */
|
||||||
|
|
||||||
/** competition round type */
|
/** competition round type */
|
||||||
type CompetitionRoundType = 0 | 1 /** 题包环节 | 加时环节 */
|
type CompetitionRoundType = 0 | 1 | 3 /** 题包环节 | 加时环节 | 决赛环节 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* competition publish status
|
* competition publish status
|
||||||
@ -351,6 +351,7 @@ declare namespace Api {
|
|||||||
id: number
|
id: number
|
||||||
/** 分组名称(兼容字段) */
|
/** 分组名称(兼容字段) */
|
||||||
name: string
|
name: string
|
||||||
|
RoundType: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateQuestionRequest {
|
interface CreateQuestionRequest {
|
||||||
@ -380,6 +381,19 @@ declare namespace Api {
|
|||||||
roundType: number
|
roundType: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddExtraTimeParams extends Array<ExtraTimeItem> {}
|
||||||
|
|
||||||
|
interface ExtraTimeItem {
|
||||||
|
GroupName: string
|
||||||
|
ActivityID: number
|
||||||
|
TeamInfos: TeamInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamInfo {
|
||||||
|
TeamID: number
|
||||||
|
TeamName: string
|
||||||
|
}
|
||||||
|
|
||||||
/** common record */
|
/** common record */
|
||||||
type CommonRecord<T = any> = {
|
type CommonRecord<T = any> = {
|
||||||
/** record id */
|
/** record id */
|
||||||
|
|||||||
10
apps/admin/src/typings/api/question.d.ts
vendored
10
apps/admin/src/typings/api/question.d.ts
vendored
@ -20,6 +20,14 @@ declare namespace Api {
|
|||||||
data: T[]
|
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 */
|
/** get question library list all params */
|
||||||
interface GetQuestionLibraryListAllParams {
|
interface GetQuestionLibraryListAllParams {
|
||||||
/** question id */
|
/** question id */
|
||||||
@ -60,6 +68,8 @@ declare namespace Api {
|
|||||||
type: QuestionScoreType
|
type: QuestionScoreType
|
||||||
/** is priority 0: no 1: yes */
|
/** is priority 0: no 1: yes */
|
||||||
IsGood: number
|
IsGood: number
|
||||||
|
/** is disabled 0: no 1: yes */
|
||||||
|
IsDisabled: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** common search params of table */
|
/** common search params of table */
|
||||||
|
|||||||
2
apps/admin/src/typings/components.d.ts
vendored
2
apps/admin/src/typings/components.d.ts
vendored
@ -12,6 +12,7 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ActionButton: typeof import('./../components/custom/user/ActionButton.vue')['default']
|
||||||
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
|
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
|
||||||
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
||||||
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||||
@ -186,6 +187,7 @@ declare module 'vue' {
|
|||||||
|
|
||||||
// For TSX support
|
// For TSX support
|
||||||
declare global {
|
declare global {
|
||||||
|
const ActionButton: typeof import('./../components/custom/user/ActionButton.vue')['default']
|
||||||
const AppProvider: typeof import('./../components/common/app-provider.vue')['default']
|
const AppProvider: typeof import('./../components/common/app-provider.vue')['default']
|
||||||
const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
||||||
const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||||
|
|||||||
@ -103,7 +103,7 @@ async function fetchTeamListByGroupId(GroupID: number) {
|
|||||||
|
|
||||||
/** 获取活动题目列表 */
|
/** 获取活动题目列表 */
|
||||||
async function getQuestionList(id: number) {
|
async function getQuestionList(id: number) {
|
||||||
const { data, error } = await fetchGetQuestionList(id)
|
const { data, error } = await fetchGetQuestionList(id, 3)
|
||||||
if (!error) {
|
if (!error) {
|
||||||
questionsList.value = data?.data || []
|
questionsList.value = data?.data || []
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NButton, NCard, NForm, NFormItem, NInput, NInputNumber, NModal, NSelect, NTag, type SelectOption } from 'naive-ui'
|
import { NButton, NCard, NForm, NFormItem, NInputNumber, NModal, NSelect, NTag, type SelectOption } from 'naive-ui'
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { VueDraggable } from 'vue-draggable-plus'
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
import { PublishStatus } from '@/enum/business'
|
import { PublishStatus } from '@/enum/business'
|
||||||
@ -166,7 +166,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getRoundTypeName(roundType: string) {
|
function getRoundTypeName(roundType: number) {
|
||||||
const option = roundTypeOptions.value?.find((item: any) => item.value === roundType)
|
const option = roundTypeOptions.value?.find((item: any) => item.value === roundType)
|
||||||
return option?.label || '未知类型'
|
return option?.label || '未知类型'
|
||||||
}
|
}
|
||||||
@ -268,14 +268,14 @@ defineExpose({ validate, reset, modelValue })
|
|||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="h-8 w-1.5 rounded-full bg-blue-500" />
|
<div class="h-8 w-1.5 rounded-full bg-blue-500" />
|
||||||
<NTag type="primary" class="!text-blue-500">
|
<NTag type="primary" class="!text-blue-500">
|
||||||
{{ getRoundTypeName(round.roundType) }}
|
{{ getRoundTypeName(Number(round.roundType)) }}
|
||||||
</NTag>
|
</NTag>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<NInput
|
<!-- <NInput
|
||||||
v-model:value="round.name" placeholder="请输入环节名称" :bordered="false"
|
v-model:value="round.name" placeholder="请输入环节名称" :bordered="false"
|
||||||
:disabled="!isEdit"
|
:disabled="!isEdit"
|
||||||
class="border transition-all !w-80 !border-transparent !rounded-lg !bg-transparent !text-xl !font-bold focus-within:shadow-sm focus-within:!border-blue-500 hover:!border-gray-200 focus-within:!bg-white"
|
class="border transition-all !w-80 !border-transparent !rounded-lg !bg-transparent !text-xl !font-bold focus-within:shadow-sm focus-within:!border-blue-500 hover:!border-gray-200 focus-within:!bg-white"
|
||||||
/>
|
/> -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500 font-bold"
|
class="flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500 font-bold"
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'
|
|||||||
import WangEditor from '@/components/common/wang-editor.vue'
|
import WangEditor from '@/components/common/wang-editor.vue'
|
||||||
import SvgIcon from '@/components/custom/svg-icon.vue'
|
import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||||
import { QuestionCategoryEnum, QuestionType } from '@/enum/business'
|
import { QuestionCategoryEnum, QuestionType } from '@/enum/business'
|
||||||
import { fetchAddQuestionLibrary, fetchDeleteQuestionLibrary, fetchGetQuestionLibraryListAll, fetchUpdateQuestionLibrary } from '@/service/api/question'
|
import { fetchAddQuestionLibrary, fetchDeleteQuestionLibrary, fetchDisableQuestionLibrary, fetchGetQuestionLibraryListAll, fetchUpdateQuestionLibrary } from '@/service/api/question'
|
||||||
/**
|
/**
|
||||||
* 为了user端题目展示的效果以及布局可控,指定规则:
|
* 为了user端题目展示的效果以及布局可控,指定规则:
|
||||||
* 1.普通类型 =》用存文字存 ['汉字加一加','词语听写','汉字听写','成语写一写']
|
* 1.普通类型 =》用存文字存 ['汉字加一加','词语听写','汉字听写','成语写一写']
|
||||||
@ -113,6 +113,7 @@ const questionForm = ref({
|
|||||||
type: QuestionType.SingleChoice as QuestionType | number, // 题目类型
|
type: QuestionType.SingleChoice as QuestionType | number, // 题目类型
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
IsGood: 0,
|
IsGood: 0,
|
||||||
|
IsDisabled: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileList = ref<UploadFileInfo[]>([])
|
const fileList = ref<UploadFileInfo[]>([])
|
||||||
@ -177,6 +178,30 @@ function handleBatchDelete() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleBatchDisable(disabled: boolean) {
|
||||||
|
if (selectedQuestionIds.value.length === 0) {
|
||||||
|
window?.$message?.warning('请先选择要操作的题目')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = selectedQuestionIds.value.map(id => ({
|
||||||
|
Id: id,
|
||||||
|
IsDisabled: disabled,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { error } = await fetchDisableQuestionLibrary(payload)
|
||||||
|
if (!error) {
|
||||||
|
window?.$message?.success(`批量${disabled ? '禁用' : '启用'}成功`)
|
||||||
|
selectedQuestionIds.value = []
|
||||||
|
if (props.currentCategory?.Id) {
|
||||||
|
fetchData(props.currentCategory.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window?.$message?.error(`批量${disabled ? '禁用' : '启用'}失败`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
name: [{ required: true, message: '请输入题目正文内容', trigger: ['blur'] }],
|
name: [{ required: true, message: '请输入题目正文内容', trigger: ['blur'] }],
|
||||||
answer: [{ required: true, message: '请输入题目答案', trigger: ['blur'] }],
|
answer: [{ required: true, message: '请输入题目答案', trigger: ['blur'] }],
|
||||||
@ -241,6 +266,7 @@ function handleAddQuestion() {
|
|||||||
type: props.currentCategory?.QuestionType || QuestionType.SingleChoice,
|
type: props.currentCategory?.QuestionType || QuestionType.SingleChoice,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
IsGood: 0,
|
IsGood: 0,
|
||||||
|
IsDisabled: 0,
|
||||||
}
|
}
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
showQuestionModal.value = true
|
showQuestionModal.value = true
|
||||||
@ -265,6 +291,7 @@ function handleEditQuestion(id: number) {
|
|||||||
type: question.Type,
|
type: question.Type,
|
||||||
imageUrl: question.ImageUrl || '',
|
imageUrl: question.ImageUrl || '',
|
||||||
IsGood: question.IsPriority || 0,
|
IsGood: question.IsPriority || 0,
|
||||||
|
IsDisabled: question.IsDisabled || 0,
|
||||||
}
|
}
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
if (question.ImageUrl) {
|
if (question.ImageUrl) {
|
||||||
@ -317,6 +344,7 @@ async function submitQuestion() {
|
|||||||
type: questionForm.value.type as any,
|
type: questionForm.value.type as any,
|
||||||
questionId: props.currentCategory.Id,
|
questionId: props.currentCategory.Id,
|
||||||
IsGood: questionForm.value.IsGood,
|
IsGood: questionForm.value.IsGood,
|
||||||
|
IsDisabled: questionForm.value.IsDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileToUpload: File | null = null
|
let fileToUpload: File | null = null
|
||||||
@ -385,11 +413,7 @@ async function submitQuestion() {
|
|||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div v-if="currentCategory" class="mt-2 flex items-center justify-between">
|
<div v-if="currentCategory" class="mt-2 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<NCheckbox
|
<NCheckbox :checked="isAllSelected" :indeterminate="isIndeterminate" @update:checked="handleSelectAll">
|
||||||
:checked="isAllSelected"
|
|
||||||
:indeterminate="isIndeterminate"
|
|
||||||
@update:checked="handleSelectAll"
|
|
||||||
>
|
|
||||||
全选 ({{ selectedQuestionIds.length }})
|
全选 ({{ selectedQuestionIds.length }})
|
||||||
</NCheckbox>
|
</NCheckbox>
|
||||||
<NInput v-model:value="searchText" placeholder="输入题目关键字在当前分类下搜索..." class="!w-80" @keyup.enter="handleSearch">
|
<NInput v-model:value="searchText" placeholder="输入题目关键字在当前分类下搜索..." class="!w-80" @keyup.enter="handleSearch">
|
||||||
@ -400,6 +424,24 @@ async function submitQuestion() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
|
<NButton
|
||||||
|
v-if="selectedQuestionIds.length > 0" type="warning" secondary
|
||||||
|
@click="() => handleBatchDisable(true)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon icon="carbon:view-off" />
|
||||||
|
</template>
|
||||||
|
批量禁用
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
v-if="selectedQuestionIds.length > 0" type="success" secondary
|
||||||
|
@click="() => handleBatchDisable(false)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon icon="carbon:view" />
|
||||||
|
</template>
|
||||||
|
批量启用
|
||||||
|
</NButton>
|
||||||
<NButton v-if="selectedQuestionIds.length > 0" type="error" secondary @click="handleBatchDelete">
|
<NButton v-if="selectedQuestionIds.length > 0" type="error" secondary @click="handleBatchDelete">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon icon="carbon:trash-can" />
|
<SvgIcon icon="carbon:trash-can" />
|
||||||
@ -440,8 +482,8 @@ async function submitQuestion() {
|
|||||||
<NCard
|
<NCard
|
||||||
v-for="q in questionList" :key="q.Id" size="small" hoverable class="rounded-xl transition-all" :class="{
|
v-for="q in questionList" :key="q.Id" size="small" hoverable class="rounded-xl transition-all" :class="{
|
||||||
'ring-2 ring-primary-500 bg-primary-50': selectedQuestionIds.includes(q.Id),
|
'ring-2 ring-primary-500 bg-primary-50': selectedQuestionIds.includes(q.Id),
|
||||||
}"
|
'opacity-50 grayscale': q.IsDisabled === 1,
|
||||||
@click="handleSelect(q.Id, !selectedQuestionIds.includes(q.Id))"
|
}" @click="handleSelect(q.Id, !selectedQuestionIds.includes(q.Id))"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center gap-2" @click.stop>
|
<div class="flex items-center gap-2" @click.stop>
|
||||||
@ -455,6 +497,9 @@ async function submitQuestion() {
|
|||||||
<NTag size="small" :type="q.IsGood === 1 ? 'info' : 'default'" :bordered="false">
|
<NTag size="small" :type="q.IsGood === 1 ? 'info' : 'default'" :bordered="false">
|
||||||
{{ q.IsGood === 1 ? '优先' : '随机' }}
|
{{ q.IsGood === 1 ? '优先' : '随机' }}
|
||||||
</NTag>
|
</NTag>
|
||||||
|
<NTag v-if="q.IsDisabled" size="small" type="error" :bordered="false">
|
||||||
|
已禁用
|
||||||
|
</NTag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
@ -531,7 +576,10 @@ async function submitQuestion() {
|
|||||||
|
|
||||||
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
|
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
|
||||||
<NFormItem label="题目正文内容" path="name">
|
<NFormItem label="题目正文内容" path="name">
|
||||||
<div v-if="[QuestionCategoryEnum.PoetryComprehension].includes(props.currentCategory?.QuestionType)" class="w-full overflow-hidden border border-gray-200 rounded-lg">
|
<div
|
||||||
|
v-if="[QuestionCategoryEnum.PoetryComprehension].includes(props.currentCategory?.QuestionType)"
|
||||||
|
class="w-full overflow-hidden border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
<WangEditor
|
<WangEditor
|
||||||
v-model="questionForm.name" placeholder="请输入题目内容..." height="300px"
|
v-model="questionForm.name" placeholder="请输入题目内容..." height="300px"
|
||||||
:exclude-keys="['header1', 'fontSize', 'fontFamily', 'lineHeight', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'group-image', 'group-video', 'insertTable', 'headerSelect']"
|
:exclude-keys="['header1', 'fontSize', 'fontFamily', 'lineHeight', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'group-image', 'group-video', 'insertTable', 'headerSelect']"
|
||||||
@ -565,6 +613,17 @@ async function submitQuestion() {
|
|||||||
</NRadio>
|
</NRadio>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
<!-- 是否禁用 -->
|
||||||
|
<NFormItem label="是否禁用" path="IsDisabled">
|
||||||
|
<NRadioGroup v-model:value="questionForm.IsDisabled">
|
||||||
|
<NRadio :value="0">
|
||||||
|
否
|
||||||
|
</NRadio>
|
||||||
|
<NRadio :value="1">
|
||||||
|
是
|
||||||
|
</NRadio>
|
||||||
|
</NRadioGroup>
|
||||||
|
</NFormItem>
|
||||||
<!-- 上传图片 -->
|
<!-- 上传图片 -->
|
||||||
<NFormItem v-if="questionForm.type === QuestionType.Image" label="上传图片">
|
<NFormItem v-if="questionForm.type === QuestionType.Image" label="上传图片">
|
||||||
<NUpload
|
<NUpload
|
||||||
|
|||||||
@ -150,7 +150,7 @@ function edit(item: any) {
|
|||||||
<NCard :bordered="false" page-size="small" class="card-wrapper sm:flex-1-hidden">
|
<NCard :bordered="false" page-size="small" class="card-wrapper sm:flex-1-hidden">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div class="mid-title">
|
||||||
<div class="text-16px font-bold">
|
<div class="text-16px font-bold">
|
||||||
{{ rankTitle }}
|
{{ rankTitle }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -186,7 +186,7 @@ async function fetchGroups() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { data, error } = await fetchGetGroupListByActivityID(activityId.value)
|
const { data, error } = await fetchGetGroupListByActivityID(activityId.value, 3)
|
||||||
if (error) {
|
if (error) {
|
||||||
window.$message?.error('获取题目失败')
|
window.$message?.error('获取题目失败')
|
||||||
return
|
return
|
||||||
@ -213,7 +213,7 @@ async function fetchQuestions() {
|
|||||||
if (!activityId.value)
|
if (!activityId.value)
|
||||||
return
|
return
|
||||||
try {
|
try {
|
||||||
const { data, error } = await fetchGetQuestionList(Number(activityId.value))
|
const { data, error } = await fetchGetQuestionList(Number(activityId.value), 3)
|
||||||
if (error) {
|
if (error) {
|
||||||
message.error(error.message)
|
message.error(error.message)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -55,6 +55,7 @@ function handleNext() {
|
|||||||
groupsId: groupsId.value,
|
groupsId: groupsId.value,
|
||||||
teamId: teamId.value,
|
teamId: teamId.value,
|
||||||
teamIds: teamIds.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({
|
const { data: outlineData, error } = await fetchGetCurrentQuestion({
|
||||||
ActivityID: Number(activityId.value),
|
ActivityID: Number(activityId.value),
|
||||||
GroupID: Number(groupsId.value),
|
GroupID: Number(groupsId.value),
|
||||||
RoundType: 0,
|
RoundType: Number(currentQuestionMainInfo.value?.Activity_Question.RoundType) || 0,
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(outlineData, 'outlineData')
|
console.log(outlineData, 'outlineData')
|
||||||
|
|||||||
@ -45,6 +45,8 @@ const groupsId = computed(() => Number(route.query?.groupsId)) || activityInfoSt
|
|||||||
const teamId = computed(() => activityInfoStore.teamId || Number(route.query?.teamId))
|
const teamId = computed(() => activityInfoStore.teamId || Number(route.query?.teamId))
|
||||||
const teamIds = computed(() => route.query?.teamIds as string)
|
const teamIds = computed(() => route.query?.teamIds as string)
|
||||||
|
|
||||||
|
const RoundType = route.query?.RoundType || 0
|
||||||
|
|
||||||
async function getQuestions() {
|
async function getQuestions() {
|
||||||
if (!activityId.value || !groupsId.value || !teamId.value) {
|
if (!activityId.value || !groupsId.value || !teamId.value) {
|
||||||
window?.$message?.error('参数错误')
|
window?.$message?.error('参数错误')
|
||||||
@ -53,7 +55,7 @@ async function getQuestions() {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const { data, error } = await fetchGetQuestionTableByActivityID(String(activityId.value))
|
const { data, error } = await fetchGetQuestionTableByActivityID(String(activityId.value), Number(RoundType))
|
||||||
if (error) {
|
if (error) {
|
||||||
window?.$message?.error(error.message)
|
window?.$message?.error(error.message)
|
||||||
return
|
return
|
||||||
@ -115,6 +117,7 @@ async function handleNext() {
|
|||||||
groupsId: groupsId.value,
|
groupsId: groupsId.value,
|
||||||
teamId: teamId.value,
|
teamId: teamId.value,
|
||||||
teamIds: teamIds.value,
|
teamIds: teamIds.value,
|
||||||
|
RoundType: Number(RoundType),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const activityId = computed(() => activityInfoStore.activityId || route.query?.a
|
|||||||
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
|
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
|
||||||
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
|
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
|
||||||
const teamIds = computed(() => route.query?.teamIds 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()
|
const store = useCompetitionStore()
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ async function initQuestions() {
|
|||||||
const { data: outlineData, error } = await fetchGetCurrentQuestion({
|
const { data: outlineData, error } = await fetchGetCurrentQuestion({
|
||||||
ActivityID: Number(activityId.value),
|
ActivityID: Number(activityId.value),
|
||||||
GroupID: Number(groupsId.value),
|
GroupID: Number(groupsId.value),
|
||||||
RoundType: 0,
|
RoundType: RoundType.value,
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(outlineData, 'outlineData')
|
console.log(outlineData, 'outlineData')
|
||||||
@ -78,6 +79,7 @@ async function handleCardClick(item: Api.Competition.CurrentQuestionResponse) {
|
|||||||
routerPushByKey('user_groups', {
|
routerPushByKey('user_groups', {
|
||||||
query: {
|
query: {
|
||||||
activityId: activityId.value,
|
activityId: activityId.value,
|
||||||
|
RoundType: RoundType.value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||||
import { useRouterPush } from '@/hooks/common/router'
|
import { useRouterPush } from '@/hooks/common/router'
|
||||||
@ -13,6 +13,12 @@ const loading = ref(false)
|
|||||||
const activityId = route.query.activityId as string
|
const activityId = route.query.activityId as string
|
||||||
const isAllEnd = ref(false)
|
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() {
|
async function getGroups() {
|
||||||
if (!activityId)
|
if (!activityId)
|
||||||
return
|
return
|
||||||
@ -32,7 +38,7 @@ async function getGroups() {
|
|||||||
icon: 'activity',
|
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 {
|
finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -46,7 +52,7 @@ function handleBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNext() {
|
function handleNext() {
|
||||||
routerPushByKey('user_rank-list', { query: { activityId } })
|
routerPushByKey('user_rank-list', { query: { activityId, RoundType: 0 } })
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectGroup(item: Api.Competition.ActivityTeamGroup) {
|
function selectGroup(item: Api.Competition.ActivityTeamGroup) {
|
||||||
@ -54,7 +60,14 @@ function selectGroup(item: Api.Competition.ActivityTeamGroup) {
|
|||||||
return
|
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(() => {
|
onMounted(() => {
|
||||||
@ -67,8 +80,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CompetitionLayout
|
<CompetitionLayout
|
||||||
:show-back="true" :show-next="isAllEnd" next-btn-text="查看队伍结果" :title-bg-type="2"
|
:show-back="true" :show-next="isAllEnd" next-btn-text="查看常规赛结果" next-btn-text2="查看加时赛结果"
|
||||||
title="展示组别" @back="handleBack" @next="handleNext"
|
:show-next-btn2="hasExtraTime && isAllEnd" :title-bg-type="2" title="展示组别" @back="handleBack" @next="handleNext"
|
||||||
|
@next2="handleNext2"
|
||||||
>
|
>
|
||||||
<!-- Groups Grid -->
|
<!-- Groups Grid -->
|
||||||
<div class="groups-container">
|
<div class="groups-container">
|
||||||
|
|||||||
@ -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>
|
||||||
@ -4,11 +4,23 @@ import { NDataTable, NImage, NTag, useMessage } from 'naive-ui'
|
|||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import title from '@/assets/imgs/user/rank-title.svg'
|
import title from '@/assets/imgs/user/rank-title.svg'
|
||||||
|
import ActionButton from '@/components/custom/user/ActionButton.vue'
|
||||||
import { fetchUserRankList } from '@/service/api/rank'
|
import { fetchUserRankList } from '@/service/api/rank'
|
||||||
|
import ExtraTimeModal from './components/ExtraTimeModal.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const MainID = Number(route.query.activityId)
|
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 {
|
interface TeamData {
|
||||||
@ -46,7 +58,7 @@ const columns = ref<DataTableColumns<TeamData>>([
|
|||||||
const tableData = ref<TeamData[]>([])
|
const tableData = ref<TeamData[]>([])
|
||||||
|
|
||||||
async function getUserRankList() {
|
async function getUserRankList() {
|
||||||
const { data, error } = await fetchUserRankList(MainID)
|
const { data, error } = await fetchUserRankList(MainID, RoundType)
|
||||||
if (error) {
|
if (error) {
|
||||||
message.error(error.message || '获取排行榜失败')
|
message.error(error.message || '获取排行榜失败')
|
||||||
return
|
return
|
||||||
@ -101,6 +113,8 @@ async function getUserRankList() {
|
|||||||
...questions,
|
...questions,
|
||||||
} as TeamData
|
} 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 class="mt--20px text-center text-sm text-#6b778d">
|
||||||
"勤学如春起之苗,不见其增,日有所长"
|
"勤学如春起之苗,不见其增,日有所长"
|
||||||
</p>
|
</p>
|
||||||
|
<ActionButton v-if="hasExtraTimeBtn" class="position-absolute bottom-20px right-20px" type="text" text="加时赛" @click="handleAddTime" />
|
||||||
</div>
|
</div>
|
||||||
<div class="leaderboard-content">
|
<div class="leaderboard-content">
|
||||||
<NDataTable
|
<NDataTable
|
||||||
@ -137,6 +152,7 @@ function rowClassName(row: TeamData) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ExtraTimeModal v-model:show="showExtraTimeModal" :activity-id="MainID" :team-options="teamOptions" />
|
||||||
</CompetitionLayout>
|
</CompetitionLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -14,13 +14,14 @@ const activityId = computed(() => activityInfoStore.activityId || route.query?.a
|
|||||||
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
|
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
|
||||||
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
|
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
|
||||||
const teamIds = computed(() => route.query?.teamIds as string)
|
const teamIds = computed(() => route.query?.teamIds as string)
|
||||||
|
const RoundType = computed(() => Number(route.query?.RoundType || 0))
|
||||||
|
|
||||||
const { routerPushByKey, routerBack } = useRouterPush()
|
const { routerPushByKey, routerBack } = useRouterPush()
|
||||||
|
|
||||||
const activityName = ref('规则介绍')
|
const activityName = ref('规则介绍')
|
||||||
const rulesContent = ref('')
|
const rulesContent = ref('')
|
||||||
|
|
||||||
const mockContent = computed(() => activityInfo.value?.ActivityContent || '')
|
const mockContent = computed(() => RoundType.value === 0 ? activityInfo.value?.ActivityContent || '' : activityInfo.value?.ExtraTimeContent || '')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -40,6 +41,7 @@ function handleNext() {
|
|||||||
groupsId: groupsId.value,
|
groupsId: groupsId.value,
|
||||||
teamId: teamId.value,
|
teamId: teamId.value,
|
||||||
teamIds: teamIds.value,
|
teamIds: teamIds.value,
|
||||||
|
RoundType: RoundType.value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||||
import { useRouterPush } from '@/hooks/common/router'
|
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 groupsId = (route.query?.groupsId) as string || ''
|
||||||
const teamIds = ref<number[]>([])
|
const teamIds = ref<number[]>([])
|
||||||
|
|
||||||
|
const RoundType = computed(() => Number(route.query?.RoundType || 0))
|
||||||
|
|
||||||
async function getTeams() {
|
async function getTeams() {
|
||||||
if (!groupsId)
|
if (!groupsId)
|
||||||
return
|
return
|
||||||
@ -61,7 +63,7 @@ function handleNext() {
|
|||||||
|
|
||||||
function selectTeam(_id: number) {
|
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(() => {
|
onMounted(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user