Appearance
Components
@qxs-bns/components 提供的是一组可独立组合的 Web Components。
题目体系采用“宿主直渲染”模式,同时也提供 qxs-icon、qxs-fixed-action-bar、qxs-photo-crop-tool、qxs-file-upload、qxs-image-upload、qxs-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 回收结果时再导出