Skip to content

Officia.Editor — 在线编辑

能力摘要(AI 友好)OfficiaEditor 把文档交给浏览器端编辑器再把结果写回文件。 Words 侧 open(docx/doc)officia-doc/1 JSON、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

java
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

java
byte[] docx = OfficiaEditor.save(json, EditorFormat.DOCX);   // 语义 docx,可被 Word 再打开继续编辑
byte[] pdf  = OfficiaEditor.save(json, EditorFormat.PDF);    // 与 OfficiaWords.toPdf 同一条排版渲染链

EditorFormat 只有 DOCXPDF 两个常量。工作簿不走 save,见下一节。

工作簿:打开 / 重算 / 保存

java
String wbJson = OfficiaEditor.openWorkbook(xlsxBytes);   // officia-workbook/1 契约
String next   = OfficiaEditor.recalc(wbJson);            // 重算全部公式格
byte[] xlsx   = OfficiaEditor.saveWorkbook(next);        // 写回 xlsx

recalc 用的就是 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+ 容器会自动把它暴露出来,你不需要拷贝任何文件:

html
<script src="/officia-editor/officia-editor.js"></script>

脚本加载后挂一个全局 OfficiaEditorOfficiaEditor.version 可读版本号)。 不是 Servlet 容器时,自己把 /officia-editor/* 映射到 classpath 的 /META-INF/resources/officia-editor/* 即可。

挂一个能打字的文档

js
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 才可编辑。

功能区、属性面板、网格

js
// 功能区:六个标签的真工具栏,命令派发交给库里的命令表
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_CSSOfficiaEditor.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 字节

能力边界

工具栏上的每一个控件都有一个机检过的状态,这就是能力承诺本身:

状态含义WordsCells
正常(可点)模型里有这个东西,且保存回目标格式时语义仍在5347
受限(只读)只读透传 / 不属于文档内容 / 可编辑但导出时降级140
不可用(置灰)模型层就没有这个东西1120
合计7867

本轮明确不做

  • 实时协同(多人同改一份)——必然要服务与长连接。命令层已设计成可序列化操作日志,为将来留门。
  • 修订 / 批注 / 脚注的编辑(打开保留、存回不丢,但改不了)。
  • 浮动对象拖拽、图文环绕的编辑(只读)。
  • 演示文稿(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 后中文变方块? 与其它模块同一个原因与解法,见 字体替换诊断
  • 输出带水印、被限页? 那是未授权的评估态,见授权与加载