- 新增6种题目类型枚举:逆向接诗句、场景猜诗句、联想对对碰、字词飞花令、诗词常识、换字组成语 - 新增QuestionDrawer组件,支持富文本编辑和拼音转换功能 - 优化CategoryTree组件,增加分类选择本地存储和自动填充题目类型 - 更新question API接口,简化新增和更新题目库的参数传递 - 扩展QuestionRenderer组件,支持新增的题目类型渲染 - 添加多个题目库JSON文件,包含成语、诗词常识、字词飞花令等题目数据 - 调整环境配置,更新开发环境API地址
421 lines
11 KiB
Vue
421 lines
11 KiB
Vue
<script setup lang="ts">
|
||
import { computed, watch } from 'vue'
|
||
import TianZiGe from '@/components/custom/user/TianZiGe.vue'
|
||
import { QuestionCategoryEnum } from '@/enum/business'
|
||
import { useCompetitionStore } from '@/store/modules/competition'
|
||
|
||
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 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 (content.value) {
|
||
// 创建临时元素提取HTML文本内容
|
||
const div = document.createElement('div')
|
||
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="w-full flex p-6">
|
||
<!-- <p>template:{{ template }}</p> -->
|
||
<!-- 模板A: 汉字听写1-提示 (拼音+提示) -->
|
||
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation1 || template === QuestionCategoryEnum.ChineseCharacterDictation2" class="text-center">
|
||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||
<div class="ptions-container1 mb-2 text-center text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="rich-content ml-3 mt-4 flex gap-1 color-red-600">
|
||
<!-- <TianZiGe v-for="(char, index) in pinyinChars" :key="index" :char="char" /> -->
|
||
{{ content }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板B: 汉字听写-同音字 (大字) -->
|
||
<!-- 暂时未匹配到枚举,保持原样或删除 -->
|
||
<!-- <div v-else-if="template === 'TEMPLATE_DICTATION_HOMOPHONE'" class="text-center"> -->
|
||
|
||
<!-- 模板C: 汉字加一加 (提示+部件) -->
|
||
<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 === QuestionCategoryEnum.WordDictation" class="flex flex-col items-center">
|
||
<div class="mb-8 flex flex-wrap justify-center gap-6">
|
||
<TianZiGe v-for="(char, index) in pinyinChars" :key="index" :char="char" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板E: 成语-文字要求 -->
|
||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting1 || template === QuestionCategoryEnum.IdiomWriting2" class="text-center">
|
||
<div class="mb-8 text-4xl text-gray-800 font-bold">
|
||
<!-- {{ content }} -->
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板G: 诗词理解-选择题 (通用选择题模板) -->
|
||
<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="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板ChangeWords: 换字组成语 -->
|
||
<div v-else-if="template === QuestionCategoryEnum.ChangeWords" class="w-full flex flex-col items-center">
|
||
<div class="options-container text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板: 场景猜诗句 -->
|
||
<div v-else-if="template === QuestionCategoryEnum.SceneBasedPoemGuessing" class="w-full flex flex-col items-center">
|
||
<div class="options-container text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板: 联想对对碰 -->
|
||
<div v-else-if="template === QuestionCategoryEnum.AssociationMatching" class="w-full flex flex-col items-center">
|
||
<div class="options-container text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板: 字词飞花令 -->
|
||
<div v-else-if="template === QuestionCategoryEnum.CharacterWordFlowerDrinkingGame" class="w-full flex flex-col items-center">
|
||
<div class="options-container text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板: 诗词常识 -->
|
||
<div v-else-if="template === QuestionCategoryEnum.PoetryCommonKnowledge" class="w-full flex flex-col items-center">
|
||
<div class="options-container text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模板: 逆向接诗句 -->
|
||
<div v-else-if="template === QuestionCategoryEnum.ReversePoemMatching" class="w-full flex flex-col items-center">
|
||
<div class="options-container text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||
<div class="rich-content" v-html="content" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="text-red-500">
|
||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||
<div class="ptions-container1 mb-2 text-center text-2xl">
|
||
{{ title }}
|
||
</div>
|
||
<div class="rich-content ml-3 mt-4 flex gap-1 color-red-600">
|
||
{{ content }}
|
||
</div>
|
||
</div>
|
||
未知的题目模板: {{ template }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.options-container {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 2rem;
|
||
margin-bottom: 3rem;
|
||
|
||
.option-card {
|
||
// width: 320px;
|
||
// height: 260px;
|
||
}
|
||
}
|
||
|
||
.option-card {
|
||
display: flex;
|
||
cursor: pointer;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 2px solid transparent;
|
||
border-radius: 0.75rem;
|
||
/* rounded-xl */
|
||
padding: 1rem;
|
||
/* p-4 */
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
border-color: #f87171;
|
||
/* border-red-400 */
|
||
background-color: #fef2f2;
|
||
/* bg-red-50 */
|
||
box-shadow:
|
||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
/* shadow-lg */
|
||
}
|
||
|
||
&.is-mixed {
|
||
flex-direction: column;
|
||
}
|
||
|
||
&.is-text {
|
||
flex-direction: row;
|
||
gap: 1rem;
|
||
/* gap-4 */
|
||
}
|
||
}
|
||
|
||
.option-image-wrapper {
|
||
margin-bottom: 0.8rem;
|
||
height: 6rem;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
border-radius: 0.5rem;
|
||
/* rounded-lg */
|
||
padding: 0.5rem;
|
||
/* p-2 */
|
||
}
|
||
|
||
.option-image {
|
||
height: 100%;
|
||
width: 100%;
|
||
object-fit: contain;
|
||
transition: transform 0.3s;
|
||
|
||
.group:hover & {
|
||
transform: scale(1.05);
|
||
}
|
||
}
|
||
|
||
.tianzige-wrapper {
|
||
position: relative;
|
||
margin-bottom: 0.75rem;
|
||
/* mb-3 */
|
||
height: 8rem;
|
||
/* h-32 */
|
||
width: 8rem;
|
||
/* w-32 */
|
||
display: flex;
|
||
user-select: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 2px solid #ef4444;
|
||
/* border-red-500 */
|
||
background-color: white;
|
||
}
|
||
|
||
.tianzige-bg {
|
||
pointer-events: none;
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 0;
|
||
height: 100%;
|
||
width: 100%;
|
||
|
||
.line-horizontal {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
height: 1px;
|
||
width: 100%;
|
||
border-top: 1px dashed #f87171;
|
||
/* border-red-400 */
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.line-vertical {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 0;
|
||
height: 100%;
|
||
width: 1px;
|
||
border-left: 1px dashed #f87171;
|
||
/* border-red-400 */
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.line-diagonal {
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
.tianzige-text {
|
||
font-family: 'KaiTi', 'STKaiti', serif;
|
||
/* font-kaaiti */
|
||
z-index: 10;
|
||
font-size: 3.75rem;
|
||
/* text-6xl */
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
/* text-gray-800 */
|
||
}
|
||
|
||
.option-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
/* gap-3 */
|
||
}
|
||
|
||
.option-label {
|
||
height: 2rem;
|
||
/* h-8 */
|
||
width: 2rem;
|
||
/* w-8 */
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 9999px;
|
||
/* rounded-full */
|
||
background-color: #fee2e2;
|
||
/* bg-red-100 */
|
||
color: #dc2626;
|
||
/* text-red-600 */
|
||
font-weight: 700;
|
||
|
||
.group:hover & {
|
||
background-color: #ef4444;
|
||
/* bg-red-500 */
|
||
color: white;
|
||
}
|
||
}
|
||
|
||
.option-text {
|
||
font-size: 1.25rem;
|
||
/* text-xl */
|
||
color: #374151;
|
||
/* text-gray-700 */
|
||
font-weight: 500;
|
||
|
||
.group:hover & {
|
||
color: #b91c1c;
|
||
/* text-red-700 */
|
||
}
|
||
}
|
||
|
||
.homophone {
|
||
:deep(img) {
|
||
height: 230px !important;
|
||
width: auto !important;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
|
||
.rich-content {
|
||
:deep(img) {
|
||
max-width: 200px;
|
||
height: auto;
|
||
object-fit: contain;
|
||
margin: 0 auto;
|
||
vertical-align: middle;
|
||
}
|
||
}
|
||
</style>
|