Files
reader-star/apps/admin/src/views/user copy/modules/QuestionRenderer.vue
rjl ca42c6a876 feat(admin): 新增用户端答题流程页面与相关功能模块
- 新增用户端答题流程主页面,包含封面、介绍、答题、答题图解四个状态
- 新增用户端组件:封面、介绍页、答题页、答题图解页、题目渲染器及通用布局组件
- 新增用户端类型定义、模拟数据及业务常量
- 新增用户路由及类型声明,包含组别和队伍选择页面
- 新增管理端首页模块:数据卡片、头部横幅、折线图、饼图、创意横幅和项目新闻
- 新增富文本编辑器组件,支持图片和视频上传至OSS
- 优化题库分类树,改为扁平化列表结构
- 完善比赛创建功能,增加轮次类型、题目分数类型及关联AP字段
- 修复模板删除函数调用参数错误
- 添加 pinyin-pro 依赖以支持拼音处理功能
2026-02-05 18:00:11 +08:00

302 lines
8.6 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 type { QuestionContent, UiTemplateType } from '../types'
import { computed } from 'vue'
const props = defineProps<{
template: UiTemplateType
content: QuestionContent
}>()
const pinyinChars = computed(() => {
if (props.template === 'TEMPLATE_WORD_DICTATION' && props.content.title) {
return props.content.title.split(' ')
}
return []
})
</script>
<template>
<div class="min-h-[400px] w-full flex items-center justify-center p-10">
<!-- 模板A: 汉字听写-提示 (拼音+提示) -->
<div v-if="template === 'TEMPLATE_DICTATION_HINT'" class="text-center">
<div class="mb-6 inline-block text-[120px] text-red-500 font-bold leading-none" style="text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
{{ content.title }}
</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="text-[100px] text-red-600 font-bold">
{{ content.title }}
</div>
</div>
<!-- 模板C: 汉字加一加 (提示+部件) -->
<div v-else-if="template === 'TEMPLATE_COMPONENT_ADD'" class="text-center">
<div class="text-[100px] text-red-600 font-bold">
{{ content.title }}
</div>
</div>
<!-- 模板D: 词语听写 (拼音+提示) -->
<div v-else-if="template === 'TEMPLATE_WORD_DICTATION'" 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"
class="relative h-32 w-32 flex select-none items-center justify-center border-2 border-red-500 bg-white"
>
<!-- 米字格背景 -->
<div class="pointer-events-none absolute inset-0 z-0 h-full w-full">
<!-- 横虚线 -->
<div class="absolute left-0 top-1/2 h-[1px] w-full border-t border-red-400 border-dashed opacity-60" />
<!-- 竖虚线 -->
<div class="absolute left-1/2 top-0 h-full w-[1px] border-l border-red-400 border-dashed opacity-60" />
<!-- 斜虚线 (SVG) -->
<svg width="100%" height="100%" class="absolute inset-0 opacity-60">
<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="z-10 text-5xl text-gray-800 font-bold font-sans">{{ char }}</span>
</div>
</div>
</div>
<!-- 模板E: 成语-文字要求 -->
<div v-else-if="template === 'TEMPLATE_IDIOM_TEXT_REQ'" class="text-center">
<div class="mb-8 text-4xl text-gray-800 font-bold">
{{ content.title }}
</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">
<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>
<!-- 模板G: 诗词理解-选择题 (通用选择题模板) -->
<div v-else-if="template === 'TEMPLATE_POETRY_MULTIPLE_CHOICE'" class="w-full flex flex-col items-center">
<div class="mb-8 text-3xl text-gray-800 font-bold leading-relaxed">
{{ content.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>
</div>
<div v-else class="text-red-500">
未知的题目模板: {{ 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 */
background-color: #f9fafb; /* bg-gray-50 */
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: 1rem;
height: 9rem;
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 */
}
}
</style>