Officia.Editor — 在线编辑
能力摘要(AI 友好):
OfficiaEditor把文档交给浏览器端编辑器再把结果写回文件。 Words 侧open(docx/doc)→officia-doc/1JSON、save(json, DOCX|PDF)写回; Cells 侧openWorkbook(xlsx)/recalc/saveWorkbook是一条 JSON in → JSON out 的闭环。 前端产物officia-editor.js打在同一个 jar 的META-INF/resources/下随包分发。 没有 Document Server、没有独立进程、没有新端口——客户只多引一个 jar。
别的在线编辑方案里,Document Server 承担的「转换 / 保存回调」,在这里就是两次普通方法调用。
引入
在线编辑随聚合包 officia-all 一起分发,不需要额外的 Maven 坐标—— 引入方式与其它能力完全一致,见快速开始。
版本要求
Officia.Editor 是 1.1.3 之后新增的模块。用 1.1.3 或更早的 officia-all 会 import 不到 plus.ruoyi.officia.editor.OfficiaEditor,也取不到 /officia-editor/officia-editor.js。请对照更新日志确认你用的版本已包含本模块。
打开:文档 → JSON
import plus.ruoyi.officia.editor.OfficiaEditor;
import plus.ruoyi.officia.editor.EditorDocument;
import plus.ruoyi.officia.editor.EditorFormat;
EditorDocument doc = OfficiaEditor.open(docxBytes); // docx / doc 按内容嗅探,不看扩展名
String json = doc.getJson(); // officia-doc/1 契约,直接回给前端
doc.getSourceFormat(); // "docx" 或 "doc"
doc.getBlockCount(); // 顶层块数(不是页数——页数要排版后才知道)
doc.getWarnings(); // 解析告警;先 hasWarnings() 判一下这份 JSON 里除了内容,还带着逐码位的字体前进宽度——所以前端不必再向服务端要任何东西 就能自己排版渲染。
两端排版一致是工程目标,不是数学保证
浏览器里的排版与 officia 服务端的排版力求一致,编辑器的「检查」面板会把两个页数并排显示, 让差异随时可见。要看服务端排出来的真实效果,用 OfficiaEditor.toImages(json) 逐页出图比对。
保存:JSON → docx / PDF
byte[] docx = OfficiaEditor.save(json, EditorFormat.DOCX); // 语义 docx,可被 Word 再打开继续编辑
byte[] pdf = OfficiaEditor.save(json, EditorFormat.PDF); // 与 OfficiaWords.toPdf 同一条排版渲染链EditorFormat 只有 DOCX 与 PDF 两个常量。工作簿不走 save,见下一节。
工作簿:打开 / 重算 / 保存
String wbJson = OfficiaEditor.openWorkbook(xlsxBytes); // officia-workbook/1 契约
String next = OfficiaEditor.recalc(wbJson); // 重算全部公式格
byte[] xlsx = OfficiaEditor.saveWorkbook(next); // 写回 xlsxrecalc 用的就是 Officia.Cells 那一个公式引擎
浏览器端没有另写一套 JS 求值器。编辑器里看到的数字与 OfficiaCells.toPdf(xlsx) 算出来的因此必然一致——「预览和导出对不上」这类最难自证的问题在结构上被排除了。 算不出来的格写成 #VALUE!,不让整张表失败;失败原因附在返回 JSON 的根级可选字段 recalcErrors([{sheet, ref, message}],无失败时不出现)。循环引用由求值栈检出并落成错误格,不会栈溢出。
saveWorkbook 不会自动重算
什么时候重算是调用方的节奏问题(前端多为改一格 debounce 调一次 recalc)。 保存时再算一遍,会让「存下来的数字」与「用户刚在屏幕上确认过的数字」出自两次不同的求值。 需要落盘前兜底就自己串一下:saveWorkbook(recalc(json))。
前端:officia-editor.js
产物打在 jar 的 META-INF/resources/officia-editor/officia-editor.js——Servlet 3.0 规范约定的 静态资源目录,Spring Boot 等 Servlet 3.0+ 容器会自动把它暴露出来,你不需要拷贝任何文件:
<script src="/officia-editor/officia-editor.js"></script>脚本加载后挂一个全局 OfficiaEditor(OfficiaEditor.version 可读版本号)。 不是 Servlet 容器时,自己把 /officia-editor/* 映射到 classpath 的 /META-INF/resources/officia-editor/* 即可。
挂一个能打字的文档
const view = OfficiaEditor.mountEditable(host, json, {
zoom: 1,
onChange: (state) => refresh(state), // 每次内容或选区变化回调,供宿主刷新工具栏
});
view.pageCount; // 前端排版页数
view.fontMisses; // 字体度量缺口;>0 说明下发的字体表不全
view.json(); // 当前文档 JSON —— 存盘时把它交回 Java 侧
view.focus(); view.undo(); view.redo(); view.destroy();OfficiaEditor.mount(...) 是只读渲染,mountEditable 才可编辑。
功能区、属性面板、网格
// 功能区:六个标签的真工具栏,命令派发交给库里的命令表
const ribbon = OfficiaEditor.mountRibbon(host, {
surface: 'words', // 或 'cells'
onCommand: (e) => OfficiaEditor.runWordsCommand(view, e), // Cells 侧用 runCellsCommand(editor, e)
});
// 外壳:右侧属性面板 + 底部状态栏
const shell = OfficiaEditor.mountWordsShell(panelHost, statusHost, { view, onZoom });
// 工作簿网格
const editor = OfficiaEditor.Cells.mountWorkbook(host, workbookJson);样式取 OfficiaEditor.RIBBON_CSS 与 OfficiaEditor.SHELL_CSS。 Cells 侧的全部对外面收在 OfficiaEditor.Cells.* 命名空间下——Words 与 Cells 是两份并列的契约, 两边都有「合并单元格」「设列宽」,说的却是不同的东西,摊到一起必然撞名。
runWordsCommand 返回 false 不是异常
false 表示这次点击没有改动文档:光标不在可编辑处、选项值解析不出来, 或这个控件按设计该由宿主处理(缩放、视图开关、插图要弹文件框)。 OfficiaEditor.WORDS_UNWIRED / CELLS_UNWIRED 是一张 Map<控件名, 原因>, 可以把原因直接显示给用户,而不是静默吞掉一次点击。
属性面板与状态栏
| 界面 | 属性面板标签 |
|---|---|
| Words | 段落 · 表格 · 页面 · 检查 |
| Cells | 单元格 · 工作表 · 页面 · 检查 |
面板自己不存任何值,每个字段都声明「怎么从文档读」「怎么写回一条命令」, 所以「光标移到别的段落还留着上一段的值」这种失真在结构上不可能发生。 「检查」页显示往返 diff、只读透传对象数、字体度量缺口,以及编辑器页数与 officia 排版页数是否相等。
方法一览
| 方法 | 说明 |
|---|---|
open(docx)(byte[] / InputStream / File) | 文档 → officia-doc/1 JSON(EditorDocument) |
open(docx, options) | 带转换选项(字体目录等) |
save(json, format) | JSON → docx / PDF 字节 |
save(json, format, options) | 带转换选项(仅 PDF 用得上) |
save(json, format, out)(OutputStream / File) | 直写流或文件;不关闭调用方传入的流 |
toImages(json[, options]) | 每页一张 PNG,按页序 → List<byte[]> |
openWorkbook(xlsx) | 工作簿 → officia-workbook/1 JSON |
recalc(workbookJson) | 重算全部公式格,返回同契约 JSON |
saveWorkbook(workbookJson) | 工作簿 JSON → xlsx 字节 |
能力边界
工具栏上的每一个控件都有一个机检过的状态,这就是能力承诺本身:
| 状态 | 含义 | Words | Cells |
|---|---|---|---|
| 正常(可点) | 模型里有这个东西,且保存回目标格式时语义仍在 | 53 | 47 |
| 受限(只读) | 只读透传 / 不属于文档内容 / 可编辑但导出时降级 | 14 | 0 |
| 不可用(置灰) | 模型层就没有这个东西 | 11 | 20 |
| 合计 | 78 | 67 |
本轮明确不做:
- 实时协同(多人同改一份)——必然要服务与长连接。命令层已设计成可序列化操作日志,为将来留门。
- 修订 / 批注 / 脚注的编辑(打开保留、存回不丢,但改不了)。
- 浮动对象拖拽、图文环绕的编辑(只读)。
- 演示文稿(pptx)在线编辑——pptx 仍可用 Officia.Slides 转 PDF。
- 前端引入任何 UI 框架(零依赖对前端产物同样适用)。
一个容易被问到的字节差异
save(json, PDF) 与 OfficiaWords.toPdf(docx) 走同一条排版渲染链,从同一份内容出发必然产出相同字节。 但 toPdf(docx) 会额外把 docx 内嵌的字体叠加进字体表,而 save 只拿得到 JSON、拿不到原始 docx 包, 用的是基础字体表——所以对内嵌了字体的文档,两者字节可以不同。这是信息量差异,不是缺陷。
常见问题
- 要不要另外部署一个文档服务? 不要。
open/save是两次普通方法调用,没有新进程、没有新端口。 officia-editor.js从哪里下载? 不用下载,它随 jar 分发;Servlet 3.0+ 容器会自动把/officia-editor/officia-editor.js暴露出来。- 能离线用吗? 能。
officia-editor.js不依赖任何 URL,给它 JSON 字符串就能跑,file://也行; Electron / 桌面壳 / 纯静态页都能嵌。 - 存成 PDF 后中文变方块? 与其它模块同一个原因与解法,见 字体替换诊断。
- 输出带水印、被限页? 那是未授权的评估态,见授权与加载。