Files
reader-star/apps/admin/src/views/user/modules/QuestionRenderer.vue
rjl b71a758474 feat(user): 重构用户端竞赛流程并添加新页面
- 将原单页竞赛流程拆分为独立路由页面(首页、封面、规则、抽题、答题、图解、组别、队伍)
- 新增 Pinia store 管理竞赛状态、计时器和音频控制
- 添加 SCSS 变量文件定义主题色
- 更新路由配置和类型定义以支持新页面结构
- 重构 QuestionRenderer 组件优化样式
- 添加图标依赖并更新国际化配置
- 移动图片资源到用户目录并清理旧文件
2026-02-06 17:45:44 +08:00

301 lines
8.5 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-6">
<!-- 模板A: 汉字听写-提示 (拼音+提示) -->
<div v-if="template === 'TEMPLATE_DICTATION_HINT'" class="text-center">
<div class="mb-6 inline-block text-5xl 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-1 text-2xl 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 */
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 */
}
}
</style>