uni-answer-sheet 1.0.6 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +387 -0
- package/REFERENCE_ASSETS.md +7 -0
- package/THIRD_PARTY_NOTICES.md +1536 -0
- package/dist/browser.js +202 -0
- package/dist/chunks/index-CDvIw14k.js +2256 -0
- package/dist/chunks/runtime-C-fdAw2T.js +24380 -0
- package/dist/index.d.ts +609 -0
- package/dist/index.js +5 -2
- package/dist/style.css +1 -0
- package/package.json +34 -150
- package/dist/index.css +0 -30267
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
- 新增 `getRecognitionData()`,`save()` 成功数据新增 `recognitionData`;等待字体、图片及分页稳定后测量。
|
|
6
|
+
- 使用坐标协议版本 2:`origin: 'topLeftPaper'`、96 dpi 像素,每张物理纸面左上角为原点。四角定位点、父区域、作答线和单元格均使用纸面全局坐标;跨页片段保留题目关联。
|
|
7
|
+
- 导出考号、班级、缺考标记、选项、填空、主观作答、评分框、选做题、禁答区、页码、学科、二维码及辅助定位点,校验越界和标记重叠。
|
|
8
|
+
- 补齐作业卡班级/学号卷头、无分值批阅框、禁止答题区与空白答题区,并支持保存和回显。
|
|
9
|
+
- 回显忽略传入的坐标缓存,按当前内容重新计算;题卡文档版本保持 1,挂载 API 和富文本插件协议保持兼容。
|
|
10
|
+
- 建议配合 `uni-editor@0.2.1` 使用;保留对 `0.2.0` 题干字母框坐标的兼容。两个包由宿主分别安装。
|
|
11
|
+
|
|
12
|
+
坐标输出采用本包的扁平结构;参考项目旧版 `cutparamJsonstr2` 协议映射、扫描图像校正及识别服务不包含在本包中。PDF 服务和演示按钮也不进入 npm 包。
|
package/README.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Uni Answer Sheet
|
|
2
|
+
|
|
3
|
+
独立制卡插件,保留题卡布局、题型设置、分页和题卡合一编辑。通过显式注册 `uni-editor/plugin` 接入富文本。公共 API 只使用 DOM、普通对象和 Promise,适用于原生 JavaScript、Vue、React 等浏览器项目;宿主无需安装 React、Tiptap 或 MathLive。
|
|
4
|
+
|
|
5
|
+
`2.0.0` 使用全新的 DOM 插件 API,与原 `1.x` Vue 组件版接口不兼容。升级时请按本文重新挂载和注册富文本插件,旧版数据需转换为本文说明的文档格式。
|
|
6
|
+
|
|
7
|
+
`2.1.0` 新增纸面全局坐标导出、识别标记、作业卡卷头和作答区域支持,沿用 `2.0.0` 的挂载及插件 API。详见 [更新记录](./CHANGELOG.md)。
|
|
8
|
+
|
|
9
|
+
## 安装与依赖
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install uni-answer-sheet@^2.1.0
|
|
13
|
+
# 富文本由业务方单独安装,不会随制卡包自动安装。
|
|
14
|
+
npm install uni-editor@^0.2.1
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`uni-answer-sheet` 不依赖、不打包、不自动下载 `uni-editor`,也没有自动安装编辑器的 peer dependency。`uni-editor/plugin` 从 **0.2.0** 提供,旧版 0.1.0 没有此入口。两个包各自包含内部所需运行时。
|
|
18
|
+
|
|
19
|
+
本地验证未发布的版本时,从仓库根目录分别安装构建生成的 `.tgz` 文件:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install ./artifacts/uni-answer-sheet-2.1.0.tgz ./artifacts/uni-editor-0.2.1.tgz
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 原生 JavaScript / TypeScript
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<div id="card" style="height: 800px; width: 100%"></div>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createAnswerSheet } from 'uni-answer-sheet';
|
|
33
|
+
import { createEditorPlugin } from 'uni-editor/plugin';
|
|
34
|
+
import 'uni-answer-sheet/style.css';
|
|
35
|
+
import 'uni-editor/style.css';
|
|
36
|
+
|
|
37
|
+
const sheet = createAnswerSheet('#card', {
|
|
38
|
+
plugins: [createEditorPlugin()],
|
|
39
|
+
// initialValue: previouslySavedData,
|
|
40
|
+
uploadImage: async file => {
|
|
41
|
+
const form = new FormData();
|
|
42
|
+
form.append('file', file);
|
|
43
|
+
const response = await fetch('/your-upload-endpoint', { method: 'POST', body: form });
|
|
44
|
+
if (!response.ok) throw new Error('上传失败');
|
|
45
|
+
const { url } = await response.json();
|
|
46
|
+
return url;
|
|
47
|
+
},
|
|
48
|
+
onChange(draft) { /* 草稿变化,不代表已经通过完整性校验。 */ },
|
|
49
|
+
onSave(result) { /* 内置「保存」按钮的结果;由业务方决定如何持久化。 */ },
|
|
50
|
+
onError(error) { console.error(error); }
|
|
51
|
+
});
|
|
52
|
+
await sheet.ready;
|
|
53
|
+
|
|
54
|
+
const result = await sheet.save();
|
|
55
|
+
if (result.ok) {
|
|
56
|
+
console.log(result.data); // 可直接 JSON.stringify;提交后端由业务方负责。
|
|
57
|
+
} else {
|
|
58
|
+
console.log(result.issues);
|
|
59
|
+
sheet.focusIssue(result.issues[0]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 路由离开或宿主组件卸载时:
|
|
63
|
+
sheet.destroy();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
未传 `initialValue` 且未调用 `initialize()` 时,从白纸开始并显示新建弹窗,页面布局仍可在制卡设置中选择。也可以调用兼容的新建方法(采用默认布局):
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
await sheet.newDocument({
|
|
70
|
+
mode: 'template', // 或 'exam'
|
|
71
|
+
name: '高一数学答题卡', grade: '高一', subject: '数学'
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
上传回调可以省略,此时图片以 data URL 存入文档。回调返回的 URL 必须可供浏览器加载;临时 `blob:` URL 不适合持久保存,会被完整性校验拦截。
|
|
76
|
+
|
|
77
|
+
## 初始化试卷和页面布局
|
|
78
|
+
|
|
79
|
+
接入方已完成试卷信息、页面布局设置时,在创建实例后**立即调用** `initialize()`,无需先等待 `ready`。首屏直接显示配置好的空白题卡,不会先闪出新建弹窗。
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const sheet = createAnswerSheet('#card', {
|
|
83
|
+
plugins: [createEditorPlugin()]
|
|
84
|
+
});
|
|
85
|
+
await sheet.initialize({
|
|
86
|
+
name: '高一数学期中试卷',
|
|
87
|
+
gradeCode: 'S01',
|
|
88
|
+
courseCode: 'MATHS',
|
|
89
|
+
mode: 'exam', // 可选,默认 template
|
|
90
|
+
layout: {
|
|
91
|
+
preset: 'a3-two', // a4 单栏 / a3-two 双栏 / a3-three 三栏
|
|
92
|
+
margin: 15, // 页边距,mm
|
|
93
|
+
gap: 10, // 栏间距,mm
|
|
94
|
+
questionCard: true,
|
|
95
|
+
scanMarking: false,
|
|
96
|
+
halfPoint: false,
|
|
97
|
+
choiceDirection: 'row', // row 横排 / column 竖排
|
|
98
|
+
mergeObjective: false
|
|
99
|
+
},
|
|
100
|
+
header: {
|
|
101
|
+
focusText: true, // 注意事项
|
|
102
|
+
writeKaohao: false, // 考场/座位号
|
|
103
|
+
missExam: false, // 填涂标记
|
|
104
|
+
mifeng: false, // 密封线
|
|
105
|
+
kaohao: 'school', // school 准考证号 / code 条形码
|
|
106
|
+
weishu: 8 // 准考证号位数,4–12 的整数
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- 推荐传 `name`、`gradeCode`、`courseCode`,名称去除首尾空格后最多 30 个字符。仍兼容 `{ name, grade: '高一', subject: '数学' }` 旧参数。`mode`、`layout`、`header` 可省略,默认 A3 双栏、15 mm 边距、10 mm 栏间距及默认卷头。
|
|
112
|
+
- 方法返回 `Promise<void>`,等待工作区及编辑器就绪。生成名称对应的卷头,初始没有题目;后续可在界面修改布局、添加题块。空卡仍无法通过 `save()` 的完整性校验。
|
|
113
|
+
- 未调用此方法时保留原有弹窗流程。若配置来自异步请求,先取到配置,再创建实例并调用 `initialize()`;若等弹窗已经显示后才调用,也会应用配置并关闭弹窗。
|
|
114
|
+
- 非法参数抛出 `AnswerSheetDataError`,`issues` 包含字段位置,当前题卡保持不变;不会用静默回退掩盖错误配置。
|
|
115
|
+
- **再次调用会新建空卡并替换当前内容**,同时清空旧卡的选中状态和撤销记录。恢复已有题卡请用 `load(savedData)` 或 `initialValue`,无需再初始化。
|
|
116
|
+
|
|
117
|
+
## 年级、课程与学科字典
|
|
118
|
+
|
|
119
|
+
沿用参考项目的三层关系:`gradeCode` 年级、`courseCode` 学校课程、`subjectCode` 标准学科。课程名称可自定义,题型可用范围和高中数学计分规则按 `subjectCode` 判断。保存结果在 `data.creation` 中同时保留编码和显示名称;回显不依赖当时的权限字典。
|
|
120
|
+
|
|
121
|
+
未传字典时,年级为 `P01`–`P06`(小学)、`J01`–`J03`(初中)、`S01`–`S03`(高中)。内置基础学科使用 `CHINESE`、`MATHS`、`ENGLISH`、`PHYSICAL`、`CHEMICAL`、`BIOLOGICAL`、`POLITICAL`、`HISTORY`、`GEOGRAPHIC`、`SCIENCE`、`IT`、`JAPANESE`,按参考项目的学段限制显示。这只是默认基础字典;学校实际课程和权限由接入方传入,插件不会请求后端。
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const sheet = createAnswerSheet('#card', {
|
|
125
|
+
plugins: [createEditorPlugin()],
|
|
126
|
+
dictionaries: {
|
|
127
|
+
// 可直接使用参考项目 allowGrades 的 value/label 选项。
|
|
128
|
+
grades: [{ value: 'S01', label: '高一' }],
|
|
129
|
+
// 对应 getGradeCourseMap;选项顺序由接入方决定。
|
|
130
|
+
gradeCourseMap: {
|
|
131
|
+
S01: [{ value: 'maths-school-01', label: '数学实验班' }]
|
|
132
|
+
},
|
|
133
|
+
// 对应 getCourseCodeToSubjectCode / getCourseToSubjectObj。
|
|
134
|
+
courseCodeToSubjectCode: { 'maths-school-01': 'MATHS' },
|
|
135
|
+
defaultGradeCode: 'S01',
|
|
136
|
+
defaultCourseCode: 'maths-school-01'
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
await sheet.initialize({
|
|
140
|
+
name: '高一数学试卷', mode: 'exam',
|
|
141
|
+
gradeCode: 'S01', courseCode: 'maths-school-01',
|
|
142
|
+
exampaperId: 'paper-123', createMode: 'MANUALLY',
|
|
143
|
+
layout: { pageNum: 2, quesCard: true, choiceType: false }
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- 课程选项也可直接携带 `subjectCode`;若同时传映射表,两者须一致。自定义课程必须有明确学科映射,不从名称或编码前缀猜测。
|
|
148
|
+
- 新建弹窗过滤没有课程的年级;默认选指定年级/课程,无效默认值退到首个可用项。切换年级后选该年级第一门课程,与参考项目一致。
|
|
149
|
+
- 显式传 `grades: []` 或 `gradeCourseMap: {}` 表示没有可用项,不会回退到全部课程。异步取到字典后可 `sheet.setOptions({ dictionaries })`;完整替换字典,不影响当前题卡内容。若需要首屏免弹窗,应先取到字典再挂载并立即初始化。
|
|
150
|
+
- 字典选项使用 `{ value, label }`;原始 `GRADE` 字典的 `{ dictCode, dictValue }` 需由接入方转换。自定义年级可带 `classify: 'P' | 'J' | 'S'`;`J01-NEW`、`S01-NEW` 可作为接入方选项传入。
|
|
151
|
+
- 编码初始化检查年级/课程是否可用;同时传名称或 `subjectCode` 时必须与字典一致。传入字典后,旧名称初始化也必须匹配字典,同名课程需改用编码。非法字典抛出带字段路径的 `AnswerSheetDataError`,不替换当前配置。
|
|
152
|
+
- `id`(模板 ID)、`exampaperId`(参考项目的考试试卷 ID 拼写)、`createMode`(`MANUALLY` / `IMPORTED` / `THIRD`)是可选业务元数据,原样保存,不触发服务端创建或导入。
|
|
153
|
+
- 未调用初始化时,弹窗兼容路由 `gradeCode`、`courseCode`、`name`、`mode`、`id`、`exampaperId` 以及旧的 `grade`、`subject`;无效路由选项回退到当前可用项。
|
|
154
|
+
|
|
155
|
+
参考布局字段可直接放在 `layout`:
|
|
156
|
+
|
|
157
|
+
| 参考字段 | 当前文档字段 | 值含义 |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| `pageNum` | `preset` | `1` → `a4`,`2` → `a3-two`,`3` → `a3-three` |
|
|
160
|
+
| `quesCard` | `questionCard` | 题卡合一 |
|
|
161
|
+
| `scoreType` | `halfPoint` | 允许半分 |
|
|
162
|
+
| `choiceType` | `choiceDirection` | `false` 横排 / `row`,`true` 竖排 / `column` |
|
|
163
|
+
| `mixOb` | `mergeObjective` | 客观题合并 |
|
|
164
|
+
|
|
165
|
+
别名仅用于初始化输入,保存仍使用当前文档字段;同时传两种字段时必须一致。卷头的 `focusText`、`writeKaohao`、`missExam`、`mifeng`、`kaohao`、`weishu` 已与参考项目同名,放在 `header`。参考项目 `answerInfo.courseCode` 是二维码显示开关,对应这里的 `recognition.examQrCode`,和课程编码 `courseCode` 分开。当前不接收整个 `pageinfoJsonstr` 作为初始化配置。
|
|
166
|
+
|
|
167
|
+
## 保存与完整性
|
|
168
|
+
|
|
169
|
+
`save(): Promise<SaveResult>` 等待输入、图片上传、字体加载和分页测量稳定,然后进行校验。连续保存调用共享当前进行中的保存过程。不会自动请求后端、下载文件或写 localStorage。
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
type SaveResult =
|
|
173
|
+
| { ok: true; data: AnswerSheetData }
|
|
174
|
+
| { ok: false; issues: ValidationIssue[] };
|
|
175
|
+
|
|
176
|
+
interface ValidationIssue {
|
|
177
|
+
code: string;
|
|
178
|
+
path: string;
|
|
179
|
+
message: string;
|
|
180
|
+
blockId?: string;
|
|
181
|
+
questionId?: string;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
成功数据包含原始 `CardDocument`(`version: 1`、创建信息、卷头、题块、富文本、题型配置)、参考项目格式的 `sectionInfoList`、`quesTextMap`,以及当前排版的 `recognitionData`。稳定题目 ID 与展示题号分别保留。完整数据可直接通过 `initialValue` 或 `load()` 回读;导出的对象是独立副本,外部修改不会影响当前题卡。
|
|
186
|
+
|
|
187
|
+
校验包括:
|
|
188
|
+
|
|
189
|
+
- 名称、年级、学科、纸面标题,以及非空题块/题目。
|
|
190
|
+
- 题块/题目/选项 ID、组内题号、题型、选项数、子题数、分值和子题总分。
|
|
191
|
+
- 少选得分、选做数量、断句评分、作文格数/行数等题型约束。
|
|
192
|
+
- 富文本结构、空公式、图片地址和可见图片加载失败。
|
|
193
|
+
- 题卡合一的题干、材料、选项内容,以及所选填涂位置的对应关系。普通答题卡不要求填写题干,也不要求提供正确答案。
|
|
194
|
+
- 纸面标题、不可拆分内容的纵向溢出,以及未分片编辑区域的横向溢出。
|
|
195
|
+
- 定位点及识别区域是否在纸面内、标记是否重叠、纸面编码是否超出 8 面、二维码 ID 和学科识别编码是否有效。
|
|
196
|
+
|
|
197
|
+
无法在 8 秒内完成输入/上传或稳定测量时返回 `EDITOR_BUSY` / `LAYOUT_PENDING`;容器隐藏或未设置有效尺寸时返回 `LAYOUT_UNAVAILABLE`,不会跳过布局校验。校验是当前数据和实际渲染的完整性检查,不判断学科内容或答案是否正确。外部图片/字体稍后失效仍需业务方管理。
|
|
198
|
+
|
|
199
|
+
结构非法的数据在加载前抛出 `AnswerSheetDataError`,其 `issues` 提供字段位置;缺少题干、分值等可编辑草稿允许加载,但不能通过保存校验。实例未就绪/已销毁属于调用错误,会抛异常。
|
|
200
|
+
|
|
201
|
+
## 定位标记与识别坐标
|
|
202
|
+
|
|
203
|
+
新建后的纸面自动显示四角定位点,上方 26×13 px、下方 13×26 px。纸面编码使用参考项目的三方格映射,最多区分 **8 个物理纸面**,不按栏计数;超过时保存返回 `PAGE_CODE_LIMIT`,不会循环使用编码。学科标记沿用参考项目 15 个标准学科的四方格映射,仅显示在正面。二维码编码内容为 `creation.exampaperId` 本身。
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
await sheet.initialize({
|
|
207
|
+
name: '期中考试', gradeCode: 'S01', courseCode: 'MATHS',
|
|
208
|
+
exampaperId: 'paper-123',
|
|
209
|
+
recognition: {
|
|
210
|
+
examQrCode: true, // 默认 false
|
|
211
|
+
subjectMarkers: true, // 默认 true;自定义学科无标准编码时可关闭
|
|
212
|
+
pointBlack: false // 参考项目考号区的辅助识别点,默认 false
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
const saved = await sheet.save();
|
|
216
|
+
if (saved.ok) {
|
|
217
|
+
const coordinates = saved.data.recognitionData;
|
|
218
|
+
// 或单独获取:await sheet.getRecognitionData()
|
|
219
|
+
// 同样等待排版并校验,不通过时抛出 AnswerSheetDataError。
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
坐标结构:
|
|
224
|
+
|
|
225
|
+
- `version: 2`、`unit: 'px'`、`dpi: 96`、`origin: 'topLeftPaper'`。这是坐标协议版本,题卡文档的 `version: 1` 不变。
|
|
226
|
+
- `pages[]` 每项对应一个物理纸面,`pageNumber` 从 1 开始;A3 的两栏/三栏属于同一项。
|
|
227
|
+
- 每面包含 `pageSize`、`anchorOrigin`、`anchorSize`、`panelSize`、四个 `anchors` 和 `regions[]`。`anchorOrigin` 仅记录左上定位点在纸面上的位置,与 `anchors.topLeft.x/y` 一致;它不再充当坐标原点。
|
|
228
|
+
- **所有区域、单元格及四个定位点都使用所在物理纸面左上角 `(0, 0)` 的全局坐标**,向右为 X 正方向、向下为 Y 正方向。`rect: { x, y, width, height }` 已包含纸面边距和栏位置,可直接使用,不需要累加定位点或父区域偏移。
|
|
229
|
+
- 每张物理纸面独立从 `(0, 0)` 开始;不包含视口缩放、滚动、上一张纸的高度或纸张之间的展示间距。密封线条码、顶部二维码仍在纸张内,坐标无需使用负值。
|
|
230
|
+
- 区域通过 `blockId`、`questionId`、`partId`、`answerId`、`optionId` 关联源数据;`role` 区分 `region`、`cell`、`line`。跨页/跨栏区域分开记录并保留相同题目 ID,选做共享区域另有 `questionIds`。
|
|
231
|
+
- `sectionType` 包括 `ClassId`、`StudentId`、`IgnoreFlag`、`MultiChoice`、`Checkbox`、`FrameOnly`、`ScoreBox`、`QuestionChoice`、`Blank`、`PageCode`、`SubjectCode`、`ExamQrCode`、`ReferencePoint`。填涂区域包含实际单元格坐标,不依赖等宽假设。
|
|
232
|
+
|
|
233
|
+
读取单元格示例:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
const coordinates = await sheet.getRecognitionData();
|
|
237
|
+
const paper = coordinates.pages.find(page => page.pageNumber === 2);
|
|
238
|
+
const option = paper?.regions.find(region => region.optionId === 'option-A');
|
|
239
|
+
if (option) {
|
|
240
|
+
// 直接取第二张物理纸面上的位置,parentId 只表示归属。
|
|
241
|
+
// drawBoxOnPaper 代表宿主自己的绘制方法。
|
|
242
|
+
const { x, y, width, height } = option.rect;
|
|
243
|
+
drawBoxOnPaper(2, x, y, width, height);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
这是扁平的制卡坐标协议,不是旧版 `cutparamJsonstr2` 的嵌套结构。回显时忽略传入的 `recognitionData`(包括旧 `version: 1 / topLeftAnchor` 数据),保存时按当前字体、内容和版式重算并返回版本2。独立答题卡临时“编辑题目”时,保存会结束临时编辑并测量正式纸面。
|
|
248
|
+
|
|
249
|
+
换算毫米时,矩形各数值乘 `25.4 / 96`;换算以左上角为原点的 PDF 点坐标时乘 `72 / 96`。若 PDF 库以左下角为原点,矩形下边缘的 Y 应使用 `(paper.pageSize.height - y - height) * 72 / 96`。定位点同样保存全局坐标,供扫描端做图像配准。最终 PDF 识别应使用 **PDF 渲染浏览器重新测量的坐标**。
|
|
250
|
+
|
|
251
|
+
## 作业卡卷头与无分值批阅
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
await sheet.initialize({
|
|
255
|
+
name: '数学作业', gradeCode: 'J01', courseCode: 'MATHS',
|
|
256
|
+
layout: { usage: 'homework', preset: 'a3-two', scanMarking: false },
|
|
257
|
+
header: { tiaomaStyle: 1, classText: true }
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`layout.usage` 为 `exam`(默认)或 `homework`,独立于新建入口的 `mode`。界面可在“页面布局 → 题卡用途”切换。作业卷头的 `tiaomaStyle` 沿用参考字段:`1` 为班级/两位学号填涂,`2` 为条码;`classText` 默认 `true`,控制班级填涂区。考试的考号位数、密封线等配置保留,切回考试时恢复。
|
|
262
|
+
|
|
263
|
+
作业题配置可勾选“无分值”,保存为 `score: 0`、`itemScores: [0, ...]`;`null` 仍表示未配置,保存校验不通过。手阅无分值题使用参考项目的半对/错批阅框,多小题共用答题外框并显示 `(1)`、`(2)` 等批阅框。网阅隐藏手阅批阅框。跨页/跨栏时,仅首段保留批阅框,续段为 `FrameOnly`。
|
|
264
|
+
|
|
265
|
+
坐标新增 `ClassId`;班级和两位学号各包含 20 个 `cell`,`value` 如 `1:9` 表示十位的数字 9,`2:0` 表示个位的数字 0,显示顺序为 9→0。选择题和填空题完整分区外框使用 `scope: 'section'`,内部题目区域、作答线和填涂格通过 `parentId` 建立层级;**所有层级的矩形都使用同一纸面全局坐标,父子关系不影响坐标值**。
|
|
266
|
+
|
|
267
|
+
断句题的题干字母框与普通选择题一样按 `optionId` 导出;没有选项正文的断句题不提供“各选项前空方框”设置。制卡适配层兼容 `uni-editor@0.2.0` 的题干字母框坐标,`uni-editor@0.2.1` 在 `getAnswerRegions()` 中直接提供这些区域,并修复固定行高的累计排版偏移。
|
|
268
|
+
|
|
269
|
+
## 禁答区与空白答题区
|
|
270
|
+
|
|
271
|
+
“添加题块”弹窗的纸面区域入口,普通答题卡显示“禁止答题区”,题卡合一显示“空白答题区”。区域可编辑文字、拖动调整高度、复制、删除和调整顺序。源数据存为 `kind: 'area'`、`areaType: 'forbiden' | 'editArea'`、`answerHeight`、`document`,保留参考项目 `forbiden` 的拼写。
|
|
272
|
+
|
|
273
|
+
默认禁答区高 100 px,空白答题区高 200 px。禁答区与参考项目一致,不能拆分,高度不超过一栏,剩余空间不足时整块换栏;空白答题区可连续跨栏/跨页。它们不属于题目,不进入 `sectionInfoList`,需用完整保存数据回显。切换题卡合一时,与参考项目一样删除不适用的纸面区域。仅含纸面区域的卡不能作为完整试卷保存。
|
|
274
|
+
|
|
275
|
+
## 回显已保存的题卡
|
|
276
|
+
|
|
277
|
+
将之前 `save()` 成功返回的 **`result.data` 整体保存**,回显时传给 `load(data): Promise<void>`。JSON 字符串先 `JSON.parse()`,不要传 `{ ok, data }` 外层结果,也不要只传 `sectionInfoList`。
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// savedData 是业务方从自己的存储中取回的完整对象。
|
|
281
|
+
await sheet.load(savedData);
|
|
282
|
+
// 回显后可继续编辑;再次保存仍会检查完整性。
|
|
283
|
+
const result = await sheet.save();
|
|
284
|
+
if (result.ok) {
|
|
285
|
+
const json = JSON.stringify(result.data);
|
|
286
|
+
// 业务方自行持久化 json。
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
首次挂载即可回显,无需先创建空白卡:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const sheet = createAnswerSheet('#card', {
|
|
294
|
+
plugins: [createEditorPlugin()],
|
|
295
|
+
initialValue: savedData
|
|
296
|
+
});
|
|
297
|
+
await sheet.ready;
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`load()` 替换整张卡,恢复纸张/分栏、名称与卷头、题块顺序、题型配置、富文本和题目 ID;分页按恢复的内容重新计算。旧卡的选中状态、设置弹窗和撤销记录会清空;插件、上传回调、只读设置与事件回调沿用当前实例。数据以副本载入,不会与外部对象共享修改。
|
|
301
|
+
|
|
302
|
+
`load()` 可在 `ready` 前调用,会等待工作区和编辑器初始化。它只检查数据结构,允许回显未完成草稿;输入结构非法时抛出 `AnswerSheetDataError`,原卡保持不变。字体、图片和分页是否完整可用仍由 `validate()` / `save()` 检查。若连续调用 `load()`,最后一次为准,被替换的调用以 `AbortError` 结束。保留的 `setDocument()` 与 `load()` 行为相同。
|
|
303
|
+
|
|
304
|
+
## API
|
|
305
|
+
|
|
306
|
+
| 方法 | 用途 |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
| `ready: Promise<void>` | 等待工作区挂载;`save()` 自行继续等待各编辑器和布局就绪 |
|
|
309
|
+
| `initialize(options)` | 设置试卷信息、布局和卷头并新建空卡;创建实例后立即调用可跳过新建弹窗 |
|
|
310
|
+
| `getDocument()` | 当前草稿副本,不做完整性校验 |
|
|
311
|
+
| `load(data)` | 回显完整保存数据或草稿,清空旧卡的编辑状态 |
|
|
312
|
+
| `setDocument(value)` | `load()` 的兼容方法 |
|
|
313
|
+
| `newDocument(creation)` | 新建空白题卡 |
|
|
314
|
+
| `getReferenceData()` | 返回 `sectionInfoList` / `quesTextMap` |
|
|
315
|
+
| `setReferenceData(data)` | 接入参考项目的题目结构,保留当前纸张和卷头 |
|
|
316
|
+
| `validate()` | 返回 `{ valid, issues }`,不导出数据 |
|
|
317
|
+
| `save()` | 校验并返回完整保存数据 |
|
|
318
|
+
| `focusIssue(issue)` | 定位对应题块并打开设置 |
|
|
319
|
+
| `setOptions({ readOnly, uploadImage, onChange, onSave, onError })` | 更新行为,不重建文档;插件不可中途替换 |
|
|
320
|
+
| `destroy()` | 卸载画布、编辑器、菜单和监听 |
|
|
321
|
+
|
|
322
|
+
内置保存按钮触发 `onSave`。直接调用 `sheet.save()` 时从返回值接收结果,不额外触发该回调。
|
|
323
|
+
|
|
324
|
+
## Vue 3
|
|
325
|
+
|
|
326
|
+
```vue
|
|
327
|
+
<script setup lang="ts">
|
|
328
|
+
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
|
329
|
+
import { createAnswerSheet, type AnswerSheetInstance } from 'uni-answer-sheet';
|
|
330
|
+
import { createEditorPlugin } from 'uni-editor/plugin';
|
|
331
|
+
import 'uni-answer-sheet/style.css';
|
|
332
|
+
import 'uni-editor/style.css';
|
|
333
|
+
|
|
334
|
+
const container = ref<HTMLElement>();
|
|
335
|
+
let sheet: AnswerSheetInstance | undefined;
|
|
336
|
+
onMounted(() => { sheet = createAnswerSheet(container.value!, { plugins: [createEditorPlugin()] }); });
|
|
337
|
+
onBeforeUnmount(() => sheet?.destroy());
|
|
338
|
+
defineExpose({ save: () => sheet!.save() });
|
|
339
|
+
</script>
|
|
340
|
+
<template><div ref="container" style="height: 800px" /></template>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## React
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
import { useEffect, useRef } from 'react';
|
|
347
|
+
import { createAnswerSheet, type AnswerSheetInstance } from 'uni-answer-sheet';
|
|
348
|
+
import { createEditorPlugin } from 'uni-editor/plugin';
|
|
349
|
+
import 'uni-answer-sheet/style.css';
|
|
350
|
+
import 'uni-editor/style.css';
|
|
351
|
+
|
|
352
|
+
export function CardDesigner() {
|
|
353
|
+
const container = useRef<HTMLDivElement>(null);
|
|
354
|
+
const instance = useRef<AnswerSheetInstance | null>(null);
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
const sheet = createAnswerSheet(container.current!, { plugins: [createEditorPlugin()] });
|
|
357
|
+
instance.current = sheet;
|
|
358
|
+
return () => { instance.current = null; sheet.destroy(); };
|
|
359
|
+
}, []);
|
|
360
|
+
return <div ref={container} style={{ height: 800 }} />;
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## 不使用构建工具
|
|
365
|
+
|
|
366
|
+
将两个包各自的 `dist/` 完整复制到不同目录,保留 `assets/` 字体和 CSS 相对路径。不要只复制 JavaScript。
|
|
367
|
+
|
|
368
|
+
```html
|
|
369
|
+
<link rel="stylesheet" href="/vendor/card/style.css">
|
|
370
|
+
<link rel="stylesheet" href="/vendor/editor/style.css">
|
|
371
|
+
<div id="card" style="height:800px"></div>
|
|
372
|
+
<script src="/vendor/editor/browser.js"></script>
|
|
373
|
+
<script src="/vendor/card/browser.js"></script>
|
|
374
|
+
<script>
|
|
375
|
+
const sheet = UniAnswerSheet.createAnswerSheet('#card', {
|
|
376
|
+
plugins: [UniEditor.createEditorPlugin()]
|
|
377
|
+
});
|
|
378
|
+
</script>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## 使用边界与插件协议
|
|
382
|
+
|
|
383
|
+
支持现代浏览器的 DOM 环境。SSR 可以导入模块,但须在客户端挂载后创建实例。样式限定在工作区及编辑器菜单内,不设置宿主 `body` 的宽高或滚动。
|
|
384
|
+
|
|
385
|
+
当前工作区使用一组页面导航/全屏 ID,**同一 document 同时只允许一个制卡实例**;第二次创建会明确报错。切换页面前销毁旧实例即可。独立富文本实例数量不受此限制。原生小程序、服务端排版不在支持范围内。
|
|
386
|
+
|
|
387
|
+
协议类型 `AnswerSheetEditorPlugin` 可直接从制卡包导入,使用结构类型,无跨包运行时依赖。v1 要求分页切片、内容身份、填涂绑定、作答区域、HTML 转换和忙碌状态能力,并约定 `.rt-content` 测量表面。`prepare()` 在制卡挂载时自动调用;缺少插件、重复注册或版本不兼容会立即报错。其他编辑器需完整实现该协议才能替换,不能仅实现一个文本输入框。
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Reference project assets
|
|
2
|
+
|
|
3
|
+
The homework marking glyphs in `src/card/reference/HomeworkMarking.tsx` use the exact outlines of the reference project's iconfont characters U+E6CF (`piyue-banduicuo`) and U+E6D1 (`piyue-cuo`).
|
|
4
|
+
|
|
5
|
+
Source: `web-user/src/style/global.scss`, iconfont project 1458451, font file `font_1458451_5u779h2hy9n.ttf` (the reference project's configured asset). They are embedded as local SVG paths; no runtime request to the font service is made.
|
|
6
|
+
|
|
7
|
+
These are reference-project assets, not glyphs supplied under a bundled npm dependency's license.
|