Files
reader-star/apps/admin/src/views/user/modules/QuestionRenderer.vue
rjl 771fb807eb feat(admin): 新增题目类型、优化题目库管理并添加拼音助手
- 新增6种题目类型枚举:逆向接诗句、场景猜诗句、联想对对碰、字词飞花令、诗词常识、换字组成语
- 新增QuestionDrawer组件,支持富文本编辑和拼音转换功能
- 优化CategoryTree组件,增加分类选择本地存储和自动填充题目类型
- 更新question API接口,简化新增和更新题目库的参数传递
- 扩展QuestionRenderer组件,支持新增的题目类型渲染
- 添加多个题目库JSON文件,包含成语、诗词常识、字词飞花令等题目数据
- 调整环境配置,更新开发环境API地址
2026-03-16 17:59:49 +08:00

421 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>