@lark-apaas/coding-steering 0.1.72 → 0.1.73-alpha.20261026071512
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-steering",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73-alpha.20261026071512",
|
|
4
4
|
"description": "Stack-specific steering content for miaoda-coding templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"steering"
|
|
8
8
|
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
11
|
+
},
|
|
9
12
|
"devDependencies": {
|
|
10
13
|
"markdownlint-cli": "^0.47.0"
|
|
11
14
|
},
|
|
@@ -17,8 +20,5 @@
|
|
|
17
20
|
"miaoda",
|
|
18
21
|
"coding-steering"
|
|
19
22
|
],
|
|
20
|
-
"license": "MIT"
|
|
21
|
-
|
|
22
|
-
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
record.v1.0版本 // 会被错误解析
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
4. **特殊字段需特化展示**:Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
|
|
64
|
-
5. **特殊字段需专用录入组件**:SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
|
|
63
|
+
4. **特殊字段需特化展示**:Text(`BitableText` 分段渲染:链接可点击、保留换行)、Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
|
|
64
|
+
5. **特殊字段需专用录入组件**:Text(多行文本框 `Textarea`)、SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
|
|
65
65
|
|
|
66
66
|
---
|
|
67
67
|
|
|
@@ -89,7 +89,8 @@
|
|
|
89
89
|
|
|
90
90
|
| BizType | 写入格式 | 读取格式 | 说明 |
|
|
91
91
|
| --------------------------------------- | ---------------- | ------------------ | ------------------------ |
|
|
92
|
-
| `Text
|
|
92
|
+
| `Text` | `string \| TextSegment[]` | `{ text: string; originalValue?: TextSegment[] }` | 写入:纯文本直接传字符串;需要保留或写入链接、@ 时传分段数组,通常就是读到的 `originalValue` 修改后原样回传(见「文本字段写入分段」)。读取:`text` 是拼接后的纯文本,用于筛选、排序、比较;展示用 `originalValue`。读取时传 `textFormat: 'original'` 才有 `originalValue`;服务端开关关闭时也可能缺失,缺失时退回 `text` |
|
|
93
|
+
| `Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
|
|
93
94
|
| `Phone` | `string` | `string` | 读写均为字符串 |
|
|
94
95
|
| `Number`/`Progress`/`Currency`/`Rating` | `number` | `number` | 数值类型 |
|
|
95
96
|
| `SingleSelect` | `string` | `string` | 必须匹配 enumValues |
|
|
@@ -100,6 +101,22 @@
|
|
|
100
101
|
| `Url` | `{ text, link }` | `{ text, link }` | 读写格式一致 |
|
|
101
102
|
| `Attachment` | `string[]` | 复杂对象 | 写入文件 URL 数组 |
|
|
102
103
|
|
|
104
|
+
`TextSegment` 定义:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// 多维表格文本字段的一段内容,原样来自多维表格;各段 text 依次拼接等于外层的 text
|
|
108
|
+
type TextSegment = {
|
|
109
|
+
type: string; // 'text' 普通文字(可能含 \n,也可能是空串)/ 'url' 超链接 / 'mention' @文档或@人;遇到不认识的按 text 显示
|
|
110
|
+
text: string;
|
|
111
|
+
link?: string; // url 段、@文档段的地址
|
|
112
|
+
mentionType?: string | number; // mention 段:读出是 Bitable / Docx / Wiki / User 等字符串,取值开放,按有没有 link 判断能否点击;写入接受字符串或数字,必须与链接的文档类型一致
|
|
113
|
+
token?: string; // @文档的文档 token;@人时是飞书 open_id,不是妙搭用户 ID,不要拿去查用户或和人员字段比较
|
|
114
|
+
[k: string]: unknown; // 其他字段原样保留
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
兼容说明:`originalValue` 是可选的新增字段,不传 `textFormat` 时返回内容与原来完全一致,只读 `.text` 的存量代码不用改也能正常运行。要链接可点击、保留换行时,再按下文迁移:读取加 `textFormat: 'original'`,展示从 `.text` 换成 `BitableText`。写入同理:Text 字段继续传字符串的存量代码行为不变,分段数组是新增的可选写法。
|
|
119
|
+
|
|
103
120
|
### 只读字段
|
|
104
121
|
|
|
105
122
|
| BizType | 读取格式 |
|
|
@@ -175,7 +192,7 @@
|
|
|
175
192
|
|
|
176
193
|
### dimensions value 类型(常见)
|
|
177
194
|
|
|
178
|
-
- `Text` / `Email` / `Barcode`: `{ text: string }`
|
|
195
|
+
- `Text` / `Email` / `Barcode`: `{ text: string }`(Text 分组维度只有 `text`,没有 `originalValue`,不要按 originalValue 渲染分组标签)
|
|
179
196
|
- `Number`: `string`
|
|
180
197
|
- `Progress`: `string`
|
|
181
198
|
- `Currency`: `string`
|
|
@@ -228,6 +245,9 @@ const response = await capabilityClient.load('plugin_instance_id').call<{
|
|
|
228
245
|
conditions: [{ fieldName: '状态', operator: 'is', value: ['进行中'] }],
|
|
229
246
|
},
|
|
230
247
|
pageSize: 20,
|
|
248
|
+
// 列表展示文本字段(链接可点击、保留换行)时传;不传就没有 originalValue,页面只能显示纯文本。
|
|
249
|
+
// 同步入库、导出、统计等不展示的读取不要传,见「代码实现注意事项」第 6 条
|
|
250
|
+
textFormat: 'original',
|
|
231
251
|
});
|
|
232
252
|
|
|
233
253
|
// ⚠️ 响应数据直接从 response 访问,不要加 .data 或 .output 前缀
|
|
@@ -240,17 +260,17 @@ logger.info(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${h
|
|
|
240
260
|
const detail = await capabilityClient.load('plugin_instance_id').call<{
|
|
241
261
|
id: string;
|
|
242
262
|
record: Record<string, any>;
|
|
243
|
-
}>('getRecord', { recordID: 'recXXX' });
|
|
263
|
+
}>('getRecord', { recordID: 'recXXX', textFormat: 'original' }); // 详情 / 编辑页要展示文本字段,传 textFormat
|
|
244
264
|
// ✅ 正确:detail.id / detail.record
|
|
245
265
|
// ❌ 错误:detail.data.output.id
|
|
246
|
-
const title = detail.record['标题']?.text;
|
|
266
|
+
const title = detail.record['标题']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
|
|
247
267
|
|
|
248
268
|
// 批量新增(注意:写入时用写入格式,字段名必须用引号包裹)
|
|
249
269
|
await capabilityClient.load('plugin_instance_id').call('batchAddRecords', {
|
|
250
270
|
records: [
|
|
251
271
|
{
|
|
252
272
|
record: {
|
|
253
|
-
文本字段: '内容', // Text: string(非 { text: string }
|
|
273
|
+
文本字段: '内容', // Text: string(非 { text: string });要带链接时传 TextSegment[],见「文本字段写入分段」
|
|
254
274
|
数字字段: 100, // Number: number
|
|
255
275
|
单选字段: '选项A', // SingleSelect: string(必须匹配 enumValues)
|
|
256
276
|
多选字段: ['标签1', '标签2'], // MultiSelect: string[]
|
|
@@ -263,7 +283,7 @@ await capabilityClient.load('plugin_instance_id').call('batchAddRecords', {
|
|
|
263
283
|
],
|
|
264
284
|
});
|
|
265
285
|
|
|
266
|
-
//
|
|
286
|
+
// 批量更新(record 里只放改过的字段,见下方「代码实现注意事项」第 4 条)
|
|
267
287
|
await capabilityClient.load('plugin_instance_id').call('batchUpdateRecords', {
|
|
268
288
|
records: [{ id: 'record_id', record: { 文本字段: '新内容' } }],
|
|
269
289
|
});
|
|
@@ -274,7 +294,7 @@ await capabilityClient
|
|
|
274
294
|
.call('deleteRecords', { recordIDs: ['id1', 'id2'] });
|
|
275
295
|
|
|
276
296
|
// 读取时注意格式差异
|
|
277
|
-
const textValue = record.record['文本字段']?.text; //
|
|
297
|
+
const textValue = record.record['文本字段']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
|
|
278
298
|
const numberValue = record.record['数字字段']; // number
|
|
279
299
|
const userIds = record.record['人员字段']; // number[]
|
|
280
300
|
|
|
@@ -300,23 +320,125 @@ const chartData =
|
|
|
300
320
|
1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,用 `logger.info('response', response)` 临时输出查看(或在浏览器开发者工具 Network 面板看)。
|
|
301
321
|
2. 在进行写入操作时,比如确认提交时,应该有loading效果,禁止用户重复点击。
|
|
302
322
|
3. 如果capabilityClient call失败,命中了error逻辑,要打印日志,同时把error message提示给用户。
|
|
303
|
-
4.
|
|
323
|
+
4. **只提交改过的字段(硬约束)**。编辑后调用 batchUpdateRecords 时,**只提交和读取时相比值有变化的字段**,逐字段比较后再组装 `record`。Text 字段没改动时**禁止**放进请求:多维表格按字段整体覆盖,而编辑表单的文本框提交的是纯字符串,带链接的文本会被写成纯文本,链接永久丢失。要改文字又保留链接,按下方「文本字段写入分段」写分段数组。
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// 编辑页读取:含链接提示依赖 originalValue,getRecord 也要传 textFormat(见第 6 条)
|
|
327
|
+
const { record: original } = await capabilityClient.load(pluginInstanceId).call<{ id: string; record: Record<string, any> }>('getRecord', {
|
|
328
|
+
recordID: recordId,
|
|
329
|
+
textFormat: 'original',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// 表单回填:Text 字段用 .text(表单值是 string),不要把整个 { text, originalValue } 对象放进表单,
|
|
333
|
+
// 否则文本框显示 [object Object],保存时还会把对象当写入值提交
|
|
334
|
+
const initForm = {
|
|
335
|
+
文本字段: original['文本字段']?.text ?? '',
|
|
336
|
+
数字字段: original['数字字段'],
|
|
337
|
+
多选字段: original['多选字段'] ?? [],
|
|
338
|
+
// ...其余可编辑字段;只读字段(创建时间、创建人、公式等)不要放进表单
|
|
339
|
+
// Attachment 读取是复杂对象、写入是文件 URL 数组,编辑时按写入格式单独维护
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// 保存:form 是以 initForm 初始化、用户编辑后的表单值;逐字段和读取值比较,只组装改过的字段
|
|
343
|
+
const changed: Record<string, unknown> = {};
|
|
344
|
+
// Text:用 .text 和表单值比
|
|
345
|
+
if (form['文本字段'] !== (original['文本字段']?.text ?? '')) changed['文本字段'] = form['文本字段'];
|
|
346
|
+
// 标量字段:直接比较
|
|
347
|
+
if (form['数字字段'] !== original['数字字段']) changed['数字字段'] = form['数字字段'];
|
|
348
|
+
// 数组、对象字段(多选、人员、超链接等)按值比较,不要用 !==(引用不同会被误判为已修改)
|
|
349
|
+
const sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
350
|
+
if (!sameValue(form['多选字段'], original['多选字段'] ?? [])) changed['多选字段'] = form['多选字段'];
|
|
351
|
+
// ...其余字段同样逐个比较;只读字段不要放进 changed
|
|
352
|
+
// Attachment 读取格式与写入格式不同:未改动时不提交,改动时按写入格式(文件 URL 数组)提交
|
|
353
|
+
if (Object.keys(changed).length > 0) {
|
|
354
|
+
await capabilityClient.load(pluginInstanceId).call('batchUpdateRecords', {
|
|
355
|
+
records: [{ id: recordId, record: changed }],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
含链接文本的编辑提示:编辑控件检测到 `originalValue` 里有 `url` 或 `mention` 段时,在控件下方提示「该文本包含链接,保存后链接将变为纯文本」(示例见「推荐 UI 组件」)。编辑表单读取记录时必须传 `textFormat: 'original'`,否则拿不到分段,提示永远不出现。
|
|
304
361
|
5. **分页必须使用游标分页,禁止全量拉取后切片**。先拉全部数据再 `slice` 会导致性能极差、浪费带宽和内存。每次只拉当前页数据,通过 `pageToken` 翻页:
|
|
305
362
|
|
|
306
363
|
```typescript
|
|
307
|
-
const result = await capabilityClient.load(pluginInstanceId).call
|
|
364
|
+
const result = await capabilityClient.load(pluginInstanceId).call<{
|
|
365
|
+
records: Array<{ id: string; record: Record<string, any> }>;
|
|
366
|
+
hasMore: boolean;
|
|
367
|
+
pageToken?: string;
|
|
368
|
+
total: number;
|
|
369
|
+
}>('searchRecords', {
|
|
308
370
|
pageSize: 10,
|
|
309
371
|
pageToken: cursor, // 上一页返回的 pageToken
|
|
372
|
+
// 要把文本字段展示给用户时再加 textFormat: 'original'(见第 6 条)
|
|
310
373
|
});
|
|
311
374
|
// 返回:{ records, pageToken(下一页游标), hasMore, total }
|
|
312
375
|
```
|
|
313
376
|
|
|
377
|
+
6. **展示或编辑文本字段时显式要原始分段**。列表、详情、编辑表单回显这类要把文本字段展示给用户(或在编辑时提示链接)的读取,searchRecords / getRecord 传 `textFormat: 'original'`;不传就没有 `originalValue`,页面只能显示纯文本。同步入库、导出、统计等不展示的读取不要传——返回体会变大,也没有用处。编辑表单读取记录时同样要传,否则拿不到分段,第 4 条的含链接提示永远不出现。传了也可能拿不到(服务端开关关闭),渲染时必须能退回 `text`。
|
|
378
|
+
|
|
379
|
+
### 文本字段写入分段
|
|
380
|
+
|
|
381
|
+
Text 字段写入除了字符串,还可以传分段数组 `TextSegment[]`,用来保留或写入链接、@。
|
|
382
|
+
|
|
383
|
+
**原样写回 / 改文字保链接**:读取时传 `textFormat: 'original'`,把要改的普通文字段(`type: 'text'`)的 `text` 改掉,其余段(url、mention、空段)**原样保留不要删字段**,整个数组作为写入值。
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
const detail = await capabilityClient.load(pluginInstanceId).call<{ id: string; record: Record<string, any> }>('getRecord', {
|
|
387
|
+
recordID: recordId,
|
|
388
|
+
textFormat: 'original',
|
|
389
|
+
});
|
|
390
|
+
const value = detail.record['文本字段'] as { text: string; originalValue?: TextSegment[] } | null | undefined;
|
|
391
|
+
// 空单元格读回来没有这个字段(null),按空分段处理;有文字却拿不到 originalValue(未传 textFormat、服务端开关关闭或回滚)才报错:
|
|
392
|
+
// 这时不能按分段写回(写入 [] 会清空单元格),也不能静默跳过(用户会以为已保存)
|
|
393
|
+
const segments: TextSegment[] | undefined = value?.originalValue ?? (value?.text ? undefined : []);
|
|
394
|
+
if (!segments) {
|
|
395
|
+
throw new Error('暂时无法保留文本中的链接,请稍后重试');
|
|
396
|
+
}
|
|
397
|
+
// 改开头的文字:第一段是普通文字就改它,否则(空单元格、开头是链接或 @)在最前面补一段文字
|
|
398
|
+
const next: TextSegment[] =
|
|
399
|
+
segments[0]?.type === 'text'
|
|
400
|
+
? [{ ...segments[0], text: '新的开头文字' }, ...segments.slice(1)]
|
|
401
|
+
: [{ type: 'text', text: '新的开头文字' }, ...segments];
|
|
402
|
+
await capabilityClient.load(pluginInstanceId).call('batchUpdateRecords', {
|
|
403
|
+
records: [{ id: recordId, record: { 文本字段: next } }],
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**程序化写入链接**:
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
await capabilityClient.load(pluginInstanceId).call('batchAddRecords', {
|
|
411
|
+
records: [
|
|
412
|
+
{
|
|
413
|
+
record: {
|
|
414
|
+
文本字段: [
|
|
415
|
+
{ type: 'text', text: '详情见 ' },
|
|
416
|
+
{ type: 'url', text: '需求文档', link: 'https://example.com/doc' },
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**写入规则**(每段缺字符串 `type` 或 `text` 时插件入参校验直接报错、不会到多维表格;其余不满足的由多维表格拒绝,错误码 800074060,message 说明原因):
|
|
425
|
+
|
|
426
|
+
- 每段必须有字符串 `type` 和 `text`(空段写 `''`)
|
|
427
|
+
- `url` 段必须有 `text` 和 `link`(`link` 不要传空串:多维表格会把它补成 `http://<显示文字>`)
|
|
428
|
+
- @文档(非 User 的 mention)必须同时有 `token` 和 `link`,且 `mentionType` 必须与链接的文档类型一致——**不要自己拼 @文档,复用读到的 mention 段**
|
|
429
|
+
- @人段 `mentionType: 'User'`、`token` 为读到的 open_id
|
|
430
|
+
- `mentionNotify`:原样透传。BOE 实测通过接口写入(含原样写回)未观察到 @ 通知,以多维表格侧说明为准;不要为了「不打扰」去删改读到的 mention 段
|
|
431
|
+
- 空数组 `[]` 会清空单元格;要清空才这样写
|
|
432
|
+
|
|
433
|
+
**编辑表单不变**:用户在文本框里编辑文本仍提交字符串(链接会变纯文本,保留第 4 条的编辑提示);文本框未改动时仍然不提交该字段(与「只提交改过的字段」一致)。不要把文本框的字符串和 `originalValue` 拼接。
|
|
434
|
+
|
|
314
435
|
---
|
|
315
436
|
|
|
316
437
|
## 推荐 UI 组件
|
|
317
438
|
|
|
318
439
|
| BizType | 展示 | 录入 |
|
|
319
440
|
| ---------------------------- | -------------------------------------------------------------------- | ------------------------------------------- |
|
|
441
|
+
| `Text` | `<BitableText value={val} />`(见下方示例) | `<Textarea />`(多行;不能用单行 `<Input>`,否则换行会在编辑时被吞掉) |
|
|
320
442
|
| `User` | `<UserDisplay value={ids.map(id => id.toString())} />` | `<UserSelect multiple />` |
|
|
321
443
|
| `Url` | `<Hyperlink value={val} readOnly />` | `<Hyperlink readOnly={false} />` |
|
|
322
444
|
| `SingleSelect`/`MultiSelect` | 文本/标签 | `<Select>` 选项来自 enumValues |
|
|
@@ -328,8 +450,53 @@ const chartData =
|
|
|
328
450
|
|
|
329
451
|
BizType 为 `User` 的字段展示必须使用 business-ui `UserDisplay`;不要直接渲染、拼接或 `join` 用户 ID 文本。
|
|
330
452
|
|
|
453
|
+
BizType 为 `Text` 的字段展示必须使用下方 `BitableText`;不要直接渲染 `.text`(多行会挤成一行,链接不可点击)。
|
|
454
|
+
|
|
331
455
|
### 组件代码示例
|
|
332
456
|
|
|
457
|
+
```tsx
|
|
458
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
459
|
+
|
|
460
|
+
// 文本展示:按 originalValue 分段渲染(TextSegment 定义见「BizType 读写格式对照表」)
|
|
461
|
+
function BitableText({ value }: { value?: { text: string; originalValue?: TextSegment[] } }) {
|
|
462
|
+
if (!value?.text) return <span>-</span>;
|
|
463
|
+
// 未传 textFormat、服务端开关关闭或回滚时没有 originalValue,退回纯文本
|
|
464
|
+
const segments = value.originalValue ?? [{ type: 'text', text: value.text }];
|
|
465
|
+
return (
|
|
466
|
+
// pre-wrap 保留多行文本的换行
|
|
467
|
+
<span className="whitespace-pre-wrap break-words">
|
|
468
|
+
{segments.map((seg, i) =>
|
|
469
|
+
// 只有 http(s) 链接可点击;@人没有 link,按文字显示
|
|
470
|
+
typeof seg.link === 'string' && /^https?:\/\//.test(seg.link) ? (
|
|
471
|
+
<a key={i} href={seg.link} target="_blank" rel="noopener noreferrer" className="text-primary underline">
|
|
472
|
+
{seg.text}
|
|
473
|
+
</a>
|
|
474
|
+
) : (
|
|
475
|
+
<span key={i}>{seg.text}</span>
|
|
476
|
+
),
|
|
477
|
+
)}
|
|
478
|
+
</span>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
示例里的 `/^https?:\/\//` 校验不能省:链接来自表格里的用户输入,直接塞进 `href` 会放过 `javascript:` 这类链接。
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
// 文本录入:多行 Textarea(不能用单行 Input,换行会被吞掉)
|
|
487
|
+
// formData 回填时 Text 字段取 .text(字符串),见「代码实现注意事项」第 4 条
|
|
488
|
+
<Textarea
|
|
489
|
+
value={formData['文本字段'] ?? ''}
|
|
490
|
+
onChange={(e) => setFormData({ ...formData, 文本字段: e.target.value })}
|
|
491
|
+
placeholder="请输入"
|
|
492
|
+
/>
|
|
493
|
+
{record['文本字段']?.originalValue?.some((seg) => seg.type === 'url' || seg.type === 'mention') && (
|
|
494
|
+
<p className="text-xs text-muted-foreground">该文本包含链接,保存后链接将变为纯文本</p>
|
|
495
|
+
)}
|
|
496
|
+
|
|
497
|
+
// 保存时只提交改过的字段,见「代码实现注意事项」第 4 条
|
|
498
|
+
```
|
|
499
|
+
|
|
333
500
|
```tsx
|
|
334
501
|
// 人员展示(business-ui UserDisplay 使用 value;不要传 users)
|
|
335
502
|
<UserDisplay
|
|
@@ -379,8 +546,10 @@ BizType 为 `User` 的字段展示必须使用 business-ui `UserDisplay`;不
|
|
|
379
546
|
|
|
380
547
|
```tsx
|
|
381
548
|
const columns = [
|
|
382
|
-
// 文本字段 -
|
|
383
|
-
{ title: '文本',
|
|
549
|
+
// 文本字段 - 用 BitableText 分段渲染(链接可点击、保留换行),不要 dataIndex 直接取 text
|
|
550
|
+
{ title: '文本', key: 'text', render: (_, r) => <BitableText value={r.record['文本字段']} /> },
|
|
551
|
+
// @deprecated 旧写法:{ title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' }
|
|
552
|
+
// 仍能运行,但多行挤成一行、链接不可点击;新页面不要再用,存量页面改到这一列时换成上面的 BitableText 写法
|
|
384
553
|
// 人员字段 - 使用 business-ui UserDisplay 组件,传 value(用户 ID 字符串数组),不要传 users
|
|
385
554
|
{
|
|
386
555
|
title: '负责人',
|
|
@@ -424,6 +593,28 @@ const columns = [
|
|
|
424
593
|
|
|
425
594
|
---
|
|
426
595
|
|
|
596
|
+
## ⚠️ 文本字段(Text)常见错误
|
|
597
|
+
|
|
598
|
+
**1. 直接渲染 `record['文本字段'].text` → 多行挤成一行、链接不可点击**
|
|
599
|
+
|
|
600
|
+
```tsx
|
|
601
|
+
// ❌ 错误
|
|
602
|
+
<span>{record.record['文本字段']?.text}</span>
|
|
603
|
+
|
|
604
|
+
// ✅ 正确:用 BitableText(见「推荐 UI 组件」)
|
|
605
|
+
<BitableText value={record.record['文本字段']} />
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**2. 用 `originalValue` 做筛选、排序或等值比较** — 一律用 `text`;`originalValue` 只用于展示
|
|
609
|
+
|
|
610
|
+
**3. 编辑表单把整条记录原样提交 → 没改的文本字段里的链接被覆盖成纯文本** — 只提交改过的字段;表单回填 Text 用 `.text`(见「代码实现注意事项」第 4 条)
|
|
611
|
+
|
|
612
|
+
**4. 读取时不传 `textFormat`,却去读 `originalValue` → 永远拿不到分段** — 列表、详情、编辑页这类展示或编辑文本字段的读取,searchRecords / getRecord 都传 `textFormat: 'original'`;同步、导出、统计等不展示的读取不传
|
|
613
|
+
|
|
614
|
+
**5. 改写分段时删掉 mention 段的 `token`/`link` 或改了 `mentionType` → 写入被拒** — 只改 `type: 'text'` 段的 `text`,其余段原样保留(见「文本字段写入分段」)
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
427
618
|
## 搜索过滤条件规则
|
|
428
619
|
|
|
429
620
|
### 核心规则
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
record.v1.0版本 // 会被错误解析
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
4. **特殊字段需特化展示**:Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
|
|
60
|
-
5. **特殊字段需专用录入组件**:SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
|
|
59
|
+
4. **特殊字段需特化展示**:Text(`BitableText` 分段渲染:链接可点击、保留换行)、Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
|
|
60
|
+
5. **特殊字段需专用录入组件**:Text(多行文本框 `Textarea`)、SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
|
|
86
86
|
| BizType | 写入格式 | 读取格式 | 说明 |
|
|
87
87
|
| --------------------------------------- | ---------------- | ------------------ | ------------------------ |
|
|
88
|
-
| `Text
|
|
88
|
+
| `Text` | `string \| TextSegment[]` | `{ text: string; originalValue?: TextSegment[] }` | 写入:纯文本直接传字符串;需要保留或写入链接、@ 时传分段数组,通常就是读到的 `originalValue` 修改后原样回传(见「文本字段写入分段」)。读取:`text` 是拼接后的纯文本,用于筛选、排序、比较;展示用 `originalValue`。读取时传 `textFormat: 'original'` 才有 `originalValue`;服务端开关关闭时也可能缺失,缺失时退回 `text` |
|
|
89
|
+
| `Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
|
|
89
90
|
| `Phone` | `string` | `string` | 读写均为字符串 |
|
|
90
91
|
| `Number`/`Progress`/`Currency`/`Rating` | `number` | `number` | 数值类型 |
|
|
91
92
|
| `SingleSelect` | `string` | `string` | 必须匹配 enumValues |
|
|
@@ -96,6 +97,22 @@
|
|
|
96
97
|
| `Url` | `{ text, link }` | `{ text, link }` | 读写格式一致 |
|
|
97
98
|
| `Attachment` | `string[]` | 复杂对象 | 写入文件 URL 数组 |
|
|
98
99
|
|
|
100
|
+
`TextSegment` 定义:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// 多维表格文本字段的一段内容,原样来自多维表格;各段 text 依次拼接等于外层的 text
|
|
104
|
+
type TextSegment = {
|
|
105
|
+
type: string; // 'text' 普通文字(可能含 \n,也可能是空串)/ 'url' 超链接 / 'mention' @文档或@人;遇到不认识的按 text 显示
|
|
106
|
+
text: string;
|
|
107
|
+
link?: string; // url 段、@文档段的地址
|
|
108
|
+
mentionType?: string | number; // mention 段:读出是 Bitable / Docx / Wiki / User 等字符串,取值开放,按有没有 link 判断能否点击;写入接受字符串或数字,必须与链接的文档类型一致
|
|
109
|
+
token?: string; // @文档的文档 token;@人时是飞书 open_id,不是妙搭用户 ID,不要拿去查用户或和人员字段比较
|
|
110
|
+
[k: string]: unknown; // 其他字段原样保留
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
兼容说明:`originalValue` 是可选的新增字段,不传 `textFormat` 时返回内容与原来完全一致,只读 `.text` 的存量代码不用改也能正常运行。要链接可点击、保留换行时,再按下文迁移:读取加 `textFormat: 'original'`,展示从 `.text` 换成 `BitableText`。写入同理:Text 字段继续传字符串的存量代码行为不变,分段数组是新增的可选写法。
|
|
115
|
+
|
|
99
116
|
### 只读字段
|
|
100
117
|
|
|
101
118
|
| BizType | 读取格式 |
|
|
@@ -157,7 +174,7 @@
|
|
|
157
174
|
|
|
158
175
|
### dimensions value 类型(常见)
|
|
159
176
|
|
|
160
|
-
- `Text` / `Email` / `Barcode`: `{ text: string }`
|
|
177
|
+
- `Text` / `Email` / `Barcode`: `{ text: string }`(Text 分组维度只有 `text`,没有 `originalValue`,不要按 originalValue 渲染分组标签)
|
|
161
178
|
- `Number`: `string`
|
|
162
179
|
- `Progress`: `string`
|
|
163
180
|
- `Currency`: `string`
|
|
@@ -207,6 +224,9 @@ const response = await (bitable as any).call('searchRecords', {
|
|
|
207
224
|
conditions: [{ fieldName: '状态', operator: 'is', value: ['进行中'] }],
|
|
208
225
|
},
|
|
209
226
|
pageSize: 20,
|
|
227
|
+
// 列表展示文本字段(链接可点击、保留换行)时传;不传就没有 originalValue,页面只能显示纯文本。
|
|
228
|
+
// 同步入库、导出、统计等不展示的读取不要传,见「代码实现注意事项」第 6 条
|
|
229
|
+
textFormat: 'original',
|
|
210
230
|
});
|
|
211
231
|
|
|
212
232
|
// ⚠️ 响应数据直接从 response 访问,不要加 .data 或 .output 前缀
|
|
@@ -216,17 +236,17 @@ const { records, hasMore, total, pageToken } = response;
|
|
|
216
236
|
console.log(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${hasMore}`);
|
|
217
237
|
|
|
218
238
|
// 获取单条记录
|
|
219
|
-
const detail = await (bitable as any).call('getRecord', { recordID: 'recXXX' });
|
|
239
|
+
const detail = await (bitable as any).call('getRecord', { recordID: 'recXXX', textFormat: 'original' }); // 详情 / 编辑页要展示文本字段,传 textFormat
|
|
220
240
|
// ✅ 正确:detail.id / detail.record
|
|
221
241
|
// ❌ 错误:detail.data.output.id
|
|
222
|
-
const title = detail.record['标题']?.text;
|
|
242
|
+
const title = detail.record['标题']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
|
|
223
243
|
|
|
224
244
|
// 批量新增(注意:写入时用写入格式,字段名必须用引号包裹)
|
|
225
245
|
await (bitable as any).call('batchAddRecords', {
|
|
226
246
|
records: [
|
|
227
247
|
{
|
|
228
248
|
record: {
|
|
229
|
-
文本字段: '内容', // Text: string(非 { text: string }
|
|
249
|
+
文本字段: '内容', // Text: string(非 { text: string });要带链接时传 TextSegment[],见「文本字段写入分段」
|
|
230
250
|
数字字段: 100, // Number: number
|
|
231
251
|
单选字段: '选项A', // SingleSelect: string(必须匹配 enumValues)
|
|
232
252
|
多选字段: ['标签1', '标签2'], // MultiSelect: string[]
|
|
@@ -239,7 +259,7 @@ await (bitable as any).call('batchAddRecords', {
|
|
|
239
259
|
],
|
|
240
260
|
});
|
|
241
261
|
|
|
242
|
-
//
|
|
262
|
+
// 批量更新(record 里只放改过的字段,见下方「代码实现注意事项」第 4 条)
|
|
243
263
|
await (bitable as any).call('batchUpdateRecords', {
|
|
244
264
|
records: [{ id: 'record_id', record: { 文本字段: '新内容' } }],
|
|
245
265
|
});
|
|
@@ -248,7 +268,7 @@ await (bitable as any).call('batchUpdateRecords', {
|
|
|
248
268
|
await (bitable as any).call('deleteRecords', { recordIDs: ['id1', 'id2'] });
|
|
249
269
|
|
|
250
270
|
// 读取时注意格式差异
|
|
251
|
-
const textValue = record.record['文本字段']?.text; //
|
|
271
|
+
const textValue = record.record['文本字段']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
|
|
252
272
|
const numberValue = record.record['数字字段']; // number
|
|
253
273
|
const userIds = record.record['人员字段']; // number[]
|
|
254
274
|
|
|
@@ -272,7 +292,44 @@ const chartData =
|
|
|
272
292
|
1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,先用 `console.log` 打印完整响应再编码,禁止凭猜测修改数据路径。
|
|
273
293
|
2. 在进行写入操作时,比如确认提交时,应该有loading效果,禁止用户重复点击。
|
|
274
294
|
3. 如果capabilityClient call失败,命中了error逻辑,要打印日志,同时把error message提示给用户。
|
|
275
|
-
4.
|
|
295
|
+
4. **只提交改过的字段(硬约束)**。编辑后调用 batchUpdateRecords 时,**只提交和读取时相比值有变化的字段**,逐字段比较后再组装 `record`。Text 字段没改动时**禁止**放进请求:多维表格按字段整体覆盖,而编辑表单的文本框提交的是纯字符串,带链接的文本会被写成纯文本,链接永久丢失。要改文字又保留链接,按下方「文本字段写入分段」写分段数组。
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// 编辑页读取:含链接提示依赖 originalValue,getRecord 也要传 textFormat(见第 6 条)
|
|
299
|
+
const { record: original } = await (bitable as any).call('getRecord', {
|
|
300
|
+
recordID: recordId,
|
|
301
|
+
textFormat: 'original',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// 表单回填:Text 字段用 .text(表单值是 string),不要把整个 { text, originalValue } 对象放进表单,
|
|
305
|
+
// 否则文本框显示 [object Object],保存时还会把对象当写入值提交
|
|
306
|
+
const initForm = {
|
|
307
|
+
文本字段: original['文本字段']?.text ?? '',
|
|
308
|
+
数字字段: original['数字字段'],
|
|
309
|
+
多选字段: original['多选字段'] ?? [],
|
|
310
|
+
// ...其余可编辑字段;只读字段(创建时间、创建人、公式等)不要放进表单
|
|
311
|
+
// Attachment 读取是复杂对象、写入是文件 URL 数组,编辑时按写入格式单独维护
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// 保存:form 是以 initForm 初始化、用户编辑后的表单值;逐字段和读取值比较,只组装改过的字段
|
|
315
|
+
const changed: Record<string, unknown> = {};
|
|
316
|
+
// Text:用 .text 和表单值比
|
|
317
|
+
if (form['文本字段'] !== (original['文本字段']?.text ?? '')) changed['文本字段'] = form['文本字段'];
|
|
318
|
+
// 标量字段:直接比较
|
|
319
|
+
if (form['数字字段'] !== original['数字字段']) changed['数字字段'] = form['数字字段'];
|
|
320
|
+
// 数组、对象字段(多选、人员、超链接等)按值比较,不要用 !==(引用不同会被误判为已修改)
|
|
321
|
+
const sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
322
|
+
if (!sameValue(form['多选字段'], original['多选字段'] ?? [])) changed['多选字段'] = form['多选字段'];
|
|
323
|
+
// ...其余字段同样逐个比较;只读字段不要放进 changed
|
|
324
|
+
// Attachment 读取格式与写入格式不同:未改动时不提交,改动时按写入格式(文件 URL 数组)提交
|
|
325
|
+
if (Object.keys(changed).length > 0) {
|
|
326
|
+
await (bitable as any).call('batchUpdateRecords', {
|
|
327
|
+
records: [{ id: recordId, record: changed }],
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
含链接文本的编辑提示:编辑控件检测到 `originalValue` 里有 `url` 或 `mention` 段时,在控件下方提示「该文本包含链接,保存后链接将变为纯文本」(示例见「推荐 UI 组件」)。编辑表单读取记录时必须传 `textFormat: 'original'`,否则拿不到分段,提示永远不出现。
|
|
276
333
|
5. **分页必须使用游标分页,禁止全量拉取后切片**。先拉全部数据再 `slice` 会导致性能极差、浪费带宽和内存。每次只拉当前页数据,通过 `pageToken` 翻页:
|
|
277
334
|
|
|
278
335
|
```typescript
|
|
@@ -280,10 +337,69 @@ const chartData =
|
|
|
280
337
|
const result = await (ex as any).call('searchRecords', {
|
|
281
338
|
pageSize: 10,
|
|
282
339
|
pageToken: cursor, // 上一页返回的 pageToken
|
|
340
|
+
// 要把文本字段展示给用户时再加 textFormat: 'original'(见第 6 条)
|
|
283
341
|
});
|
|
284
342
|
// 返回:{ records, pageToken(下一页游标), hasMore, total }
|
|
285
343
|
```
|
|
286
344
|
|
|
345
|
+
6. **展示或编辑文本字段时显式要原始分段**。列表、详情、编辑表单回显这类要把文本字段展示给用户(或在编辑时提示链接)的读取,searchRecords / getRecord 传 `textFormat: 'original'`;不传就没有 `originalValue`,页面只能显示纯文本。同步入库、导出、统计等不展示的读取不要传——返回体会变大,也没有用处。编辑表单读取记录时同样要传,否则拿不到分段,第 4 条的含链接提示永远不出现。传了也可能拿不到(服务端开关关闭),渲染时必须能退回 `text`。
|
|
346
|
+
|
|
347
|
+
### 文本字段写入分段
|
|
348
|
+
|
|
349
|
+
Text 字段写入除了字符串,还可以传分段数组 `TextSegment[]`,用来保留或写入链接、@。
|
|
350
|
+
|
|
351
|
+
**原样写回 / 改文字保链接**:读取时传 `textFormat: 'original'`,把要改的普通文字段(`type: 'text'`)的 `text` 改掉,其余段(url、mention、空段)**原样保留不要删字段**,整个数组作为写入值。
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
const detail = await (bitable as any).call('getRecord', {
|
|
355
|
+
recordID: recordId,
|
|
356
|
+
textFormat: 'original',
|
|
357
|
+
});
|
|
358
|
+
const value = detail.record['文本字段'] as { text: string; originalValue?: TextSegment[] } | null | undefined;
|
|
359
|
+
// 空单元格读回来没有这个字段(null),按空分段处理;有文字却拿不到 originalValue(未传 textFormat、服务端开关关闭或回滚)才报错:
|
|
360
|
+
// 这时不能按分段写回(写入 [] 会清空单元格),也不能静默跳过(用户会以为已保存)
|
|
361
|
+
const segments: TextSegment[] | undefined = value?.originalValue ?? (value?.text ? undefined : []);
|
|
362
|
+
if (!segments) {
|
|
363
|
+
throw new Error('暂时无法保留文本中的链接,请稍后重试');
|
|
364
|
+
}
|
|
365
|
+
// 改开头的文字:第一段是普通文字就改它,否则(空单元格、开头是链接或 @)在最前面补一段文字
|
|
366
|
+
const next: TextSegment[] =
|
|
367
|
+
segments[0]?.type === 'text'
|
|
368
|
+
? [{ ...segments[0], text: '新的开头文字' }, ...segments.slice(1)]
|
|
369
|
+
: [{ type: 'text', text: '新的开头文字' }, ...segments];
|
|
370
|
+
await (bitable as any).call('batchUpdateRecords', {
|
|
371
|
+
records: [{ id: recordId, record: { 文本字段: next } }],
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**程序化写入链接**:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
await (bitable as any).call('batchAddRecords', {
|
|
379
|
+
records: [
|
|
380
|
+
{
|
|
381
|
+
record: {
|
|
382
|
+
文本字段: [
|
|
383
|
+
{ type: 'text', text: '详情见 ' },
|
|
384
|
+
{ type: 'url', text: '需求文档', link: 'https://example.com/doc' },
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**写入规则**(每段缺字符串 `type` 或 `text` 时插件入参校验直接报错、不会到多维表格;其余不满足的由多维表格拒绝,错误码 800074060,message 说明原因):
|
|
393
|
+
|
|
394
|
+
- 每段必须有字符串 `type` 和 `text`(空段写 `''`)
|
|
395
|
+
- `url` 段必须有 `text` 和 `link`(`link` 不要传空串:多维表格会把它补成 `http://<显示文字>`)
|
|
396
|
+
- @文档(非 User 的 mention)必须同时有 `token` 和 `link`,且 `mentionType` 必须与链接的文档类型一致——**不要自己拼 @文档,复用读到的 mention 段**
|
|
397
|
+
- @人段 `mentionType: 'User'`、`token` 为读到的 open_id
|
|
398
|
+
- `mentionNotify`:原样透传。BOE 实测通过接口写入(含原样写回)未观察到 @ 通知,以多维表格侧说明为准;不要为了「不打扰」去删改读到的 mention 段
|
|
399
|
+
- 空数组 `[]` 会清空单元格;要清空才这样写
|
|
400
|
+
|
|
401
|
+
**编辑表单不变**:用户在文本框里编辑文本仍提交字符串(链接会变纯文本,保留第 4 条的编辑提示);文本框未改动时仍然不提交该字段(与「只提交改过的字段」一致)。不要把文本框的字符串和 `originalValue` 拼接。
|
|
402
|
+
|
|
287
403
|
---
|
|
288
404
|
|
|
289
405
|
## 推荐 UI 组件(vite-react / shadcn-ui)
|
|
@@ -292,6 +408,7 @@ vite-react 用的是 shadcn-ui + Radix Primitives(无 spark UI 的 `<UserDispl
|
|
|
292
408
|
|
|
293
409
|
| BizType | 展示 | 录入 |
|
|
294
410
|
| ---------------------------- | -------------------------------------------------------------------- | --------------------------------------------------- |
|
|
411
|
+
| `Text` | `<BitableText value={val} />`(见下方示例,原生元素 + tailwind) | shadcn `<Textarea />`(多行;不能用单行 `<Input>`,否则换行会在编辑时被吞掉) |
|
|
295
412
|
| `User` | `<Avatar>` + 名字(按需查询 `getCurrentUserProfile` 等接口) | 自己拼:`<Select>` / `<Combobox>` 加搜索接口 |
|
|
296
413
|
| `Url` | `<a href={val.link} target="_blank">{val.text}</a>` | 两个 `<Input>`(一个填 text,一个填 link) |
|
|
297
414
|
| `SingleSelect`/`MultiSelect` | 文本 / `<Badge>` 标签 | `<Select>` 选项来自 enumValues |
|
|
@@ -303,12 +420,15 @@ vite-react 用的是 shadcn-ui + Radix Primitives(无 spark UI 的 `<UserDispl
|
|
|
303
420
|
|
|
304
421
|
> spark 体系下的 `<UserDisplay>` / `<UserSelect>` / `<Hyperlink>` 在 vite-react 不可用。`@lark-apaas/client-toolkit-lite` 没有暴露这些组件 —— 需要自己用 shadcn 的 `<Avatar>` / `<Select>` / `<Combobox>` 拼装。人员数据的获取需要走业务 API(视具体 capability 而定)。
|
|
305
422
|
|
|
423
|
+
BizType 为 `Text` 的字段展示必须使用下方 `BitableText`(原生元素 + tailwind 实现,不依赖 spark 组件);不要直接渲染 `.text`(多行会挤成一行,链接不可点击)。
|
|
424
|
+
|
|
306
425
|
### 组件代码示例
|
|
307
426
|
|
|
308
427
|
```tsx
|
|
309
428
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
310
429
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
311
430
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
431
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
312
432
|
|
|
313
433
|
// 单选下拉(选项必须来自 enumValues)
|
|
314
434
|
<Select value={value} onValueChange={setValue}>
|
|
@@ -333,16 +453,57 @@ import { Checkbox } from '@/components/ui/checkbox';
|
|
|
333
453
|
<a href={record['超链接']?.link} target="_blank" rel="noreferrer" className="text-primary underline">
|
|
334
454
|
{record['超链接']?.text}
|
|
335
455
|
</a>
|
|
456
|
+
|
|
457
|
+
// 文本录入:shadcn 多行 Textarea(不能用单行 Input,换行会被吞掉)
|
|
458
|
+
// formData 回填时 Text 字段取 .text(字符串),见「代码实现注意事项」第 4 条
|
|
459
|
+
<Textarea
|
|
460
|
+
value={formData['文本字段'] ?? ''}
|
|
461
|
+
onChange={(e) => setFormData({ ...formData, 文本字段: e.target.value })}
|
|
462
|
+
placeholder="请输入"
|
|
463
|
+
/>
|
|
464
|
+
{record['文本字段']?.originalValue?.some((seg) => seg.type === 'url' || seg.type === 'mention') && (
|
|
465
|
+
<p className="text-xs text-muted-foreground">该文本包含链接,保存后链接将变为纯文本</p>
|
|
466
|
+
)}
|
|
467
|
+
|
|
468
|
+
// 保存时只提交改过的字段,见「代码实现注意事项」第 4 条
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
// 文本展示:按 originalValue 分段渲染(TextSegment 定义见「BizType 读写格式对照表」)
|
|
473
|
+
function BitableText({ value }: { value?: { text: string; originalValue?: TextSegment[] } }) {
|
|
474
|
+
if (!value?.text) return <span>-</span>;
|
|
475
|
+
// 未传 textFormat、服务端开关关闭或回滚时没有 originalValue,退回纯文本
|
|
476
|
+
const segments = value.originalValue ?? [{ type: 'text', text: value.text }];
|
|
477
|
+
return (
|
|
478
|
+
// pre-wrap 保留多行文本的换行
|
|
479
|
+
<span className="whitespace-pre-wrap break-words">
|
|
480
|
+
{segments.map((seg, i) =>
|
|
481
|
+
// 只有 http(s) 链接可点击;@人没有 link,按文字显示
|
|
482
|
+
typeof seg.link === 'string' && /^https?:\/\//.test(seg.link) ? (
|
|
483
|
+
<a key={i} href={seg.link} target="_blank" rel="noopener noreferrer" className="text-primary underline">
|
|
484
|
+
{seg.text}
|
|
485
|
+
</a>
|
|
486
|
+
) : (
|
|
487
|
+
<span key={i}>{seg.text}</span>
|
|
488
|
+
),
|
|
489
|
+
)}
|
|
490
|
+
</span>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
336
493
|
```
|
|
337
494
|
|
|
495
|
+
示例里的 `/^https?:\/\//` 校验不能省:链接来自表格里的用户输入,直接塞进 `href` 会放过 `javascript:` 这类链接。
|
|
496
|
+
|
|
338
497
|
---
|
|
339
498
|
|
|
340
499
|
## 表格列渲染示例
|
|
341
500
|
|
|
342
501
|
```tsx
|
|
343
502
|
const columns = [
|
|
344
|
-
// 文本字段 -
|
|
345
|
-
{ title: '文本',
|
|
503
|
+
// 文本字段 - 用 BitableText 分段渲染(链接可点击、保留换行),不要 dataIndex 直接取 text
|
|
504
|
+
{ title: '文本', key: 'text', render: (_, r) => <BitableText value={r.record['文本字段']} /> },
|
|
505
|
+
// @deprecated 旧写法:{ title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' }
|
|
506
|
+
// 仍能运行,但多行挤成一行、链接不可点击;新页面不要再用,存量页面改到这一列时换成上面的 BitableText 写法
|
|
346
507
|
// 人员字段 - vite-react 自行用 Avatar + 名字渲染(数据来源走业务接口)
|
|
347
508
|
{
|
|
348
509
|
title: '负责人',
|
|
@@ -391,6 +552,36 @@ const columns = [
|
|
|
391
552
|
|
|
392
553
|
---
|
|
393
554
|
|
|
555
|
+
## ⚠️ 文本字段(Text)常见错误
|
|
556
|
+
|
|
557
|
+
### 1. 直接渲染 `record['文本字段'].text` → 多行挤成一行、链接不可点击
|
|
558
|
+
|
|
559
|
+
```tsx
|
|
560
|
+
// ❌ 错误
|
|
561
|
+
<span>{record.record['文本字段']?.text}</span>
|
|
562
|
+
|
|
563
|
+
// ✅ 正确:用 BitableText(见「推荐 UI 组件」)
|
|
564
|
+
<BitableText value={record.record['文本字段']} />
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### 2. 用 `originalValue` 做筛选、排序或等值比较
|
|
568
|
+
|
|
569
|
+
一律用 `text`;`originalValue` 只用于展示。
|
|
570
|
+
|
|
571
|
+
### 3. 编辑表单把整条记录原样提交 → 没改的文本字段里的链接被覆盖成纯文本
|
|
572
|
+
|
|
573
|
+
只提交改过的字段;表单回填 Text 用 `.text`(见「代码实现注意事项」第 4 条)。
|
|
574
|
+
|
|
575
|
+
### 4. 读取时不传 `textFormat`,却去读 `originalValue` → 永远拿不到分段
|
|
576
|
+
|
|
577
|
+
列表、详情、编辑页这类展示或编辑文本字段的读取,searchRecords / getRecord 都传 `textFormat: 'original'`;同步、导出、统计等不展示的读取不传。
|
|
578
|
+
|
|
579
|
+
### 5. 改写分段时删掉 mention 段的 `token`/`link` 或改了 `mentionType` → 写入被拒
|
|
580
|
+
|
|
581
|
+
只改 `type: 'text'` 段的 `text`,其余段原样保留(见「文本字段写入分段」)。
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
394
585
|
## 搜索过滤条件规则
|
|
395
586
|
|
|
396
587
|
### 核心规则
|