自定义字段类型
字段插件是价值密度最高的贡献点:JSON 字段、富文本字段、Excel 公式字段都是它做的。本篇以内置的 JSON 字段(plugins/marketplace/json-field/)为底走一遍完整闭环。
存储契约:骑在基础类型上
Section titled “存储契约:骑在基础类型上”先立住一个观念:你不发明新的存储类型。声明 baseType(text / number / date / richtext)后,字段在引擎里就是那个基础类型 —— 排序、筛选、移动端、没装你插件的协同端全按基础类型工作。你的插件只接管两件事:格子怎么显示、双击之后怎么编辑。
字段的插件身份与配置存在 field.properties.xPluginField({ pluginId, typeKey, version, config, summaryPath }),由宿主管理。这带来的用户保障:你的插件卸载后,数据完好 —— 格子退回基础类型显示(配合 summaryPath 还能显示摘要)。
JSON 字段的真实 manifest(略去次要字段):
{ "id": "xinghan.json-field", "runtime": "static", "ui": "ui/dist", "capabilities": [], "contributes": { "views": [ { "id": "editor", "title": "JSON", "icon": "braces", "location": "floating", "entry": "ui/index.html#/editor" } ], "fieldTypes": [ { "typeKey": "jsonDoc", "name": { "zh-CN": "JSON", "en-US": "JSON" }, "icon": "braces", "baseType": "text", "editor": { "view": "editor", "size": { "width": 760, "height": 560 }, "placement": "center" }, "validation": { "maxBytes": 1048576 } } ] }}注意两点:
- 字段插件可以是纯
static—— 编辑器只是一块 iframe,值的写入由宿主完成,你不需要后端; editor.view指向views里声明的一个视图(这里是floating位置的editor)。
声明生效后,「新建字段」的类型选择器里会出现你的类型(归属于你的插件);hideBaseTypeInPicker: true 还能把对应基础类型从选择器里顶掉(“升级替代”语义,插件版富文本就是这么做的)。
用户双击你类型的格子 → 宿主打开 editor.view 的 iframe 模态框,并把上下文推进来:
// context 形态(经 SDK 的 context 桥送达)interface EditorContext { projectId: string tableNodeId: string recordId: string fieldId: string value: string | null // cell 当前值(baseType 决定形态,text 就是字符串) config: Record<string, unknown> // 本字段的插件配置(fieldConfig 面板产出) readonly: boolean}编辑器里三步走 —— 装桥、等上下文、resolve 回值:
一个完整的最小编辑器(ui/index.html#/editor 指向的页面,纯 ESM 无构建):
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8" /><style> body { margin: 0; padding: 12px; background: var(--background, #fff); } textarea { width: 100%; height: 70vh; font-family: monospace; background: var(--muted, #f5f5f5); color: var(--foreground, #333); border: 1px solid var(--border, #ddd); border-radius: 6px; }</style></head><body> <textarea id="input"></textarea> <script type="module"> import { installThemeBridge, installContextBridge, getContext, onContextChange, ui, } from '@xinghan/plugin-sdk/ui'
installThemeBridge() // 跟宿主亮暗与语义色 installContextBridge()
const input = document.getElementById('input') let ready = false
// 等 context/init 到达(iframe load 后宿主 rAF 推送,含本次编辑的业务上下文) function init() { const ctx = getContext() if (ready || ctx.recordId === undefined) return // 编辑器 context 还没到 ready = true input.value = typeof ctx.value === 'string' ? ctx.value : '' input.readOnly = !!ctx.readonly } onContextChange(init) init()
// 有未保存修改 → 告诉宿主"脏":用户点关闭会得到确认提示 input.addEventListener('input', () => { void ui.setModalDirty(true, '有未保存的更改,确定放弃吗?') })
// Cmd/Ctrl+S 保存:resolveModal 把新值交回宿主 —— 宿主校验尺寸 // (validation.maxBytes)后走 op 系统写入:可撤销、进历史、协同同步 window.addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault() void ui.resolveModal({ value: input.value || null }) } }) </script></body></html>真实插件通常用构建工具(json-field 是 Vue + vite),但契约就这三样:等 context → 编辑 →
resolveModal({ value })。模态框顶部的按钮条也归你声明(createModalChrome)—— 保存/格式化这类动作与宿主的最大化/关闭按钮挤同一条,不用自己画标题栏。
| 字段 | 作用 |
|---|---|
summaryPath | 从 cell JSON 里取摘要文本的 path(如 $.summary)——网格、导出、未装插件的端都用它显示。存的是快照,插件不在场也能用 |
fieldConfig | 建/改字段时的配置面板 iframe(如”校验 schema”选项)。产出的 config 存进 field.properties,编辑器 context 里回传给你 |
drawerBlock | 记录详情抽屉里的自定义字段块;缺省是摘要+编辑按钮 |
columnMenus / cellMenus | 只在你的字段上出现的列头/单元格菜单(cellMenus 支持 requiresValue,modal context 额外带 cellValue) |
defaultValue | 新 cell 默认值 |
validation.maxBytes | 单值尺寸护栏,缺省 256KB —— cell 值会走 WebSocket 帧、进 changeset 历史、随快照分发,失控的大值伤害整个项目,按需收紧别放松 |
值的设计建议
Section titled “值的设计建议”cell 里存什么由你定(baseType: 'text' 时就是一个字符串,通常是 JSON),两条经验:
- 小值内联,大值引用:超过几十 KB 的内容(图片、大文档)存资产库拿 ref,cell 里只存瘦引用 —— maxBytes 护栏和协同分发都会感谢你;
- 给
summaryPath留位置:设计值结构时放一个人类可读的摘要字段,没装插件的用户看到的就是它。
json-field—— 本篇的底:编辑器模态 + 文件类型认领(同一插件顺手认领了.json文件的编辑);richtext-field——baseType: 'richtext'+hideBaseTypeInPicker的”升级替代”完整形态;excel-field—— 更复杂的 fieldConfig 与 drawerBlock 用法。