Skip to content

题目组件

@qxs-bns/components-wc 提供了一套完整的题目 Web Components,涵盖题型选择、题目列表容器及各类题目编辑/预览组件。
题目内核建议使用 @qxs-bns/components-wc/core 引入;如需题干富文本,另外再引入 @qxs-bns/components-wc/editor

引入与使用

ts
import '@qxs-bns/components-wc/core'
vue
<qxs-subject-list
  :subject-list.prop="subjectList"
  :show-add.prop="false"
  @change="handleChange"
/>

在 Vue 3 中,给 Web Components 传数组、对象、函数或显式的布尔值时,建议直接使用连字符属性名配合 .prop,例如 :subject-list.prop="subjectList":show-add.prop="false"。不要再使用 .camel.prop

组件演示

loading

当前推荐用法

当前这套题目组件,推荐把 qxs-subject-list 作为主入口使用。

推荐组合

场景推荐组合说明
完整出题页qxs-subject-type + qxs-subject-list推荐默认方案。题型选择、题目增删、保存导出、业务事件都在列表层汇总
只编辑某一题qxs-subject-single / qxs-blank-fill / qxs-text-fill / qxs-scale适合自定义外层容器或只嵌入某一个题型
只做预览qxs-subject-list + is-preview统一走列表容器,隐藏编辑控件

宿主层职责

  • 引入 @qxs-bns/components-wc/core 注册题目核心组件(列表、题型组件、通用动作组件)。
  • 如需题干富文本能力,再引入 @qxs-bns/components-wc/editor 注册 qxs-blocksuite-editor
  • 通过 subject-list 传入题目数组,并在 change 事件里把最新列表同步回宿主状态。
  • 把标签、分类、AI 推荐答案、题库搜索、跳题、关联检查这些业务能力接到自己的接口或弹窗上。
  • 提交前优先调用 validate(),最终提交调用 toJSON()

提示:如果你使用 Vue 3,给 Web Components 传复杂类型时,建议直接使用连字符属性名配合 .prop,例如 :subject-list.prop="subjectList":category-list.prop="categoryList":search-handler.prop="searchHandler"。原生 JS 请使用 el['subject-list'] = value 这类写法。

Vue 3 快速开始

vue
<script setup lang="ts">
import { ref } from 'vue'
import '@qxs-bns/components-wc/core'
import '@qxs-bns/components-wc/editor'

const listRef = ref<any>(null)
const subjectList = ref<any[]>([])
const categoryList = ref([
  { categoryId: 'c1', title: '慢病管理' },
  { categoryId: 'c2', title: '健康行为' },
])

function handleSelect(e: CustomEvent) {
  const { type, canSet } = e.detail
  listRef.value?.addSubject(type, undefined, canSet ? 1 : 0)
}

function handleChange(e: CustomEvent) {
  subjectList.value = [...e.detail]
}

async function handleSubmit() {
  const result = await listRef.value?.validate()
  if (!result?.valid) {
    console.error('校验失败:', result.errors[0])
    return
  }
  const data = await listRef.value?.toJSON()
  console.log('提交数据:', data)
}
</script>

<template>
  <qxs-subject-type @select="handleSelect" />

<qxs-subject-list
    ref="listRef"
    :subject-list.prop="subjectList"
    :category-list.prop="categoryList"
    :show-add.prop="false"
    show-tag
    show-category
    show-ai
    @change="handleChange"
  />

  <button type="button" @click="handleSubmit">提交</button>
</template>

常用流程

  1. 页面初始化时,宿主层准备 subjectListcategoryListsearchHandler 等输入数据。
  2. 用户通过 qxs-subject-type 选择题型后,调用 listRef.addSubject() 新增题目。
  3. 题目列表内部发生编辑、删除、新增、标签回填等变化时,监听 change 同步外部 subjectList
  4. 宿主层按需监听 choose-tagtitle-selectjumpset-relation 这些业务事件,打开自己的弹窗或调用自己的接口。
  5. 提交前先调用 validate();通过后再调用 toJSON() 获取可直接落库的数据。

qxs-subject-type 题型选择器

题型选择器组件,提供可用题型列表供用户选择。

基本用法

html
<qxs-subject-type></qxs-subject-type>

事件

事件名Detail说明
select{ type: string, canSet: boolean }选择题型时触发

支持题型

type 值题型名称支持结果项
single单选题
multiple多选题
sort排序题
blank_fill填空题
text_fill问答题
scale量表题
page_end分页符

示例

vue
<script setup>
function handleSelect(e) {
  const { type, canSet } = e.detail
  const examAnswerRelationType = canSet ? 1 : 0
  subjectListRef.value?.addSubject(type, null, examAnswerRelationType)
}
</script>

<template>
  <qxs-subject-type @select="handleSelect" />
</template>

qxs-subject-list 题目列表

题目列表容器组件,支持拖拽排序和批量操作。

基本用法

html
<qxs-subject-list
  :subject-list.prop="subjectList"
  :category-list.prop="categoryList"
  :show-add.prop="false"
  @change="handleChange"
></qxs-subject-list>

属性

属性名类型默认值说明
subject-listany[][]题目列表数据(JS property 赋值)
is-previewbooleanfalse预览模式(隐藏编辑控件)
category-listany[][]分类选项列表,透传给单/多选/排序题
show-tagbooleanfalse是否显示标签/关键信息区域,仅作用于单/多选/排序题
show-categorybooleanfalse是否显示分类区域,仅作用于单/多选/排序题
show-aibooleanfalse是否显示 AI 推荐答案区域,仅作用于单/多选/排序题
show-resourcebooleanfalse是否显示资源区域,仅作用于单/多选/排序题
show-jumpbooleanfalse是否显示跳题逻辑入口,仅作用于单/多选/排序题
show-addbooleantrue是否显示“在此题后加入新题”入口
show-answer-settingbooleanfalse是否显示答题设置,仅作用于单/多选/排序和填空/问答的设置区
show-keybooleanfalse是否显示核心题入口,仅作用于单/多选/排序题
search-apistring''题库搜索接口地址;未传 search-handler 时作为搜索兜底
search-handler(query: string, answerType: number) => Promise<any[]>-自定义题库搜索方法,优先于 search-api
upload-image(file: File) => Promise<string>base64富文本图片上传方法(应用于所有题型)

提示:subject-listcategory-listsearch-handlerupload-image 这类数组、对象、函数类型,推荐通过 JS property 或框架的 .prop 绑定传入。

事件

事件名Detail说明
changeany[]题目列表变更时触发(增删改排序后)
title-select{ id, customId, title, value, ... }题库搜索下拉选中题目时触发
choose-tag{ customId, examId, value, item }点击标签“选择”入口时触发
jump{ customId, examId, answerType, item }点击跳题逻辑入口时触发
set-relation{ customId, examId, answerIndex, answer, item }点击关联检查入口时触发

方法

方法签名说明
addSubject(type: string, index?: number, examAnswerRelationType?: number)在指定位置添加新题目
addExam(items: any[])批量导入题目(含分页支持)
uploadExcel(list: any[])从 Excel 导入题目
setAnswerRelation(answerRelations: any, customId: string, customAnswerId: string)设置选项关联关系
setTagList(tagList: any[], customId: string)回填指定题目的标签/关键信息,并触发 change
currentListgetter获取当前题目列表
validate() => Promise<{ valid: boolean, errors: SubjectError[] }>校验整份题目列表,不抛异常
toJSON() => Promise<any[]>获取所有题目 JSON 数据;返回结果不包含 page_end,真实题目会带 pageIndex(校验失败抛出 SubjectError[]

示例

javascript
const list = document.querySelector('qxs-subject-list')

// 末尾添加单选题
list.addSubject('single')

// 在索引 0 后添加多选题
list.addSubject('multiple', 0)

// 监听变更
list.addEventListener('change', (e) => {
  console.log('题目列表变更:', e.detail)
})

// 提交所有题目数据(返回 Promise)
async function submitQuestions() {
  try {
    const data = await list.toJSON()
    console.log('提交数据:', data) // 每题都会带 pageIndex,分页符不会单独出现在结果里
    // 发送数据到服务器
  } catch (err) {
    const firstError = Array.isArray(err) ? err[0] : err
    if (firstError instanceof SubjectError) {
      console.error(`错误码: ${firstError.code}, 字段: ${firstError.field}, 信息: ${firstError.message}`)
    }
  }
}

qxs-subject-single 单选/多选/排序题

单选题、多选题、排序题的编辑/预览组件。

基本用法

html
<qxs-subject-single
  question-type="single"
  :is-edit="true"
  :order-index="0"
></qxs-subject-single>

属性

属性名类型默认值说明
order-indexnumber0题目序号(从 0 开始)
type'single' | 'multiple' | 'sort''single'题型
is-editbooleanfalse是否编辑模式
is-savebooleanfalse是否已保存(禁用输入)
is-setbooleanfalse是否已设置
is-keybooleanfalse是否核心题
show-actionbooleantrue是否显示操作栏
show-addbooleantrue是否显示“在此题后加入新题”入口
show-answer-settingbooleanfalse是否显示答题设置入口与只读展示
show-keybooleanfalse是否显示核心题入口与核心题标识
show-analysisbooleanfalse是否显示解析输入框
answer-listArray[]选项列表
tag-listTagItem[][]已选标签/关键信息列表
category-listCategory[][]分类选项列表
ai-answerstring''AI 推荐答案文本
resource-listResource[][]题目资源列表
show-tagbooleanfalse是否显示标签/关键信息区域
show-categorybooleanfalse是否显示分类区域
show-aibooleanfalse是否显示 AI 推荐答案区域
show-resourcebooleanfalse是否显示资源区域
show-jumpbooleanfalse是否显示跳题逻辑入口
has-jumpbooleanfalse当前题目是否已设置跳题逻辑
rich-text-contentstring''富文本内容
analysisstring''题目解析
answer-check-typenumber1答题设置类型
exam-answer-relation-typenumber0关联类型(0=无,1=结果项,2=关联检查)
least-answer-countnumber0多选题或排序题的至少选择项数,0 表示未设置
search-apistring''题库搜索接口地址;未传 search-handler 时作为搜索兜底
search-handler(query: string, answerType: number) => Promise<SearchResult[]>-自定义题库搜索方法,优先于 search-api
custom-idstring''自定义 ID

提示:tag-listcategory-listresource-listsearch-handler 这类数组、对象、函数类型,推荐通过 JS property 或框架的 .prop 绑定传入。

事件

事件名Detail说明
save{ title, answers, analysis, examExpand, isSetCorrectAnswer, ... }保存题目(验证通过后触发)
edit-点击编辑按钮
delete-点击删除按钮
move'up' | 'down'题目位置移动事件;当前默认不展示对应按钮
addstring点击“在此题后加入新题”后触发,值为题型标识
set-key{ value: boolean }设置/取消核心题
set-answer-setting{ value: number }修改答题设置
title-select{ id, customId, title, value, ... }题库搜索下拉选中题目时触发
choose-tag{ customId, examId, value }点击标签“选择”入口时触发
tag-change{ customId, examId, value }标签删除或调整后触发
category-change{ customId, examId, value }分类变更时触发
jump{ customId, examId, answerType }点击跳题逻辑入口时触发
set-relation{ customId, examId, answerIndex, answer }点击关联检查入口时触发,仅 exam-answer-relation-type=2 时生效

Answer 数据结构

typescript
interface Answer {
  title: string           // 选项文本
  isCorrect: boolean      // 是否正确答案
  customAnswerId?: string // 自定义选项 ID
  answerId?: string       // 选项 ID
  resultItem?: string     // 结果项内容
  orderIndex?: number     // 排序索引
  answerRelations?: any[] // 关联关系
}

业务接入契约(单/多选/排序)

以下契约以当前 components-wc 实现为准。

  • 组件内核负责:通用展示、基础校验、序列化、事件抛出、AI 一键勾选。
  • 业务宿主负责:真实业务接口、标签选择器、AI 推荐答案生成、跳题/关联检查配置弹窗。
  • 如果使用 qxs-subject-list,推荐优先在列表层对接业务事件;列表会负责把核心业务配置透传给 qxs-subject-single
能力业务方输入组件行为组件输出事件 / 回调业务方职责
标签/关键信息show-tagtag-list,或列表项中的 tagInfos/memberTagInfo展示已选标签,支持删除,保存时在开启状态下执行必填校验,并序列化到 tagInfos/memberTagInfo单题层:choose-tagtag-change;列表层:choose-tag;列表方法:setTagList()提供标签选择器、标签树/接口,并在选择完成后回填标签
AI 推荐答案show-aiai-answer,或列表项中的 aiAnswer展示 AI 推荐答案文本,支持“一键勾选”,保存时原样带回 aiAnswer无强制事件;通过 save/toJSON 返回最终结果生成或下发 aiAnswer 内容,并写入题目数据
题库搜索searchHandlersearch-api在题目标题输入时触发搜索,展示下拉候选,选中后自动回填标题单题层:title-select;列表层:title-select提供搜索接口或回调,并决定选中题库题目后的后续业务逻辑
分类show-categorycategory-listcategory-id展示分类下拉并保存 categoryId单题层:category-change;列表层内部自动写回当前题目数据提供分类选项数据;如直接使用单题组件,则自行监听分类变更
资源展示show-resourceresource-list,或列表项中的 resourceList/examResourceBOList展示图片/视频资源入口,支持预览,并在保存时原样序列化资源数据提供资源数据本身,以及资源上传/管理链路
跳题逻辑show-jump、题目数据中的 hasSet/is-set展示跳题入口和“已设置跳题逻辑”提示单题层:jump;列表层:jump打开跳题配置弹窗、保存跳题结果,并回写题目状态
关联检查 / 结果项exam-answer-relation-type、题目选项中的 answerRelations/resultItem1 时组件内编辑结果项;2 时展示关联检查入口和关联数量set-relation 仅在 exam-answer-relation-type=2 时触发;结果项通过 save/toJSON 返回关联检查模式下提供关联选择器,并在选择完成后调用 setAnswerRelation() 或直接回写题目数据
题目新增入口show-add控制“在此题后加入新题”入口显隐单题层:add;列表层默认已内建处理新增逻辑如直接使用单题组件,则自行接管新增题型的处理

最小业务接入示例

下面示例演示一个 Vue 宿主如何在列表层接入标签、AI 推荐答案和题库搜索。示例只描述最短接法,不依赖任何厂家端专有组件。

vue
<script setup lang="ts">
import '@qxs-bns/components-wc/core'
import { ref } from 'vue'

const listRef = ref<any>(null)

const subjectList = ref([
  {
    customId: 'q1',
    answerType: 'single',
    title: '患者是否规律服药?',
    aiAnswer: '1. 是',
    tagInfos: [],
    answers: [
      { title: '是', isCorrect: false },
      { title: '否', isCorrect: false },
    ],
  },
])

async function searchHandler(query: string, answerType: number) {
  const response = await fetch(`/api/question-search?q=${encodeURIComponent(query)}&answerType=${answerType}`)
  const data = await response.json()
  return (data?.list || []).map((item: any) => ({
    ...item,
    value: item.title,
  }))
}

async function openTagDialog() {
  // 这里替换成你自己的标签弹窗或标签选择器
  return [
    { tagId: 't1', tagName: '慢病管理' },
    { tagId: 't2', tagName: '依从性' },
  ]
}

async function handleChooseTag(e: CustomEvent) {
  const { customId } = e.detail
  const selected = await openTagDialog()
  listRef.value?.setTagList(selected, customId)
}

function handleChange(e: CustomEvent) {
  subjectList.value = [...e.detail]
}

function handleTitleSelect(e: CustomEvent) {
  console.log('题库题目已选中:', e.detail)
}
</script>

<template>
  <qxs-subject-list
    ref="listRef"
    :subject-list.prop="subjectList"
    show-tag
    show-ai
    :search-handler.prop="searchHandler"
    @change="handleChange"
    @choose-tag="handleChooseTag"
    @title-select="handleTitleSelect"
  />
</template>

qxs-blank-fill 填空题

填空题组件,支持多个填空答案。

基本用法

html
<qxs-blank-fill
  :is-edit="true"
  :order-index="0"
></qxs-blank-fill>

属性

属性名类型默认值说明
order-indexnumber0题目序号
is-editbooleanfalse是否编辑模式
is-savebooleanfalse是否已保存
show-addbooleantrue是否显示“在此题后加入新题”入口
show-answer-settingbooleanfalse是否显示填空题答题设置区域
titlestring''题目标题
answer-listArray[]答案列表
exam-answer-settingobject{}答题设置
rich-text-contentstring''富文本内容
analysisstring''题目解析

事件

事件名Detail说明
save{ title, answers, analysis, ... }保存题目
edit-点击编辑按钮
delete-点击删除按钮
move'up' | 'down'移动题目位置
add{ type, canSet }添加新题目

qxs-text-fill 问答题

问答题组件,支持文本输入答案。

基本用法

html
<qxs-text-fill
  :is-edit="true"
  :order-index="0"
></qxs-text-fill>

属性

属性名类型默认值说明
order-indexnumber0题目序号
is-editbooleanfalse是否编辑模式
is-savebooleanfalse是否已保存
show-addbooleantrue是否显示“在此题后加入新题”入口
show-answer-settingbooleanfalse是否显示问答题答题设置区域
titlestring''题目标题
answer-listArray[]关键词列表,兼容 title / answer 两种字段名
exam-answer-settingobject{}答题设置
exam-expandstring''完整推荐/正确答案
rich-text-contentstring''富文本内容
analysisstring''题目解析

事件

事件名Detail说明
save{ title, answers, analysis, ... }保存题目
edit-点击编辑按钮
delete-点击删除按钮
move'up' | 'down'移动题目位置
add{ type, canSet }添加新题目

问答题导出时遵循厂家端口径:

  • answers:关键词数组
  • examExpand:完整推荐/正确答案
  • examAnswerSettingBO:关键词命中规则

qxs-scale 量表题

量表题组件,支持矩阵式评分。

基本用法

html
<qxs-scale
  :is-edit="true"
  :order-index="0"
></qxs-scale>

属性

属性名类型默认值说明
order-indexnumber0题目序号
is-editbooleanfalse是否编辑模式
is-savebooleanfalse是否已保存
titlestring''题目标题
answer-listArray[]量表选项(列)
scale-questionsArray[]量表题目列表(行)
rich-text-contentstring''富文本内容
analysisstring''题目解析

事件

事件名Detail说明
save{ title, answers, scaleQuestions, analysis, ... }保存题目
edit-点击编辑按钮
delete-点击删除按钮
move'up' | 'down'移动题目位置
add{ type, canSet }添加新题目

qxs-subject-rich-text 富文本块

富文本块组件,用于在题目列表中展示和编辑富文本内容。

基本用法

html
<qxs-subject-rich-text
  :is-edit="true"
  :rich-text-content="content"
></qxs-subject-rich-text>

属性

属性名类型默认值说明
order-indexnumber0序号
is-editbooleanfalse是否编辑模式
is-savebooleanfalse是否已保存
show-actionbooleantrue是否显示操作栏
is-setbooleanfalse是否已设置
rich-text-contentstring''富文本内容
upload-image(file: File) => Promise<string>base64图片上传方法
exam-answer-relation-typenumber0关联类型

事件

事件名Detail说明
save{ richTextContent: string }保存富文本内容
edit-点击编辑按钮
delete-点击删除按钮
add{ type, canSet }添加新题目

qxs-page-end 分页符

分页符组件,用于在题目列表中插入分页。

注意:qxs-page-end 只作为编辑态分隔符存在。通过 qxs-subject-list.toJSON() 导出时,分页符不会单独出现在结果里;组件会把分页位置换算成每道题的 pageIndex

基本用法

html
<qxs-page-end
  :order-index="0"
></qxs-page-end>

属性

属性名类型默认值说明
order-indexnumber0序号
is-editbooleanfalse是否编辑模式
show-actionbooleantrue是否显示操作栏

事件

事件名Detail说明
delete-点击删除按钮
add{ type, canSet }添加新题目
题目组件 has loaded