@tiku-canpoint/qp-component-ui
v1.0.13
Published
试题组件库 - 基于 Vue3,提供试题卡片和无头试题组件
Readme
@tiku-canpoint/qp-component-ui
试题组件库 - 基于 Vue3,提供试题卡片和无头试题组件,支持 LaTeX 数学公式渲染。
安装
npm install @tiku-canpoint/qp-component-ui
# 或
pnpm add @tiku-canpoint/qp-component-ui前置要求
- Vue 3.3.0+
完整引入
// main.ts
import { createApp } from 'vue'
import QpComponentUI from '@tiku-canpoint/qp-component-ui'
import '@tiku-canpoint/qp-component-ui/dist/style.css'
const app = createApp(App)
app.use(QpComponentUI)
app.mount('#app')按需引入
<script setup>
import { QpQuestionCard } from '@tiku-canpoint/qp-component-ui'
</script>组件
qp-question-card 试题卡片
试题卡片主容器组件,支持多种题型渲染、LaTeX 数学公式、答案解析显示、自定义插槽等。
功能特性
- ✅ 多题型支持:单选题、多选题、填空题、判断题、简答题、综合题、完形填空、听力题等
- ✅ LaTeX 公式渲染:自动加载 KaTeX,支持行内
$...$和块级$$...$$公式 - ✅ 答案解析:支持显示/隐藏答案、解析、考点、专题
- ✅ 子题支持:综合题自动渲染子题
- ✅ 自定义插槽:头部、底部、操作区域、标签区域均可自定义
- ✅ 响应式设计:适配不同屏幕尺寸
Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| questionData | Question | - | 试题数据(必填) |
| questionNum | number | 1 | 题号 |
| showExplain | boolean | false | 是否默认显示答案解析 |
| showKnowledge | boolean | true | 是否显示考点 |
| showSpecialSubject | boolean | true | 是否显示专题 |
| showTopInfo | boolean | true | 是否显示顶部信息(试题 ID、题型、难度等) |
| showTags | boolean | false | 是否显示标签 |
| showScore | boolean | false | 是否显示分数 |
| showSource | boolean | true | 是否显示试题来源 |
| metaInfoTop | MetaInfo[] | - | 自定义顶部元信息 |
| className | string | '' | 自定义类名 |
| onExpandExplain | (question: Question, isExpanded: boolean) => void \| Promise<void> | - | 展开答案解析时的回调函数,可用于异步加载试题详情 |
Events
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| toggle-explain | 切换答案解析时触发 | (visible: boolean) |
| click | 点击试题时触发 | (question: Question) |
Slots
| 插槽名 | 说明 | 插槽参数 |
|--------|------|----------|
| header | 自定义头部内容 | { question: Question } |
| footer | 自定义底部内容 | { question: Question } |
| bottomActionArea | 底部操作按钮区域 | { question: Question } |
| tagAction | 标签操作区域 | { question: Question, tags: TagInfo[] } |
基础用法
<template>
<qp-question-card
:questionData="questionData"
:questionNum="1"
:showExplain="false"
:showTopInfo="true"
/>
</template>
<script setup>
import { QpQuestionCard } from '@tiku-canpoint/qp-component-ui'
const questionData = {
questionId: 'Q001',
quesStruct: { code: 11, name: '单选题' },
quesType: { code: 11, name: '单选题' },
context: {
stem: '已知函数 $f(x) = x^2 - 2x + 1$,则 $f(2)$ 的值为( )',
options: ['$0$', '$1$', '$2$', '$4$'],
},
explain: [{
answers: [['B']],
analysis: '将 $x = 2$ 代入:$f(2) = 2^2 - 2 \\times 2 + 1 = 1$,故选 B。',
}],
knowledge: [{ code: 'K001', name: '函数求值' }],
difficulty: { code: 2, name: '中等' },
level: 2,
}
</script>使用插槽
<template>
<qp-question-card :questionData="questionData">
<!-- 自定义头部 -->
<template #header="{ question }">
<span>难度:{{ question.difficulty?.name }}</span>
</template>
<!-- 自定义底部 -->
<template #footer="{ question }">
<span>试题 ID: {{ question.questionId }}</span>
</template>
<!-- 底部操作按钮 -->
<template #bottomActionArea="{ question }">
<button @click.stop="handleEdit(question)">编辑</button>
<button @click.stop="handleDelete(question)">删除</button>
</template>
</qp-question-card>
</template>展开回调(异步加载详情)
当试题数据需要异步加载时,可以传入 onExpandExplain 回调函数,在点击展开答案解析时执行。常用于按需加载试题详情接口:
<template>
<qp-question-card
:questionData="questionData"
:questionNum="1"
:showExplain="false"
:onExpandExplain="handleExpandExplain"
/>
</template>
<script setup>
import { ref } from 'vue'
import { QpQuestionCard } from '@tiku-canpoint/qp-component-ui'
import { getQuestionDetail } from '@/api/question'
const questionData = ref({
questionId: '12345',
// ... 基础试题数据(可能不包含完整的答案解析)
})
// 展开答案解析时的回调 - 异步加载完整试题详情
const handleExpandExplain = async (question, isExpanded) => {
if (isExpanded && !question.explain) {
try {
// 调用接口获取完整的试题详情(包含答案解析)
const detail = await getQuestionDetail(question.questionId)
// 更新试题数据,组件会自动渲染答案解析
questionData.value = {
...questionData.value,
explain: detail.explain,
knowledge: detail.knowledge,
specialSubject: detail.specialSubject,
}
} catch (error) {
console.error('加载试题详情失败', error)
}
}
}
</script>注意事项:
- 回调支持异步函数(Promise),组件会等待回调完成后再展开显示
- 如果回调执行失败(抛出异常),组件会捕获错误并继续展开
- 回调仅在展开时(isExpanded = true)触发,收起时不会触发
无头组件
useQuestion
无头试题组件,提供试题解析逻辑,不包含 UI 渲染。
功能特性
- ✅ 试题题干解析
- ✅ 答案解析生成
- ✅ 显示规则控制
- ✅ 数学公式渲染支持
- ✅ 展开回调(异步加载详情)
Options
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| question | Ref<Question \| undefined> | required | 试题数据(Ref 或 ComputedRef) |
| questionNum | number | 1 | 题号 |
| showExplain | boolean | false | 是否默认显示答案解析 |
| showKnowledge | boolean | true | 是否显示考点 |
| showSpecialSubject | boolean | true | 是否显示专题 |
| showTopInfo | boolean | true | 是否显示顶部信息 |
| showTags | boolean | false | 是否显示标签 |
| showScore | boolean | false | 是否显示分数 |
| showSource | boolean | true | 是否显示试题来源 |
| metaInfoTop | MetaInfo[] | - | 自定义顶部元信息 |
| onExpandExplain | (question: Question, isExpanded: boolean) => void \| Promise<void> | - | 展开答案解析时的回调函数,可用于异步加载试题详情 |
返回值
| 属性 | 类型 | 说明 |
|------|------|------|
| stem | UseQuestionStemReturn | 题干相关数据和计算属性 |
| answer | UseQuestionAnswerReturn | 答案相关数据和计算属性 |
| isShowExplain | Ref<boolean> | 是否显示答案解析 |
| isExpanding | Ref<boolean> | 是否正在展开(回调执行中) |
| toggleExplain | () => Promise<void> | 切换答案解析显示(支持异步回调) |
| katex | { isReady: Ref<boolean>; render: (el?) => Promise<void> } | KaTeX 相关 |
| metaInfoList | ComputedRef<MetaInfo[]> | 元信息列表 |
| childrenList | ComputedRef<Question[]> | 子题列表 |
| showChildren | ComputedRef<boolean> | 是否显示子题 |
| levelLabel | ComputedRef<string> | 难度等级 |
stem 属性
| 属性 | 类型 | 说明 |
|------|------|------|
| stemHtml | string | 题干 HTML(含公式) |
| options | string[] | 选项列表 |
| showStem | boolean | 是否显示题干 |
| showOptions | boolean | 是否显示选项 |
| getOptionHtml | (option: string, index: number) => string | 获取选项 HTML |
answer 属性
| 属性 | 类型 | 说明 |
|------|------|------|
| answerList | string[] | 答案列表 |
| analysisList | string[] | 解析列表 |
| knowledgeList | Knowledge[] | 考点列表 |
| specialSubjectList | Knowledge[] | 专题列表 |
| commentList | string[] | 点评列表 |
用法
<template>
<div>
<!-- 题干 -->
<div v-if="stem.showStem" v-html="stem.stemHtml"></div>
<!-- 选项 -->
<div v-if="stem.showOptions">
<div v-for="(option, index) in stem.options" :key="index">
<span v-html="stem.getOptionHtml(option, index)"></span>
</div>
</div>
<!-- 答案 -->
<div>
<div v-for="(item, index) in answer.answerList" :key="index" v-html="item"></div>
</div>
<!-- 解析 -->
<div>
<div v-for="(item, index) in answer.analysisList" :key="index" v-html="item"></div>
</div>
<!-- 考点 -->
<div v-if="answer.knowledgeList.length">
【考点】
<span v-for="item in answer.knowledgeList" :key="item.code">{{ item.name }}</span>
</div>
</div>
</template>
<script setup>
import { useQuestion } from '@tiku-canpoint/qp-component-ui'
const questionData = {
questionId: 'Q001',
quesStruct: { code: 11, name: '单选题' },
context: {
stem: '已知函数 $f(x) = x^2$,则 $f(2)$ 的值为',
options: ['$0$', '$1$', '$2$', '$4$'],
},
explain: [{
answers: [['D']],
analysis: '将 $x = 2$ 代入:$f(2) = 2^2 = 4$',
}],
knowledge: [{ code: 'K001', name: '函数求值' }],
}
const { stem, answer } = useQuestion({ question: questionData })
</script>展开回调(异步加载详情)
使用组合式函数时,可以传入 onExpandExplain 回调,在展开答案解析时执行异步操作:
<template>
<div ref="cardRef" class="question-card">
<!-- 题干 -->
<div v-if="stem.showStem" v-html="stem.stemHtml"></div>
<div v-if="stem.showOptions">
<div v-for="(option, index) in stem.options" :key="index">
<span v-html="stem.getOptionHtml(option, index)"></span>
</div>
</div>
<!-- 加载中提示 -->
<div v-if="isExpanding" class="loading">加载试题详情中...</div>
<!-- 答案解析 -->
<div v-if="isShowExplain">
<div v-for="item in answer.answerList" :key="item" v-html="item"></div>
</div>
<button @click="toggleExplain" :disabled="isExpanding">
{{ isExpanding ? '加载中...' : (isShowExplain ? '收起解析' : '展开解析') }}
</button>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useQuestion } from '@tiku-canpoint/qp-component-ui'
import { getQuestionDetail } from '@/api/question'
const cardRef = ref(null)
const questionData = ref({
questionId: '12345',
// ... 基础试题数据
})
const questionRef = computed(() => questionData.value)
// 展开回调 - 异步加载试题详情
const handleExpandExplain = async (question, isExpanded) => {
if (isExpanded && !question.explain) {
const detail = await getQuestionDetail(question.questionId)
questionData.value = {
...questionData.value,
explain: detail.explain,
knowledge: detail.knowledge,
specialSubject: detail.specialSubject,
}
}
}
const {
stem,
answer,
isShowExplain,
isExpanding, // 回调执行中为 true
toggleExplain,
katex,
} = useQuestion({
question: questionRef,
showExplain: false,
onExpandExplain: handleExpandExplain,
})
onMounted(() => {
katex.render(cardRef.value)
})
</script>注意事项:
- 回调支持异步函数(Promise),组件会等待回调完成后再展开显示
- 如果回调执行失败(抛出异常),组件会捕获错误并继续展开
- 回调仅在展开时(isExpanded = true)触发,收起时不会触发
isExpanding属性可用于显示加载状态(回调执行中为 true)- 按钮可以使用
:disabled="isExpanding"防止重复点击
LaTeX 公式支持
组件自动渲染 LaTeX 数学公式,支持以下格式:
- 行内公式:
$...$或\(...\) - 块级公式:
$$...$$或\[...\]
预加载 KaTeX(可选)
使用 qp-katex-loader 组件提前加载 KaTeX,避免公式渲染延迟:
<template>
<qp-katex-loader>
<qp-question-card :questionData="questionData" />
</qp-katex-loader>
</template>
<script setup>
import { QpKatexLoader, QpQuestionCard } from '@tiku-canpoint/qp-component-ui'
</script>自定义 CDN
<template>
<qp-katex-loader baseUrl="https://unpkg.com">
<qp-question-card :questionData="questionData" />
</qp-katex-loader>
</template>使用本地 KaTeX
import katex from 'katex'
import renderMathInElement from 'katex/contrib/auto-render'
import { configure } from '@tiku-canpoint/qp-component-ui'
window.katex = katex
window.renderMathInElement = renderMathInElement
configure({ loadStrategy: 'none' })数据类型
Question 试题数据
interface Question {
/** 试题 ID */
questionId: string;
/** 题型结构 */
quesStruct: {
code: number;
name: string;
};
/** 题型信息 */
quesType?: {
code: number;
name: string;
};
/** 试题上下文 */
context: {
/** 题干 */
stem: string;
/** 选项 */
options?: string[];
/** 听力原文 */
original_text?: string;
/** 音频 URL */
audioUrl?: string;
/** 音频名称 */
audioName?: string;
};
/** 解析 */
explain?: QuestionExplain[];
/** 考点 */
knowledge?: Knowledge[];
/** 专题 */
specialSubject?: Knowledge[];
/** 子题(综合题) */
children?: Question[];
/** 难度 */
difficulty?: {
code: number;
name: string;
};
/** 等级 */
level?: number;
/** 标签 */
tagPaper?: {
year?: number;
area?: string;
paperType?: string;
};
/** 来源 */
source?: string;
/** 分数 */
score?: number;
}QuestionExplain 答案解析
interface QuestionExplain {
/** 答案(二维数组支持多选、多空) */
answers: string[][];
/** 解析(支持 HTML 和 LaTeX) */
analysis?: string;
/** 点评 */
comment?: string;
}Knowledge 考点/专题
interface Knowledge {
code: string;
name: string;
}MetaInfo 元信息
interface MetaInfo {
label: string;
value: string;
}支持题型
| 题型代码 | 题型名称 | 说明 | |---------|---------|------| | 11 | 单选题 | 单个正确选项的选择题 | | 12 | 多选题 | 多个正确选项的选择题 | | 13 | 填空题 | 需要填写空白的题目 | | 14 | 判断题 | 判断对错的题目 | | 15 | 简答题 | 需要简要回答的题目 | | 16 | 多题综合类 | 包含多个子题的综合题 | | 17 | 多题综合类(同一知识点) | 同一知识点下的综合题 | | 18 | 解答题 | 需要详细解答的题目 | | 19 | 听力多题 | 包含听力材料的综合题 | | 20-22 | 听力单题 | 听力单选/填空/判断 | | 23 | 完形填空 | 完形填空类题目 | | 24 | 综合类填空 | 阅读理解类填空 | | 25 | 七选五 | 七选五类阅读理解 | | 26 | 作文 | 作文类题目 | | 27-28 | 选择填充/四选二 | 特殊选择题型 |
