
简介这是一套面向教育、培训与考试场景的Web端答题卡制作工具源码核心价值在于可视化在线设计答题卡适用于教师、教务人员、培训机构及有定制需求的开发者用户无需编程基础即可通过拖拽、点选完成布局与内容编辑显著降低答题卡制作门槛。压缩包共55个文件其中37个JavaScript负责交互逻辑、拖放处理与数据校验15个CSS控制页面样式与打印适配3个HTML搭建页面骨架总大小约718KB目录结构清晰便于二次开发。源码涵盖HTML/CSS布局、JavaScript事件处理、前端框架集成、Canvas/SVG可视化编辑、JSON数据序列化及多浏览器兼容等关键知识点交互上支持题目/选项拖拽排序、单选/多选切换、输入验证等操作并集成jQuery、layui、html2canvas、jsPDF等常用库完整呈现从界面绘制、选项设置到卡片预览与导出的一站式解决方案。目前已有518人浏览学习适合希望深入理解前端组件化开发、在线编辑器实现或需要定制答题卡系统的开发者研读参考。1. 先聊聊我为什么动手做这个东西1.1 被手动排版折磨的日常前阵子帮朋友的教育机构搭一套周测系统原本以为最难的会是题库或者成绩统计结果真正让我头疼的是一张小小的答题卡。他们每周都有不同科目的小测验数学要20道选择题英语要听力加笔试混合排版物理化学经常要挤在一张A4纸上排下40道题。每个老师用Word排出来的版式都不一样有的题号对不齐有的选项间距忽大忽小打印到纸上怎么看怎么别扭。我也试过市面上现成的在线答题卡工具但问题很明显——要么模板固定死改不了题号起始值和分栏数量要么免费版导出的文件带水印清晰度还打折扣。最后实在忍不了我决定自己动手做一套web端答题卡制作工具把可视化在线制作答题卡这件事做成一个所见即所得的编辑器。这篇文章就把整个开发过程中的关键决策、数据模型设计、打印导出的坑全部写出来你要是也在做类似方向可以直接拿去参考。1.2 做之前我先理清楚了三个核心痛点动手写代码之前我花了不少时间跟一线老师聊发现真正的痛点比我想象的集中。第一是排版效率老师们根本不想知道什么单元格合并、行高列宽他们需要的是我加一道题、改个选项数量、调整一下分栏之后页面立刻变成想要的样子。第二是格式统一一张答题卡上所有题号必须对齐选项间隔必须一致答题区域的分割线必须清晰靠肉眼手工调整不可能稳定。第三是输出可靠答题卡最终是要打印出来用的导出的文件不能在尺寸上出现任何偏差否则到了印刷环节就是灾难。1.3 我给自己划的功能边界我也给自己定了一条边界不做一个大而全的在线考试系统只专注答题卡制作这一件事。题库管理不碰在线答题不碰成绩统计不碰。整个工具只解决从调整参数到导出可打印文件这一条链路。范围收窄之后架构反而好设计了编辑器最核心的部分就是三个区域左侧参数配置面板、中间实时预览画布、顶部工具栏。数据流向也非常简单配置项改变数据模型数据模型驱动画布重新渲染整个过程没有多余的中间环节。2. 技术选型与数据模型设计这两步定生死2.1 前端技术栈为什么选Vue 3技术选型上我几乎没有纠结直接用了Vue 3 TypeScript Vite这套组合。Vue 3的组合式API在管理复杂表单状态时非常顺手尤其是答题卡这种多子组件联动的场景reactive加computed可以很自然地推导出哪些参数变了哪些区块需要重绘。拖拽方面我用的是Sortablejs配合vuedraggable这个封装组件。其实一开始也考虑过dnd-kit、interactjs这些库但对比下来Sortablejs的成熟度最高移动端触摸支持也好后续如果要出平板版本不需要重写交互层。状态管理用了Pinia。可能有人会觉得小题大做但当你需要做撤销重做、历史快照、跨组件共享试卷配置的时候没有一个统一的状态层会很痛苦。Pinia的store之间可以互相调用我建了一个usePaperStore来管整张试卷的数据一个useEditorStore管编辑器的UI状态边界很清楚。2.2 数据模型一切皆配置整个项目的核心其实不是组件怎么写而是数据结构怎么设计。答题卡本质上是一张纸 若干区块每个区块有自己的类型和参数。我定义了一份JSON Schema大致结构是这样{ paper: { pageWidth: 210, pageHeight: 297, marginTop: 15, marginLeft: 15, title: 2026学年第一学期数学周测, headerFields: [name, class, studentId], blocks: [ { id: b1, type: choice, subject: math, startNumber: 1, questionCount: 20, optionCount: 4, columns: 2, score: 3 }, { id: b2, type: answer, startNumber: 21, questionCount: 5, linesPerQuestion: 6 } ] } }这套模型最开始设计的时候有个小插曲我一开始把选项数量写死在每个题目里结果发现数学老师要4个选项英语老师要7个选项物理老师偶尔还要判断题直接打勾打叉。后来改成区块级别的optionCount一个区块内所有题目统一处理逻辑瞬间简单了很多老师们也能接受一个题型区用一种配置的设定。2.3 为什么坚决不用Canvas渲染这里有个关键决策想多说两句。答题卡预览区域我全程用的是DOM渲染没有用Canvas。原因有三个一是打印样式Canvas画出来的内容在window.print()的时候没法直接用CSS控制分页必须手动算坐标算错一毫米打印出来就废了二是可访问性DOM渲染的内容可以选中、可以搜索Canvas就是一整张位图三是开发效率DOM加CSS的方案天然支持响应式布局而Canvas所有布局都要手动计算开发周期至少翻倍。3. 可视化编辑器的核心实现细节3.1 纸张参数与画布初始化纸张参数是整个画布的基石。我在配置面板里放了一组数字输入框页面宽度、高度、上下左右页边距单位全部用毫米。预览区的A4纸效果是用一个固定宽高比的容器模拟出来的默认宽度210mm、高度297mm。这里有个小技巧浏览器里CSS的mm单位在屏幕上的表现不完全等于物理毫米它会受屏幕DPI影响所以预览区域我实际上是用像素做一层换算1mm ≈ 3.7795px96DPI下但在打印样式里直接写210mm让浏览器自己处理这样屏幕预览和打印结果才能对得上。.paper-canvas { width: 210mm; min-height: 297mm; margin: 0 auto; padding: 15mm; background: #fff; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); }3.2 题目区块的拖拽与增删改拖拽排序是编辑器最直观的交互。我用vuedraggable把中间的预览区变成一个拖拽容器每个题目区块都是可拖拽的卡片拖到新位置之后实时更新blocks数组的顺序。为了拖拽体验不显得生硬我给拖拽中的元素加了半透明效果和缩放过渡并且设了一个animation: 200的动画时长让列表滚动和元素位移都有一点点缓动。添加题目的操作放在顶部工具栏点添加选择题区块就会在当前选中位置后面插入一个默认配置的区块。为了防止误操作删除区块时我加了一步确认弹窗显示将删除题号x到题号y的区块把影响范围说清楚再执行操作。3.3 题号与选项的动态渲染逻辑这是整个项目里最容易写乱的地方。题号不能简单地1、2、3排下去因为一个区块可能从第21题开始而且分栏之后要保证题号按列优先还是行优先排列两种方式在答题卡上的分布完全不同。我专门写了一个generateNumberMatrix函数来处理function generateNumberMatrix({ start, count, columns, perColumn }) { const numbers Array.from({ length: count }, (_, i) start i) const matrix [] if (perColumn row) { for (let i 0; i numbers.length; i columns) { matrix.push(numbers.slice(i, i columns)) } } else { // 列优先先按列分桶 const colSize Math.ceil(numbers.length / columns) for (let c 0; c columns; c) { for (let r 0; r colSize; r) { const idx r * columns c if (numbers[idx] ! undefined) matrix.push(numbers[idx]) } } } return matrix }实际运行下来中文考试场景里绝大多数老师习惯行优先一行排5个题号排满再换行这种最直观。列优先更多用在小语种或者特殊排版需求上所以我把这个选项暴露给了用户默认是行优先。3.4 头部信息区的灵活配置答题卡顶部一般需要姓名、班级、学号填写栏。这些信息看起来简单但处理不好很占地方。我用了三个开关分别控制三个字段是否显示每个字段可以独立设置下划线类型和框内填写还是横线填写。另外还支持自定义一个附加文本行比如注意事项请用2B铅笔填涂。这部分在数据模型里就是一个headerFields数组渲染时根据字段类型选择对应的填涂控件模板。4. 打印导出与阅卷机识别兼容最容易翻车的一环4.1 打印样式的关键处理开发过程中最坑的部分就是打印。浏览器默认的打印样式会把背景色去掉、把边距重置如果不做处理打印出来的答题卡会非常难看。我专门写了一套media print样式核心逻辑是隐藏编辑器的侧边栏和工具栏把预览画布的阴影和圆角去掉强制白色背景。media print { body { background: #fff; } .editor-toolbar, .editor-config-panel { display: none !important; } .paper-canvas { width: auto; min-height: auto; margin: 0; padding: 0; box-shadow: none; border: none; } .answer-block { break-inside: avoid; } .question-row { break-inside: avoid; } }4.2 页面尺寸与像素换算的坑打印这里我踩了一个非常隐蔽的坑一张A4纸的物理尺寸是210mm×297mm但浏览器打印时默认有页边距如果你在CSS里设置了page { margin: 0; }内容才能真正按满幅去排否则会出现大约10mm的默认偏移。我一开始没设置导致打印出来整体偏右下后来在样式里显式声明了page { margin: 0; }同时在.paper-canvas里用padding模拟答题卡的页边距这样打印机的物理页边距归零所有边距都由我自己控制结果完全可控。4.3 阅卷机识别对排版有哪些硬性要求如果你的答题卡要拿去机房阅卷排版规范比好不好看重要得多。根据我了解到的行业通用做法机读答题卡需要满足几个关键约束定位黑块必须在固定位置且尺寸不能随意缩放通常在四角填涂区域必须是矩形且彼此间距一致题号之间的距离要均匀不能忽大忽小。我在编辑器里增加了一个机读模式选项开启后自动在四角生成定位黑块并把选项间距锁定为固定值同时禁用页边距的自由调节。这个功能做完后打印出来的答题卡拿给阅卷机测试通过率比手工排的高很多。5. 我在实际使用中踩过的坑和优化经验5.1 大题量下的渲染性能问题有一回我给一个英语老师排100道题的答题卡页面直接卡成PPT。排查后发现问题是每个题目都是独立的Vue组件100道题就是100个组件实例每个组件还要监听props变化性能自然扛不住。解决方案有两个一是把单题组件改成区块级组件整个区块只创建一个组件实例内部用v-for渲染减少组件层级二是对不可见区域做懒渲染只渲染首屏附近的题目滚动时动态替换。这两个优化做完之后300道题的答题卡也能流畅拖动。5.2 撤销重做的实现思路编辑器没有撤销重做基本没法用。我的方案是在Pinia里维护一个历史栈每次变更配置之前先压栈。但这里有个权衡如果每次输入框的每一次击键都压栈历史记录会飞快膨胀。我用了防抖批量记录策略输入框失焦或者连续操作间隔超过500毫秒才记录一次快照。快照存的是整个paper对象的深拷贝用structuredClone来做数据量控制在几百KB性能完全可接受。5.3 自定义程度和易用性的平衡做可视化工具最容易犯的毛病是什么都要用户配置结果配置面板比Word还复杂。我前几版确实把配置项做到了极致字体、行距、边框粗细全都开放出来结果用户根本不知道怎么调。后来我改成预设优先、高级设置折叠的策略提供五套答题卡模板用户在模板基础上改题号和选项数量就够了高级设置折叠在二级面板里普通用户压根不用打开。这个改动让我明显感觉到工具变得好用了同时也没有牺牲灵活性。5.4 导出到PDF的正确姿势虽然window.print()可以直接打印但很多用户需要先导出一个PDF文件存档或者发给印刷厂。最开始我尝试用html2canvas截图结果文字模糊、清晰度惨不忍睹。后来改成调用系统打印对话框用户选择另存为PDF的方式这样输出的PDF是基于矢量文字的放大缩小都清晰。如果你需要程序化生成PDF建议用Puppeteer加载一个专门的导出路由去渲染效果比任何前端截图方案都好。最后再分享一个我自己用的习惯答题卡这种工具做出来只是第一步真正有价值的是模板库的积累。每次给老师排完一张特殊的答题卡我都会顺手存成模板标注适用科目和场景。用了半年之后大部分需求都是直接套模板微调两个参数就能出稿效率比刚开始高了好几倍。你要是也在做类似工具从一开始就要把模板沉淀机制设计进去。本文还有配套的精品资源点击获取