自定义表格视图
表格顶部那排视图 tab(网格、看板、日历、甘特…)就是可扩展的位置:tableViews 贡献让你的插件定义”同一张表的另一种投影”——日历、地图、时间线、卡片墙,随你渲染。内置的卡片视图(plugins/marketplace/card-view/,275 行单文件)就是官方样板,本篇照着它走。
先分清两个”视图”:本篇的表格视图是表的投影(出现在视图 tab 排,绑定一张表);面板(manifest 的
views)是UI 挂载位置(抽屉/活动栏/设置页,与具体某张表无关)。撞名但完全是两回事。
{ "id": "dev.timeline", "runtime": "static", "ui": "ui", "capabilities": [], "contributes": { "tableViews": [ { "viewKey": "timeline", "name": { "zh-CN": "时间线", "en-US": "Timeline" }, "icon": "🕐", "entry": "ui/index.html", "writes": { "cells": true } } ] }}生效后每张表的「+ 新建视图」菜单里出现「时间线」(列在内建类型后,标注归属插件);用户建了这种视图、切到它的 tab 时,表编辑区渲染你的 iframe。
三个设计事实先立住:
- static 就够——表数据经 ui 桥在 iframe 里直接读写(下详),不需要后端进程;
- 视图实体归引擎——视图的 type 存
${pluginId}:${viewKey},你的自由配置存视图自带的styleInfo槽位;未装插件的端看到占位 tab,数据不丢; - 写入按档声明——缺省只读;
writes.cells/addRecords/deleteRecords按需开,宿主按档放行并且锁死本视图挂载的表(传别的 tableNodeId 一律拒)。
① 等 context,拿到”我在渲染哪张表”
Section titled “① 等 context,拿到”我在渲染哪张表””import { installContextBridge, getContext, onContextChange } from '@xinghan/plugin-sdk/ui'
installContextBridge()onContextChange(init)init()
function init() { const ctx = getContext() if (!ctx.tableNodeId) return // context 还没到 void render(ctx.tableNodeId, ctx.viewId)}② 经 ui 桥读字段与记录
Section titled “② 经 ui 桥读字段与记录”表视图的 iframe 有一条 ui-rpc 数据通道(SDK @xinghan/plugin-sdk/ui 的 ui 对象封装;读写走宿主 dataEngine,与表格编辑器同一条链路):
import { ui } from '@xinghan/plugin-sdk/ui'
const { fields } = await ui.dataEngineGetFields(tableNodeId)const { records } = await ui.dataEngineQueryRecords(tableNodeId, { limit: 200 })// 行形状做防御性取值(不同链路可能给 {id,data:{…}} 或扁平 {_recordId,…} ——// card-view 同款写法两头都吃):// const data = r.data ?? r; const rid = r.id ?? r._recordId渲染时注意选项字段存的是选项 id —— 显示文字要自己从 field.properties.options 里解(card-view 的 cellText 帮手函数可整段借走)。
③ 自由渲染 + 受治理写回
Section titled “③ 自由渲染 + 受治理写回”展示完全归你(DOM/canvas/图表库都行)。交互写回走同一条桥 —— 宿主校验你声明的 writes 档位后走 op 管线,用户可撤销、协同实时:
// 例:卡片拖到"进行中"列 = 改该行的状态字段await ui.dataEngineUpdateCells(tableNodeId, [ { recordId, fieldId: statusFieldId, value: optInProgressId },])行级编辑遵循「展示归插件,编辑归内核」:点开一行让宿主的记录列表/详情接管,不用自己造编辑器(card-view 的真实用法):
await ui.openRecordList({ title: cardTitle, tableNodeId, groups: [{ value: '', count: 1, records: [{ recordId, label: cardTitle }] }],})轻提示用 ui.toast({ message, type })。
宿主版本不同、放行的桥方法可能不同。参照 card-view 的做法,启动时问一句再决定功能开不开:
const caps = await ui.capabilities() // { methods: [...], tableView: {...} }const canWrite = caps.methods.includes('dataEngine.updateCells')// 不能写就把拖拽关掉,降级成纯展示 —— 别让按钮点了没反应完整示例:卡片视图(全文就贴在文档里)—— 单文件零构建,从等 context、特性检测、读数据、选项值解码,到拖拽换组写回、失败 toast 回滚,一条龙全有,是这个贡献点的最佳教材。