feat: 新增题目导入服务并优化用户端答题流程
- 新增 question-importer 服务,支持从 JSON 文件批量导入题目到题库 - 重构用户端答题流程,整合抽题、答题、结果分析页面状态管理 - 将题目分类从 UI 模板类型切换为业务分类(question_category_name) - 在题库管理页面支持题目批量选择和删除功能 - 优化用户端组别选择,增加“已结束”状态提示和禁用逻辑 - 修复题目新增/更新 API 请求参数格式问题 - 为富文本编辑器配置排除不必要的工具栏按键
This commit is contained in:
@ -3,7 +3,7 @@ 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'
|
||||
import { fetchGetQuestionOutline } from '@/service/api/game'
|
||||
import { fetchGetCurrentQuestion } from '@/service/api/game'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import { useCompetitionStore } from '@/store/modules/competition'
|
||||
import { AudioController } from '@/utils/audio'
|
||||
@ -14,40 +14,43 @@ const { routerPushByKey, routerBack } = useRouterPush()
|
||||
const route = useRoute()
|
||||
const activityInfoStore = useActivityInfoStore()
|
||||
const activityId = computed(() => activityInfoStore.activityId || route.query?.activityId as string)
|
||||
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 store = useCompetitionStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const question = ref<Api.Competition.QuestionListRecord | null>(null)
|
||||
const question = ref<Api.Competition.CurrentQuestionResponse | null>(null)
|
||||
const flippedId = ref<number | null>(null)
|
||||
const audioController = new AudioController()
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const pageDescription = computed(() => {
|
||||
if (question.value) {
|
||||
return question.value.QuestionSubTitle || ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
async function initQuestions() {
|
||||
if (!activityId.value)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { data: outlineData, error } = await fetchGetQuestionOutline(Number(activityId.value), 0)
|
||||
const { data: outlineData, error } = await fetchGetCurrentQuestion({
|
||||
ActivityID: Number(activityId.value),
|
||||
GroupID: Number(groupsId.value),
|
||||
RoundType: 0,
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(outlineData, 'outlineData')
|
||||
if (error) {
|
||||
window?.$message?.error(error.message)
|
||||
return
|
||||
}
|
||||
|
||||
// 直接使用返回的对象数据,不进行数组转换
|
||||
if (outlineData && outlineData.data) {
|
||||
question.value = outlineData.data as unknown as Api.Competition.QuestionListRecord
|
||||
store.setQuestionList([question.value]) // Store expects array, so wrap it
|
||||
// const object = {
|
||||
// ...outlineData.data?.Activity_Question || {},
|
||||
// ...outlineData.data?.QuestionList || {},
|
||||
// TeamGroup_QuestionID: outlineData.data?.TeamGroup_QuestionID || 0,
|
||||
// }
|
||||
question.value = outlineData.data as unknown as Api.Competition.CurrentQuestionResponse
|
||||
store.setCurrentQuestionMainInfo(question.value)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@ -63,7 +66,20 @@ onMounted(() => {
|
||||
initQuestions()
|
||||
})
|
||||
|
||||
async function handleCardClick(item: Api.Competition.QuestionListRecord) {
|
||||
async function handleCardClick(item: Api.Competition.CurrentQuestionResponse) {
|
||||
const { ID } = item?.Activity_Question || {}
|
||||
|
||||
if (!ID) {
|
||||
window.$message?.error('题目ID不存在')
|
||||
// 回到选组页面
|
||||
routerPushByKey('user_groups', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (flippedId.value)
|
||||
return // 防止重复点击
|
||||
|
||||
@ -75,15 +91,21 @@ async function handleCardClick(item: Api.Competition.QuestionListRecord) {
|
||||
catch (e) {
|
||||
console.error('Audio play failed', e)
|
||||
}
|
||||
|
||||
// 2. 触发翻转动画
|
||||
flippedId.value = item.ID
|
||||
flippedId.value = ID
|
||||
|
||||
// 3. 延迟1s后跳转
|
||||
setTimeout(() => {
|
||||
try {
|
||||
store.setCurrentQuestionInfo(item)
|
||||
routerPushByKey('user_game')
|
||||
store.setCurrentQuestionInfo(item.Activity_Question || {})
|
||||
routerPushByKey('user_game', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
groupsId: groupsId.value,
|
||||
teamId: teamId.value,
|
||||
teamIds: teamIds.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Navigation failed', error)
|
||||
@ -101,16 +123,21 @@ async function handleCardClick(item: Api.Competition.QuestionListRecord) {
|
||||
function handleBack() {
|
||||
routerBack()
|
||||
}
|
||||
|
||||
const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompetitionLayout :show-back="true" :show-next="false" :title="question?.ActitvityQuestionName || '抽题'" @back="handleBack">
|
||||
<CompetitionLayout
|
||||
:show-back="true" :show-next="false" :title="questionMainTitle || '抽题'"
|
||||
@back="handleBack"
|
||||
>
|
||||
<div class="h-full w-full flex flex-col items-center pt-10 font-sans">
|
||||
<div v-if="question" class="mb-10 max-w-4xl text-center">
|
||||
<!-- <div v-if="question" class="mb-10 max-w-4xl text-center">
|
||||
<div class="text-2xl text-gray-800 font-medium leading-relaxed tracking-wide">
|
||||
{{ question?.ActitvityQuestionName || '' }}
|
||||
{{ questionMainTitle || '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 题目卡片列表 -->
|
||||
<div v-if="loading" class="text-xl text-gray-500">
|
||||
@ -119,20 +146,22 @@ function handleBack() {
|
||||
<div v-else class="w-full flex flex-1 items-center justify-center pb-10">
|
||||
<div v-if="question" class="lottery-stick-container">
|
||||
<div
|
||||
class="lottery-stick"
|
||||
:class="{ 'is-selected': flippedId === question.ID }"
|
||||
class="lottery-stick" :class="{ 'is-selected': flippedId === question?.Activity_Question?.ID }"
|
||||
@click="handleCardClick(question)"
|
||||
>
|
||||
<!-- 签身 (默认显示) -->
|
||||
<div class="stick-face stick-body">
|
||||
<div class="stick-top-mark" />
|
||||
<div class="writing-mode-vertical-rl h-full flex items-center justify-center text-xl text-yellow-900 font-bold tracking-widest opacity-80">
|
||||
<div
|
||||
class="writing-mode-vertical-rl h-full flex items-center justify-center text-xl text-yellow-900 font-bold tracking-widest opacity-80"
|
||||
>
|
||||
抽 题
|
||||
</div>
|
||||
</div>
|
||||
<!-- 签面 (翻转后显示 - 实际上是放大显示的) -->
|
||||
<div class="stick-face stick-content">
|
||||
<span class="writing-mode-vertical-rl text-2xl text-red-600 font-bold">{{ question.ActitvityQuestionName }}</span>
|
||||
<span class="writing-mode-vertical-rl text-2xl text-red-600 font-bold">{{ questionMainTitle
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -190,7 +219,8 @@ function handleBack() {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #ef4444; /* red-500 */
|
||||
background-color: #ef4444;
|
||||
/* red-500 */
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user