跳转到内容

自定义字段类型

字段插件是价值密度最高的贡献点:JSON 字段、富文本字段、Excel 公式字段都是它做的。本篇以内置的 JSON 字段(plugins/marketplace/json-field/)为底走一遍完整闭环。

先立住一个观念:你不发明新的存储类型。声明 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 历史、随快照分发,失控的大值伤害整个项目,按需收紧别放松

cell 里存什么由你定(baseType: 'text' 时就是一个字符串,通常是 JSON),两条经验:

  • 小值内联,大值引用:超过几十 KB 的内容(图片、大文档)存资产库拿 ref,cell 里只存瘦引用 —— maxBytes 护栏和协同分发都会感谢你;
  • 给 summaryPath 留位置:设计值结构时放一个人类可读的摘要字段,没装插件的用户看到的就是它。
  • json-field —— 本篇的底:编辑器模态 + 文件类型认领(同一插件顺手认领了 .json 文件的编辑);
  • richtext-field —— baseType: 'richtext' + hideBaseTypeInPicker 的”升级替代”完整形态;
  • excel-field —— 更复杂的 fieldConfig 与 drawerBlock 用法。