feat: 新增题目导入服务并优化用户端答题流程

- 新增 question-importer 服务,支持从 JSON 文件批量导入题目到题库
- 重构用户端答题流程,整合抽题、答题、结果分析页面状态管理
- 将题目分类从 UI 模板类型切换为业务分类(question_category_name)
- 在题库管理页面支持题目批量选择和删除功能
- 优化用户端组别选择,增加“已结束”状态提示和禁用逻辑
- 修复题目新增/更新 API 请求参数格式问题
- 为富文本编辑器配置排除不必要的工具栏按键
This commit is contained in:
2026-02-11 17:27:08 +08:00
parent 61a7412fc1
commit f3136cd9ad
34 changed files with 4154 additions and 225 deletions

View File

@ -13,9 +13,13 @@ const { routerPushByKey } = useRouterPush()
const store = useCompetitionStore()
const activityInfoStore = useActivityInfoStore()
const { activityId, groupId, activityInfo } = storeToRefs(activityInfoStore)
// 直接从 Store 解构所需状态,保持单一数据源
const { activityId, groupId, activityInfo, teamId, teamIds } = storeToRefs(activityInfoStore)
const currentQuestionInfo = computed(() => store.currentQuestionInfo)
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
const loading = ref(false)
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
@ -24,6 +28,11 @@ const isStarted = ref(false)
console.log(activityInfo.value, 'activityInfo.value')
console.log(currentQuestionInfo.value, 'currentQuestionInfo.value')
console.log(currentQuestionDetail.value, 'currentQuestionDetail.value')
console.log(currentQuestionMainInfo.value, 'currentQuestionMainInfo.value')
const mainTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.Name || '')
// const subTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.QuestionContent || '')
onMounted(async () => {
await fetchGetQuestionDetailData()
@ -33,7 +42,14 @@ onMounted(async () => {
watch(() => store.timeLeft, (newVal) => {
if (newVal === 0 && isStarted.value) {
store.stopTimer()
routerPushByKey('user_analysis')
routerPushByKey('user_analysis', {
query: {
activityId: String(activityId.value),
groupsId: String(groupId.value),
teamId: String(teamId.value),
teamIds: teamIds.value.join(','),
},
})
}
})
@ -69,7 +85,14 @@ const chartData = [
function handleBack() {
store.stopTimer()
routerPushByKey('user_draw')
routerPushByKey('user_draw', {
query: {
activityId: String(activityId.value),
groupsId: String(groupId.value),
teamId: String(teamId.value),
teamIds: teamIds.value.join(','),
},
})
}
/**
@ -80,12 +103,15 @@ async function fetchGetQuestionDetailData() {
loading.value = true
try {
const currentActivityId = String(activityId.value || '')
const questionId = store.currentQuestionInfo.QuestionID || (store.currentQuestionInfo as any).Id
const questionId = store.currentQuestionInfo?.QuestionID || (store.currentQuestionInfo as any).Id
const { data: detailData } = await fetchGetQuestionDetail(currentActivityId, questionId)
console.log(detailData, 'detailData')
if (detailData && detailData.data) {
store.setCurrentQuestionDetail(detailData.data)
}
else {
window?.$message?.error('题目详情不存在,请联系管理员')
}
}
catch (error) {
console.error('Failed to fetch question detail', error)
@ -126,7 +152,14 @@ async function handleNext() {
else {
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
store.stopTimer()
routerPushByKey('user_analysis')
routerPushByKey('user_analysis', {
query: {
activityId: String(activityId.value),
groupsId: String(groupId.value),
teamId: String(teamId.value),
teamIds: teamIds.value.join(','),
},
})
}
}
@ -147,18 +180,19 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
<template>
<CompetitionLayout
action-position="top" :show-back="false" :show-next="true"
:title="store.currentQuestionInfo?.ActitvityQuestionName || ''" back-btn-text="返回"
:next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="false" btn-theme="light" @back="handleBack"
@next="handleNext"
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
back-btn-text="返回" :next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="isStarted" btn-theme="light"
@back="handleBack" @next="handleNext"
>
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
<!-- 题目说明 -->
<!-- <p>{{ store.currentQuestionInfo.ActitvityQuestionName }}</p> -->
<!-- <p class="mt-8px text-3xl">
{{ subTitle }}
</p> -->
<!-- 题目内容区域 -->
<div v-if="template && content" class="w-full flex flex-1 items-center justify-center overflow-hidden py-4">
<div class="max-h-full max-w-full flex items-center justify-center">
<QuestionRenderer :template="template" :content="content" class="origin-center scale-150 transform" />
<div class="max-h-full max-w-full">
<QuestionRenderer class="origin-center scale-150 transform" />
</div>
</div>