Files
reader-star/apps/admin/src/views/competition/competition-add/index.vue
rjl 264f600250 feat(user): 新增用户端答题流程页面与相关功能
- 新增用户端答题流程主页面,包含封面、介绍、答题、答题图解四个状态
- 新增用户端组件:封面、介绍页、答题页、答题图解页、题目渲染器
- 新增用户端类型定义、模拟数据及业务常量
- 新增用户路由及类型声明
- 优化题库分类树,改为扁平化列表结构
- 完善比赛创建功能,增加轮次类型、题目分数类型及关联AP字段
- 修复模板删除函数调用参数错误
2026-01-23 18:16:03 +08:00

216 lines
6.6 KiB
Vue

<script setup lang="ts">
import { NButton, NModal, NStep, NSteps } from 'naive-ui'
import { computed, ref, useTemplateRef, watch } from 'vue'
import { roundTypeOptions, scoreTypeOptions } from '@/constants/business'
import BasicInfo from './modules/BasicInfo.vue'
import GroupManagement from './modules/GroupManagement.vue'
// import HardwareBinding from './modules/HardwareBinding.vue'
import QuestionConfig from './modules/QuestionConfig.vue'
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: 'update:show', visible: boolean): void
(e: 'success'): void
}>()
const currentStep = ref<number>(1)
// Reset step when modal opens
watch(() => props.show, (val) => {
if (val) {
currentStep.value = 1
}
})
const competitionData = ref({
name: '阅读之星年度总决赛', // 竞赛名称
startTime: null, // 竞赛开始时间
endTime: null, // 竞赛结束时间
groupCount: 10, // 组别数量
teamCount: 4, // 每组队伍数量
poster: '', // 竞赛海报
rounds: [ // 赛题包配置
{
id: 'round_default',
name: '第一轮:主赛环节',
roundType: roundTypeOptions[0].value,
questions: Array.from({ length: 10 }).map(() => ({
value: null,
score: 5,
time: 30,
title: '第1题',
scoreType: scoreTypeOptions[0].value,
})),
},
],
// 名单录入
groupManagement: [], // 组别管理
// 绑定硬件
// hardware: [], // 绑定的硬件设备
// 其他配置项...
isPublic: true, // 是否公开
// 是否主动流程(主持人主动流程:需要选择大题)
})
const steps = [
{ title: '基础设置', component: BasicInfo },
{ title: '名单录入', component: GroupManagement },
// { title: '硬件绑定', component: HardwareBinding },
{ title: '题目配置', component: QuestionConfig },
]
const currentComponent = computed(() => steps[currentStep.value - 1]?.component)
const stepComponentRef = useTemplateRef('stepComponentRef')
function handleClose() {
emit('update:show', false)
}
function nextStep() {
// 校验当前步骤
const componentInstance = stepComponentRef.value
if (componentInstance && typeof componentInstance.validate === 'function') {
const isValid = componentInstance.validate()
if (!isValid)
return
}
if (currentStep.value < 4) {
currentStep.value++
}
else {
// 汇总整理数据
const { name, startTime, endTime, groupCount, teamCount, poster, rounds } = competitionData.value
// eslint-disable-next-line unused-imports/no-unused-vars
const params = {
name,
startTime,
endTime,
groupCount,
teamCount,
poster,
rounds,
}
// 提交逻辑
window.$message?.success('比赛发布成功')
emit('success')
emit('update:show', false)
}
}
function prevStep() {
if (currentStep.value > 1) {
currentStep.value -= 1
}
}
function handleReset() {
window.$dialog?.warning({
title: '重置确认',
content: '确定要重置当前步骤的数据吗?此操作无法撤销。',
positiveText: '确定重置',
negativeText: '取消',
onPositiveClick: () => {
const componentInstance = stepComponentRef.value
if (componentInstance && typeof componentInstance.reset === 'function') {
componentInstance.reset()
window.$message?.success('重置成功')
}
},
})
}
</script>
<template>
<NModal :show="show" :mask-closable="false" @update:show="handleClose">
<div
class="relative h-[90vh] max-h-[980px] max-w-[95vw] w-[1280px] flex flex-col overflow-hidden rounded-[2rem] bg-white shadow-2xl transition-all"
>
<!-- Header -->
<div class="flex items-center justify-between px-10 py-8">
<!-- Logo & Title -->
<div class="flex items-center gap-4">
<div class="h-12 w-12 flex items-center justify-center rounded-xl bg-[#3B82F6] shadow-blue-500/30 shadow-lg">
<div class="i-carbon-circle-dash text-2xl text-white" />
</div>
<div class="flex flex-col">
<h2 class="m-0 text-xl text-gray-800 font-bold leading-none">
比赛创建向导
</h2>
<span class="mt-1 text-xs text-gray-400 font-bold tracking-widest uppercase">
Setup Configuration Wizard
</span>
</div>
</div>
<!-- Steps -->
<div class="flex-1 px-20">
<NSteps :current="currentStep" class="custom-steps" size="medium">
<NStep v-for="(step, index) in steps" :key="index" :title="step.title" />
</NSteps>
</div>
<!-- Close Button -->
<div class="flex items-center gap-3">
<NButton secondary type="warning" size="small" @click="handleReset">
<template #icon>
<icon-ic-baseline-refresh class="text-icon" />
</template>
重置当前步骤
</NButton>
<button
class="h-10 w-10 flex items-center justify-center rounded-full text-gray-300 transition-colors hover:scale-110 hover:bg-gray-100 hover:text-gray-500"
@click="handleClose"
>
<NButton
type="primary" size="small"
class="h-8 w-8 rounded-full text-gray-300 transition-colors hover:bg-gray-100 hover:text-gray-500"
>
<icon-ic-baseline-close class="text-icon" />
</NButton>
</button>
</div>
</div>
<!-- Content -->
<div class="flex-1 overflow-y-auto px-10">
<div class="h-full">
<KeepAlive>
<component
:is="currentComponent" ref="stepComponentRef" v-model="competitionData"
:config="competitionData"
/>
</KeepAlive>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-between border-t border-gray-100 bg-gray-50/50 px-10 py-6">
<NButton v-if="currentStep > 1" secondary type="primary" size="large" class="px-8 font-bold" @click="prevStep">
<template #icon>
<icon-ic-baseline-arrow-back class="text-icon" />
</template>
上一步
</NButton>
<div v-else />
<NButton
type="primary" size="large" class="px-10 text-lg font-bold shadow-blue-500/30 shadow-lg"
@click="nextStep"
>
{{ currentStep === 4 ? '确认发布比赛' : '下一步' }}
<template #icon>
<icon-ic-baseline-arrow-forward v-if="currentStep < 4" class="text-icon" />
<icon-ic-baseline-check v-else class="text-icon" />
</template>
</NButton>
</div>
</div>
</NModal>
</template>