feat: 新增题目导入服务并优化用户端答题流程
- 新增 question-importer 服务,支持从 JSON 文件批量导入题目到题库 - 重构用户端答题流程,整合抽题、答题、结果分析页面状态管理 - 将题目分类从 UI 模板类型切换为业务分类(question_category_name) - 在题库管理页面支持题目批量选择和删除功能 - 优化用户端组别选择,增加“已结束”状态提示和禁用逻辑 - 修复题目新增/更新 API 请求参数格式问题 - 为富文本编辑器配置排除不必要的工具栏按键
This commit is contained in:
@ -1,58 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import type { QuestionContent, UiTemplateType } from '../types'
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import { QuestionCategoryEnum } from '@/enum/business'
|
||||
import { useCompetitionStore } from '@/store/modules/competition'
|
||||
|
||||
const props = defineProps<{
|
||||
template: UiTemplateType
|
||||
content: QuestionContent
|
||||
}>()
|
||||
// const props = defineProps<{
|
||||
// template: QuestionCategoryName
|
||||
// content: QuestionContent
|
||||
// }>()
|
||||
|
||||
const store = useCompetitionStore()
|
||||
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
|
||||
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
||||
// console.log(currentQuestionMainInfo.value, 'currentQuestionMainInfo')
|
||||
// 题目
|
||||
const title = computed(() => currentQuestionMainInfo.value?.QuestionList?.QuestionContent)
|
||||
// 归属大纲
|
||||
// const mainTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.Name)
|
||||
// 题目内容
|
||||
const content = computed(() => currentQuestionDetail.value.Name)
|
||||
// 模版类型
|
||||
const template = computed(() => currentQuestionMainInfo.value?.Activity_Question?.UIType)
|
||||
|
||||
/**
|
||||
* 组合后的标题(用于汉字加一加模板)
|
||||
* 将 content 插入到 title 的引号中,并拆分以支持不同样式
|
||||
*/
|
||||
const parsedComponentAddTitle = computed(() => {
|
||||
const t = title.value || ''
|
||||
const c = content.value || ''
|
||||
|
||||
// 尝试替换全角空引号
|
||||
if (t.includes('“”')) {
|
||||
const [prefix, suffix] = t.split('“”')
|
||||
return {
|
||||
type: 'quote_full',
|
||||
prefix,
|
||||
content: c,
|
||||
suffix,
|
||||
}
|
||||
}
|
||||
// 尝试替换半角空引号
|
||||
if (t.includes('""')) {
|
||||
const [prefix, suffix] = t.split('""')
|
||||
return {
|
||||
type: 'quote_half',
|
||||
prefix,
|
||||
content: c,
|
||||
suffix,
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到空引号,直接追加
|
||||
return {
|
||||
type: 'normal',
|
||||
prefix: t,
|
||||
content: c,
|
||||
suffix: '',
|
||||
}
|
||||
})
|
||||
|
||||
/** 获取富文本里面的text */
|
||||
const pinyinChars = computed(() => {
|
||||
if (props.template === 'TEMPLATE_WORD_DICTATION' && props.content.title) {
|
||||
if (template.value === QuestionCategoryEnum.WordDictation && content.value) {
|
||||
// 创建临时元素提取HTML文本内容
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = props.content.title
|
||||
div.innerHTML = content.value
|
||||
const text = div.textContent || ''
|
||||
return text.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [currentQuestionMainInfo, currentQuestionDetail],
|
||||
() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Current currentQuestionMainInfo:', currentQuestionMainInfo.value)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Current currentQuestionMainInfo:', currentQuestionMainInfo)
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-[400px] w-full flex items-center justify-center p-6">
|
||||
<div class="w-full flex p-6" style="border: 1px solid red;">
|
||||
<!-- template:{{ template }} -->
|
||||
<!-- 模板A: 汉字听写-提示 (拼音+提示) -->
|
||||
<div v-if="template === 'TEMPLATE_DICTATION_HINT'" class="text-center">
|
||||
<div
|
||||
class="mb-6 inline-block text-5xl font-bold leading-none"
|
||||
style="text-shadow: 2px 2px 4px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
<!-- “{{ content.title }}” -->
|
||||
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation" class="text-center">
|
||||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||||
<div class="ptions-container1 text-2xl">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="rich-content1">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="text-4xl text-gray-800 font-bold">
|
||||
{{ content.meta?.hint }}
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 模板B: 汉字听写-同音字 (大字) -->
|
||||
<div v-else-if="template === 'TEMPLATE_DICTATION_HOMOPHONE'" class="text-center">
|
||||
<div class="homophone text-[100px] text-red-600 font-bold">
|
||||
<!-- “{{ content.title }}” -->
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 暂时未匹配到枚举,保持原样或删除 -->
|
||||
<!-- <div v-else-if="template === 'TEMPLATE_DICTATION_HOMOPHONE'" class="text-center"> -->
|
||||
|
||||
<!-- 模板C: 汉字加一加 (提示+部件) -->
|
||||
<div v-else-if="template === 'TEMPLATE_COMPONENT_ADD'" class="text-center">
|
||||
<div class="text-[100px] text-red-600 font-bold">
|
||||
<!-- {{ content.title }} -->
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
<div v-else-if="template === QuestionCategoryEnum.CharacterRadicalAddition" class="text-center">
|
||||
<div class="text-[50px] font-bold">
|
||||
<template v-if="parsedComponentAddTitle.type === 'quote_full'">
|
||||
<span class="text-black">{{ parsedComponentAddTitle.prefix }}“</span>
|
||||
<span class="text-red-600">{{ parsedComponentAddTitle.content }}</span>
|
||||
<span class="text-black">”{{ parsedComponentAddTitle.suffix }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="parsedComponentAddTitle.type === 'quote_half'">
|
||||
<span class="text-black">{{ parsedComponentAddTitle.prefix }}"</span>
|
||||
<span class="text-red-600">{{ parsedComponentAddTitle.content }}</span>
|
||||
<span class="text-black">"{{ parsedComponentAddTitle.suffix }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span class="text-black">{{ parsedComponentAddTitle.prefix }}</span>
|
||||
<span class="ml-4 text-red-600">{{ parsedComponentAddTitle.content }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模板D: 词语听写 (拼音+提示) -->
|
||||
<div v-else-if="template === 'TEMPLATE_WORD_DICTATION'" class="flex flex-col items-center">
|
||||
<div v-else-if="template === QuestionCategoryEnum.WordDictation" class="flex flex-col items-center">
|
||||
<div class="mb-8 flex flex-wrap justify-center gap-6">
|
||||
<div
|
||||
v-for="(char, index) in pinyinChars" :key="index"
|
||||
@ -78,69 +151,30 @@ const pinyinChars = computed(() => {
|
||||
</div>
|
||||
|
||||
<!-- 模板E: 成语-文字要求 -->
|
||||
<div v-else-if="template === 'TEMPLATE_IDIOM_TEXT_REQ'" class="text-center">
|
||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting" class="text-center">
|
||||
<div class="mb-8 text-4xl text-gray-800 font-bold">
|
||||
<!-- {{ content.title }} -->
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
<div class="rich-content" v-html="content" />
|
||||
</div>
|
||||
<!-- <div class="rounded bg-gray-100 px-4 py-2 text-xl text-gray-500">
|
||||
示例:{{ content.meta?.example }}
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 模板F: 成语-看图 (使用Div占位) -->
|
||||
<div v-else-if="template === 'TEMPLATE_IDIOM_IMAGE'" class="flex flex-col items-center text-center">
|
||||
<!-- 模板F: 诗词理解 -->
|
||||
<!-- <div v-else-if="template === 'TEMPLATE_IDIOM_IMAGE'" class="flex flex-col items-center text-center">
|
||||
<div class="mb-6 h-60 w-80 flex items-center justify-center">
|
||||
<!-- <span class="text-gray-400">图片展示区域<br></span> -->
|
||||
<img :src="content.titleImage" alt="idiom image" class="h-full w-full object-contain">
|
||||
</div>
|
||||
<!-- <div class="text-xl text-gray-600 font-bold">
|
||||
提示:{{ content.meta?.hint }}
|
||||
</div> -->
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 模板G: 诗词理解-选择题 (通用选择题模板) -->
|
||||
<div v-else-if="template === 'TEMPLATE_POETRY_MULTIPLE_CHOICE'" class="w-full flex flex-col items-center">
|
||||
<div class="mb-1 text-2xl text-gray-800 font-bold leading-relaxed">
|
||||
{{ content.title }}
|
||||
<div v-else-if="template === QuestionCategoryEnum.PoetryComprehension" class="w-full flex flex-col items-center">
|
||||
<div class="options-container text-2xl">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<div
|
||||
v-for="(option, index) in content.options" :key="index" class="option-card group" :class="{
|
||||
'is-mixed': !option.renderType || option.renderType === 'mixed' || option.renderType === 'image',
|
||||
'is-text': option.renderType === 'text',
|
||||
}"
|
||||
>
|
||||
<!-- 1. 图片渲染模式 -->
|
||||
<div v-if="option.image" class="option-image-wrapper">
|
||||
<img :src="option.image" :alt="option.label" class="option-image">
|
||||
</div>
|
||||
|
||||
<!-- 2. 田字格渲染模式 -->
|
||||
<div v-if="option.renderType === 'tianzige'" class="tianzige-wrapper">
|
||||
<!-- 米字格背景 -->
|
||||
<div class="tianzige-bg">
|
||||
<div class="line-horizontal" />
|
||||
<div class="line-vertical" />
|
||||
<svg width="100%" height="100%" class="line-diagonal">
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="#f87171" stroke-width="1" stroke-dasharray="4 2" />
|
||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="#f87171" stroke-width="1" stroke-dasharray="4 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="tianzige-text">{{ option.text }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 选项标签和文字 -->
|
||||
<div class="option-content">
|
||||
<div class="option-label">
|
||||
{{ option.label }}
|
||||
</div>
|
||||
<!-- 如果不是田字格模式,显示普通文本 -->
|
||||
<span v-if="option.renderType !== 'tianzige'" class="option-text">
|
||||
{{ option.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||||
<div class="rich-content" v-html="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user