UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

165 lines (116 loc) 9.72 kB
# 跨点位共享场景 某些业务场景**跨多个点位共享同一份运行时上下文**——这些心智 / 参与点位 / 边界 MCP 拼不出来。具体 API 签名走 MCP 查。 ## onWorkItemFormValueChanged(5 点位共享的字段变化监听) ### 一句话核心心智 **该 API 监听的对象是"代码当前运行所在那个表单的 form state"**——5 个点位(button / tab / control / customField / aiNode)各有自家 namespace,签名 / 参数 / 返回完全一致;但能不能调通,取决于**当前代码所在的位置是不是一个能拿到 form state 的场景**。 判断标准(写代码前必看): - **表单场景**(详情页主表单 / 节点表单 / 新建页表单)→ 可用,监听同表单内的字段变化 - **挂在表单页面但自己不是表单项的位置**(详情页 button、详情页 Tab)→ 可用,监听该详情页主表单的字段变化 - **不属于表单场景的位置**(表格列展示态、表格双击编辑弹窗、视图批量按钮、新建按钮、WBS、字段配置弹窗等)→ **不可用**,没有 form state 可监听 ### 5 点位 × 可用场景速查矩阵 | 点位 | namespace | 可用场景 | 不可用场景 | Web | Mobile | |---|---|---|---|---|---| | **button** | `window.JSSDK.button` | 详情页 3 位:工作项实例-更多 / 节点-更多 / 节点流转(监听所在详情页主表单的 state) | 新建按钮 / WBS 计划表-更多 / 视图批量操作(不在表单场景内) | ✅ | ❌ | | **tab**(dashboard 点位) | `window.JSSDK.tab` | 详情页 Tab(监听所在详情页主表单的 state) | — | ✅ | ❌ | | **control** | `window.JSSDK.control` | 详情页表单 / 节点表单 / 新建页表单 | 表格列展示态(DSL 无 JSSDK)/ **表格列双击编辑弹窗(产物虽是表单代码但场景不算表单)** | ✅ | V7.35.0+ | | **customField** | `window.JSSDK.customField` | 同 control 表单 3 子场景 | 同 control + 字段配置弹窗(`FEATURE_CUSTOMFIELD_CONFIG` 入口)| ✅ | V7.42.0+ | | **aiNode** | `window.JSSDK.aiNode` | AI 节点各场景通用 | — | ✅ | ✅ | > **namespace 严格不串**:button 点位代码只能调 `window.JSSDK.button.*`,不能调 `window.JSSDK.tab.*` 或别家——平台按 namespace 路由 context,串调 runtime 报错。 > **表格列双击编辑弹窗是最易踩的坑**:它加载的就是表单 React 产物(前提是 Stage Config 同时勾选"表格列 + 表单"),代码逻辑共用 OK;但**那个弹窗本身不属于表单场景**——`onWorkItemFormValueChanged` 调进去拿不到 form state。表格弹窗里要拿初始值用 `getTableCellInitProps`(customField)或自己存(control),**不要走监听** ### 完整签名 ```ts type WatchKey = { key: string | AttributeType; // field_key(系统字段如 'priority' / 自建 'field_xxxxxx' hash)/ 'name' / 'template' type: Exclude<FieldType, | FieldType.richText | FieldType.singleSignal | FieldType.multiSignal | FieldType.singleVoting | FieldType.multiVoting | FieldType.simpleVoting >; }; onWorkItemFormValueChanged( options: { watchKeys: WatchKey[] }, // 一次最多 10 个 key callback: (changedValue: Record<string, unknown>) => void, ): Promise<() => void>; // 返回 cleanup fn ``` ### 5 条硬约束(写代码必须满足) 1. **一次最多 10 个 watchKey**——超出按需拆多次注册 2. **6 个 FieldType 不支持**`richText` / `singleSignal` / `multiSignal` / `singleVoting` / `multiVoting` / `simpleVoting`——传了报错或回调不触发 3. **返回是 `Promise<() => void>`,两层异步**:先 `await` 拿到 cleanup fn,再在 React unmount 时调用——漏 cleanup → 内存泄漏 + 重复回调 4. **fieldKey 识别**:系统字段(`name` / `priority` / `owner` 等)/ 自建字段(`field_xxxxxx` hash)的识别走 [`liteAppComponent/read-props.md §2.4`](liteAppComponent/read-props.md) 2a/2b 两路 5. **FieldType 完整枚举值** 走 MCP fallback 关键词 `FieldType 枚举`### 最小代码模板(用 control 做示例) ```ts import { FieldType } from '@lark-project/js-sdk'; const watchKeys = [ { key: 'priority', type: FieldType.singleSelect }, { key: 'owner', type: FieldType.multiUser }, ]; const off = await window.JSSDK.control.onWorkItemFormValueChanged( { watchKeys }, (changed) => { console.log('字段变了:', changed); // { priority: '...', owner: [...] } }, ); // React unmount 时 off(); ``` **切换到其他点位**:把 `window.JSSDK.control.*` 改成 `window.JSSDK.button.*` / `window.JSSDK.tab.*` / `window.JSSDK.customField.*` / `window.JSSDK.aiNode.*` 即可,签名 / watchKeys / cleanup 行为一致。 ### 各点位自家边界 / 代码片段入口 挂哪个点位,看自家 doc 的「监听其他字段变化」节: - button → [`button/index.md`](button/index.md)「监听其他字段变化」 - tab(dashboard 点位)→ [`dashboard/index.md`](dashboard/index.md)「监听其他字段变化」 - control → [`control/form-control.md §3`](control/form-control.md)「联动其他表单项」 - customField → 表单场景按上面模板把 namespace 换成 `customField`;本字段自身值的读写见 [`customField/index.md`](customField/index.md) / [`customField/value-shape.md`](customField/value-shape.md) - aiNode → 按上面模板把 namespace 换成 `aiNode`;卡片点位整体能力(getContext / getProps / watch + 数据流编排)见 [`ai_node/card.md`](ai_node/card.md) ### 边界(doc 未明走 MCP) - **节点表单和详情页主表单是否共享同一个 form state** → MCP fallback 关键词 `节点表单 详情页 字段联动` - **新建页表单是另一个独立 state**(工作项还没创建),和详情页主表单不共享 - **watchKey 超 10 上限的策略** → 按需拆多次注册 ### 典型组合需求 → 选点位 | 需求 | 选 | |---|---| | 详情页字段 A 变 → 控件 B 展示/计算自动更新 | `control` + `onWorkItemFormValueChanged` | | 详情页字段 A 变 → 存储值到平台的字段 B 自动更新 | `customField`(落库)+ `onWorkItemFormValueChanged` | | 详情页字段 A 变 → Tab 内的统计 / 图表 / 关联工作项列表自动刷新 | `tab` + `onWorkItemFormValueChanged` | | 详情页 button 点击前判断字段是否合法 | `button` 详情页场景 + `onWorkItemFormValueChanged`(mount 时注册,点击时读最新值) | | 节点流转前服务端拦截判断多字段是否合法 | `intercept` 点位(服务端 webhook,不在本 skill 生成范围)| --- ## 数据获取(WorkItem.load / Context.load) ### 一句话核心心智 `WorkItem` / `Context`**全局 namespace**`window.JSSDK.WorkItem.*` / `window.JSSDK.Context.*`),**不分点位**——任何能从自家 `getContext()` 拿到 `{spaceId, workObjectId, workItemId}` 的点位都能用它**主动读工作项实例数据 / 字段当前值**。 它和上面的 `onWorkItemFormValueChanged`**互补的两半**- `onWorkItemFormValueChanged` = 监听字段**变化**(只给变更后的增量,挂载时不给当前值) - `WorkItem.load().getFieldValue()` = 读字段**当前值**(一次性快照) 典型配对:**挂载时 `WorkItem.load` 读一次当前值 + `onWorkItemFormValueChanged` 接后续增量**。只挂监听不读初值 → 打开页面时若该字段之后无变更,UI 会一直空白。 ### WorkItem.load —— 读工作项实例数据 + 字段值 > source:飞书项目知识库 help `gvuo2oip`(《WorkItem》)。Web ✅ / Mobile V7.35.0+。 ```ts const workItem = await window.JSSDK.WorkItem.load({ spaceId, // string ← 三件套均来自各点位 getContext() workObjectId, // string (OpenAPI 里叫 work_item_type_key) workItemId, // number ⚠️ 数字类型 }); // 返回的 workItem 上: // workItem.id / name / spaceId / workObjectId / templateId // workItem.isAborted / isDeleted: boolean —— 实例是否终止 / 删除 // workItem.roleOwnersList: RoleOwners[] // workItem.getRoleList(): Promise<Role[]> —— 角色与人员列表 // workItem.getFieldValue(fieldId: string): Promise<any> —— 读指定字段当前值 const value = await workItem.getFieldValue('priority'); // 以 fieldKey 取值 ``` - **fieldKey 识别**:系统字段(`name` / `priority` / `owner` 等)/ 自建字段(`field_xxxxxx` hash)走 [`liteAppComponent/read-props.md §2.4`](liteAppComponent/read-props.md) 2a/2b 两路。 - **字段值的精确 JSON 形态**因字段类型而异(文本 / 单选 / 多选人员 / 日期 …)→ 不确定 `console.log` 实测,或 MCP 关键词 `字段与属性解析格式` 查。 - 移动端有版本门槛(V7.35.0+);更低版本或要复杂查询走自建后端 OpenAPI。 ### Context.load —— 读运行环境(语言等) ```ts const { language } = await window.JSSDK.Context.load(); // 'zh_CN' | 'en_US' | 'ja_JP';新增 locale 走 raw 兜底 ``` 用于 i18n 文案按用户语言取值。其余字段走 MCP 关键词 `Context load 返回` 查。 ### 各点位入口 identity 来自各点位 `getContext()`**liteAppComponent 例外**:多数场景用 `getDataSourceResult` 直出 `workitem.fields[fieldKey]`(见 [`liteAppComponent/read-props.md §2`](liteAppComponent/read-props.md)),不需要 `WorkItem.load`;dashboard / ai_node 卡片等"只有 ID"的场景才用 `WorkItem.load`。 --- ## (预留)其他跨点位共享场景 以后如果出现其他"跨点位共享上下文" 类的场景(例如"视图上下文"等),加在本文下方章节。