Skip to content

Components

@qxs-bns/components 提供的是一组可独立组合的 Web Components。
题目体系采用“宿主直渲染”模式,同时也提供 qxs-iconqxs-fixed-action-barqxs-photo-crop-toolqxs-file-uploadqxs-image-uploadqxs-data-chart 这类可独立接入的组件。

跨框架更新提醒

@qxs-bns/components 现在已经统一到跨框架的 Web Components 方案,不再按“Vue 组件注册 + 模板糖”这条链路设计。
如果你的页面已经用到了组件,升级时建议优先检查这几件事:

  • 接入方式:直接 import '@qxs-bns/components' 或对应子入口,然后使用 Custom Element 标签,不再依赖 Vue 全局注册
  • 复杂属性:对象、数组、函数、布尔值在 Vue 宿主里优先使用 .prop 透传,例如 :data.prop="chartData":readonly.prop="true"
  • 事件取值:组件事件本质是 CustomEvent,业务数据从 event.detail 里取,不要再按旧的 Vue 组件回调参数习惯处理
  • 样式覆盖:优先用组件暴露的属性、CSS 变量和 ::part(...),不要假设可以直接深入组件内部 DOM
  • 业务状态:像题目列表、分页边界、结果项弹窗、标签分组这类业务状态,应该继续由宿主自己维护,组件只负责局部渲染、校验和导出

更完整的迁移对照见 Components 迁移指南

宿主接入题目数组

Components 的核心不是“内建列表容器”,而是宿主自己维护题目数组,再按题型渲染单题组件。
如果这条链路能跑通,后面的校验、导出和分页 helper 都会自然接上。

loading

统一校验与提交数据

题目接入跑通之后,再把运行时 helper 接进去。
推荐让宿主直接维护最终提交数据,再由 pageIndex 派生分页边界渲染。
这一步的目标是统一校验当前 DOM 题目树;只有在需要从组件回收结果时,才额外使用导出 helper。

loading

安装

bash
pnpm add @qxs-bns/components

导入

ts
import '@qxs-bns/components'

推荐入口

新的题目接入方式是:

  • 宿主自己的按钮、菜单或侧边面板:决定新增哪种题型
  • qxs-subject-single / qxs-blank-fill / qxs-text-fill / qxs-scale:渲染题目本体
  • qxs-page-end:分页边界
  • qxs-subject-sortable:可选的官方排序器
  • qxs-icon:统一字符串型图标入口
  • qxs-fixed-action-bar:底部固定操作容器
  • qxs-photo-crop-tool:上传前图片裁剪工具
  • qxs-file-upload:文件上传组件
  • qxs-image-upload:图片上传组件
  • qxs-data-chart:图表与表格看板组件
  • helper:分页、排序、校验、导出

推荐导入

ts
import '@qxs-bns/components'
import {
  collectSubjectElements,
  getPaginationMeta,
  insertPageBreak,
  removePageBreak,
  reorderSubjects,
  serializeSubjectElements,
  validateSubjectElements,
} from '@qxs-bns/components'

Vue 3

vue
<script setup lang="ts">
import { ref } from 'vue'
import '@qxs-bns/components'
import {
  collectSubjectElements,
  serializeSubjectElements,
  validateSubjectElements,
} from '@qxs-bns/components'

const rootRef = ref<HTMLElement | null>(null)
const items = ref<any[]>([])

function addSingleQuestion() {
  items.value.push({
    customId: crypto.randomUUID(),
    answerType: 'single',
    title: '',
    analysis: '',
    isEdit: true,
    examRichTextContent: '',
    answers: Array.from({ length: 4 }, () => ({ title: '', isCorrect: false })),
  })
}

async function submit() {
  if (!rootRef.value) {
    return
  }
  const elements = collectSubjectElements(rootRef.value)
  const result = validateSubjectElements(elements)
  if (!result.valid) {
    console.error(result.errors[0])
    return
  }
  console.log(await serializeSubjectElements(elements))
}
</script>

<template>
  <button type="button" @click="addSingleQuestion">新增单选题</button>

  <div ref="rootRef">
    <template v-for="item in items" :key="item.customId">
      <qxs-page-end
        v-if="item.answerType === 'page_end'"
        :custom-id="item.customId"
      />

      <qxs-subject-single
        v-else-if="['single', 'multiple', 'sort'].includes(item.answerType)"
        :custom-id="item.customId"
        :question-type="item.answerType"
        :title="item.title"
        :answer-list.prop="item.answers"
        :exam-answer-relation-type.prop="item.examAnswerRelationType ?? 0"
        :is-edit.prop="true"
      />
    </template>
  </div>

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

React

tsx
import { useMemo, useRef, useState } from 'react'
import '@qxs-bns/components'
import {
  collectSubjectElements,
  serializeSubjectElements,
  validateSubjectElements,
} from '@qxs-bns/components'

export default function QuizBuilder() {
  const rootRef = useRef<HTMLDivElement>(null)
  const [items, setItems] = useState<any[]>([])

  function addSingleQuestion() {
    setItems((current) => current.concat({
      customId: crypto.randomUUID(),
      answerType: 'single',
      title: '',
      analysis: '',
      isEdit: true,
      examRichTextContent: '',
      answers: Array.from({ length: 4 }, () => ({ title: '', isCorrect: false })),
    }))
  }

  const content = useMemo(() => items.map((item) => {
    if (item.answerType === 'page_end') {
      return <qxs-page-end key={item.customId} custom-id={item.customId} />
    }
    return (
      <qxs-subject-single
        key={item.customId}
        custom-id={item.customId}
        question-type={item.answerType}
        title={item.title}
        is-edit
      />
    )
  }), [items])

  async function submit() {
    if (!rootRef.current) {
      return
    }
    const elements = collectSubjectElements(rootRef.current)
    const result = validateSubjectElements(elements)
    if (!result.valid) {
      console.error(result.errors[0])
      return
    }
    console.log(await serializeSubjectElements(elements))
  }

  return (
    <>
      <button type="button" onClick={addSingleQuestion}>新增单选题</button>
      <div ref={rootRef}>{content}</div>
      <button type="button" onClick={submit}>提交</button>
    </>
  )
}

原生 HTML

html
<script type="module">
  import '@qxs-bns/components'
  import {
    collectSubjectElements,
    serializeSubjectElements,
    validateSubjectElements,
  } from '@qxs-bns/components'

  const root = document.getElementById('root')
  const items = []

  function addSingleQuestion() {
    items.push({
      customId: crypto.randomUUID(),
      answerType: 'single',
      title: '',
      analysis: '',
      isEdit: true,
      examRichTextContent: '',
      answers: Array.from({ length: 4 }, () => ({ title: '', isCorrect: false })),
    })
    render()
  }

  function render() {
    root.innerHTML = ''
    items.forEach((item) => {
      const el = document.createElement(item.answerType === 'page_end' ? 'qxs-page-end' : 'qxs-subject-single')
      el.setAttribute('custom-id', item.customId)
      if (item.answerType !== 'page_end') {
        el.setAttribute('question-type', item.answerType)
        el['answer-list'] = item.answers
        el.title = item.title
        el['is-edit'] = true
      }
      root.appendChild(el)
    })
  }

  document.getElementById('addSingle').addEventListener('click', addSingleQuestion)
  document.getElementById('submit').addEventListener('click', async () => {
    const elements = collectSubjectElements(root)
    const result = validateSubjectElements(elements)
    if (!result.valid) {
      console.error(result.errors[0])
      return
    }
    console.log(await serializeSubjectElements(elements))
  })
</script>

<button id="addSingle">新增单选题</button>
<div id="root"></div>
<button id="submit">提交</button>

可用能力

题目组件

组件说明
QxsSubjectSingle单选题 / 多选题 / 排序题
QxsBlankFill填空题
QxsTextFill问答题
QxsScale量表题

辅助组件

组件说明
QxsPageEnd分页边界
QxsSubjectSortable可选排序器
QxsEditor富文本编辑器

helper

API说明
运行时与辅助组件查看分页、排序、校验、导出的 helper 清单

注意事项

  • 当前 @qxs-bns/components 已统一为跨框架 Web Components 方案
  • 题目场景直接引入 @qxs-bns/components
  • 宿主自己维护“新增题目”的 UI 和基础 JSON 模板
  • 题目本体和业务字段要分层:标签、分组、分类、结果项弹窗由宿主自己维护
  • 优先让宿主直接维护最终提交数据;提交前使用运行时 helper 做统一校验,需要从 DOM 回收结果时再导出

下一步

Components has loaded