@lark-project/cli
Version:
飞书项目插件开发工具
165 lines (116 loc) • 9.72 kB
Markdown
# 跨点位共享场景
某些业务场景**跨多个点位共享同一份运行时上下文**——这些心智 / 参与点位 / 边界 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`。
---
## (预留)其他跨点位共享场景
以后如果出现其他"跨点位共享上下文" 类的场景(例如"视图上下文"等),加在本文下方章节。