@lark-apaas/coding-steering 0.1.0-alpha.1 → 0.1.1
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 +1 -1
- package/steering/vite-react/skills/plugin-guide/SKILL.md +498 -0
- package/steering/vite-react/skills/plugin-guide/references/plugin-coding-guide.md +143 -0
- package/steering/vite-react/skills/plugin-guide/references/table.md +501 -0
- package/steering/vite-react/tech.md +55 -14
- package/steering/_common/skills/react-hook-best-practices/SKILL.md +0 -40
- package/steering/_common/skills/tailwind-conventions/SKILL.md +0 -36
- package/steering/vite-react/skills/client-coding-guide/SKILL.md +0 -48
- package/steering/vite-react/skills/component-conventions/SKILL.md +0 -48
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# 飞书多维表格 (feishu-bitable)
|
|
2
|
+
|
|
3
|
+
本插件提供了一整套与飞书多维表格进行交互(CRUD + 聚合查询)的接口。
|
|
4
|
+
|
|
5
|
+
## 开发者注意事项
|
|
6
|
+
|
|
7
|
+
**本文档是为 妙搭助手 设计的技术指南。**
|
|
8
|
+
|
|
9
|
+
在查看本插件的 Action 时,你将同时获得两部分信息:
|
|
10
|
+
|
|
11
|
+
1. **本文档 (README)**: 提供高级指引、业务逻辑、使用场景、重要约束。
|
|
12
|
+
2. **Action 的 `inputSchema` 和 `outputSchema`**: 提供精确的 JSON Schema 格式的输入/输出结构。
|
|
13
|
+
|
|
14
|
+
**请遵循以下原则:**
|
|
15
|
+
|
|
16
|
+
- **以 `Schema` 为结构基准**:严格按照 `inputSchema` 构建你的输入对象,并参照 `outputSchema` 解析返回结果。
|
|
17
|
+
- **以 `README` 为语义权威**:`README` 中的描述包含了 `Schema` 无法表达的关键信息。**必须优先遵循 `README` 中的指引和约束。**
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 重要提醒
|
|
22
|
+
|
|
23
|
+
**禁止修改插件配置中的任何字段!即使出现字段类型错误或 Action 执行错误也不能修改。**
|
|
24
|
+
|
|
25
|
+
使用前需读取配置中的 `fields` 字段,了解可操作的字段及其 `bizType`。
|
|
26
|
+
|
|
27
|
+
## 字段使用规范
|
|
28
|
+
|
|
29
|
+
1. **使用字段名称而非 ID**:所有操作必须使用 `fields.name`(字段名称),禁止使用字段 ID
|
|
30
|
+
|
|
31
|
+
2. **选项字段必须严格匹配 enumValues**:`SingleSelect` 和 `MultiSelect` 类型的选项值必须完全参考配置中的 `enumValues`,禁止自行编造
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
// 示例:假设字段配置如下
|
|
35
|
+
{
|
|
36
|
+
"name": "状态",
|
|
37
|
+
"bizType": "SingleSelect",
|
|
38
|
+
"enumValues": ["待处理", "进行中", "已完成"]
|
|
39
|
+
}
|
|
40
|
+
// ✅ 正确:使用 enumValues 中的值
|
|
41
|
+
{ "状态": "进行中" }
|
|
42
|
+
// ❌ 错误:使用不存在的选项
|
|
43
|
+
{ "状态": "处理中" }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
3. **字段名称可能包含中文或特殊字符**:必须使用括号表示法,禁止点表示法
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// ✅ 正确
|
|
50
|
+
record['状态']
|
|
51
|
+
record['v1.0版本']
|
|
52
|
+
{ '状态': '进行中', 'v1.0版本': '已发布' }
|
|
53
|
+
|
|
54
|
+
// ❌ 错误
|
|
55
|
+
record.状态 // 语法错误
|
|
56
|
+
record.v1.0版本 // 会被错误解析
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
4. **特殊字段需特化展示**:Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
|
|
60
|
+
5. **特殊字段需专用录入组件**:SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## BizType 字段类型对照表
|
|
65
|
+
|
|
66
|
+
| 字段类型 | BizType | 字段类型 | BizType | 字段类型 | BizType |
|
|
67
|
+
| -------- | -------------- | ------------ | -------------- | -------- | ------------- |
|
|
68
|
+
| 文本 | `Text` | 单选 | `SingleSelect` | 附件 | `Attachment` |
|
|
69
|
+
| 邮箱 | `Email` | 多选 | `MultiSelect` | 单向关联 | `SingleLink` |
|
|
70
|
+
| 条码 | `Barcode` | 日期 | `DateTime` | 双向关联 | `DuplexLink` |
|
|
71
|
+
| 数字 | `Number` | 复选框 | `Checkbox` | 查找引用 | `Lookup` |
|
|
72
|
+
| 进度 | `Progress` | 人员 | `User` | 公式 | `Formula` |
|
|
73
|
+
| 货币 | `Currency` | 电话号码 | `Phone` | 地理位置 | `Location` |
|
|
74
|
+
| 评分 | `Rating` | 超链接 | `Url` | 群组 | `GroupChat` |
|
|
75
|
+
| 创建时间 | `CreatedTime` | 最后更新时间 | `ModifiedTime` | 创建人 | `CreatedUser` |
|
|
76
|
+
| 修改人 | `ModifiedUser` | 自动编号 | `AutoNumber` | 按钮 | `Button` |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## BizType 读写格式对照表
|
|
81
|
+
|
|
82
|
+
**重要**:读取和写入时的数据格式可能不同,必须严格按照下表处理。
|
|
83
|
+
|
|
84
|
+
### 可写字段
|
|
85
|
+
|
|
86
|
+
| BizType | 写入格式 | 读取格式 | 说明 |
|
|
87
|
+
| --------------------------------------- | ---------------- | ------------------ | ------------------------ |
|
|
88
|
+
| `Text`/`Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
|
|
89
|
+
| `Phone` | `string` | `string` | 读写均为字符串 |
|
|
90
|
+
| `Number`/`Progress`/`Currency`/`Rating` | `number` | `number` | 数值类型 |
|
|
91
|
+
| `SingleSelect` | `string` | `string` | 必须匹配 enumValues |
|
|
92
|
+
| `MultiSelect` | `string[]` | `string[]` | 必须匹配 enumValues |
|
|
93
|
+
| `DateTime` | `number` | `number` | Unix 时间戳(毫秒) |
|
|
94
|
+
| `Checkbox` | `boolean` | `boolean \| null` | 读取时可能为 null |
|
|
95
|
+
| `User` | `number[]` | `number[]` | suda user id 数组 |
|
|
96
|
+
| `Url` | `{ text, link }` | `{ text, link }` | 读写格式一致 |
|
|
97
|
+
| `Attachment` | `string[]` | 复杂对象 | 写入文件 URL 数组 |
|
|
98
|
+
|
|
99
|
+
### 只读字段
|
|
100
|
+
|
|
101
|
+
| BizType | 读取格式 |
|
|
102
|
+
| ---------------------------- | ---------------------------------------------- |
|
|
103
|
+
| `CreatedTime`/`ModifiedTime` | `number`(时间戳) |
|
|
104
|
+
| `CreatedUser`/`ModifiedUser` | `number[]`(user id 数组) |
|
|
105
|
+
| `AutoNumber` | `{ text: string }` |
|
|
106
|
+
| `Formula` | `{ bizType: string, value: any }` |
|
|
107
|
+
| `Location` | `{ fullAddress, provinceName, cityName, ... }` |
|
|
108
|
+
| `GroupChat` | `[{ groupID, displayName, avatarImageUrl }]` |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Actions 概览
|
|
113
|
+
|
|
114
|
+
| Action | 说明 |
|
|
115
|
+
| -------------------- | ------------------------------ |
|
|
116
|
+
| `searchRecords` | 搜索记录,支持筛选、排序、分页 |
|
|
117
|
+
| `getRecord` | 根据 ID 查询单条记录 |
|
|
118
|
+
| `batchAddRecords` | 批量新增记录(最多 500 条) |
|
|
119
|
+
| `batchUpdateRecords` | 批量更新记录(最多 500 条) |
|
|
120
|
+
| `deleteRecords` | 批量删除记录(最多 500 条) |
|
|
121
|
+
| `aggregateQuery` | 聚合查询,支持分组、聚合计算、筛选、排序、分页 |
|
|
122
|
+
|
|
123
|
+
> 注意:分析/统计类场景应优先使用 `aggregateQuery`,不要用 `searchRecords` 模拟统计。
|
|
124
|
+
|
|
125
|
+
### Action 选择决策树
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
需要什么?
|
|
129
|
+
├─ 查看/编辑单条记录 → getRecord / batchUpdateRecords
|
|
130
|
+
├─ 列表分页浏览 → searchRecords(游标分页,pageToken 翻页)
|
|
131
|
+
├─ 按条件筛选少量记录 → searchRecords + filter + pageSize
|
|
132
|
+
├─ 排行榜 / TOP N → searchRecords + sort + pageSize=N
|
|
133
|
+
├─ 统计数值(计数/求和/平均/最大最小) → aggregateQuery
|
|
134
|
+
├─ 分组统计(按部门/状态等维度) → aggregateQuery + dimensions
|
|
135
|
+
├─ 统计 + 明细下钻 → 聚合用 aggregateQuery,下钻用 searchRecords + filter
|
|
136
|
+
├─ 新增数据 → batchAddRecords
|
|
137
|
+
└─ 删除数据 → deleteRecords
|
|
138
|
+
|
|
139
|
+
⚠️ 禁止模式:
|
|
140
|
+
❌ searchRecords 拉全量 → 内存 reduce/filter 做统计 → 必须用 aggregateQuery
|
|
141
|
+
❌ searchRecords 拉全量 → sort → slice 取 TOP N → 必须用 searchRecords + sort + pageSize
|
|
142
|
+
❌ searchRecords 拉全量 → find 找单条 → 必须用 getRecord
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 数据分析类Action补充说明
|
|
146
|
+
|
|
147
|
+
`aggregateQuery` 用于分组聚合统计,支持 dimensions(分组维度)和 measures(聚合计算)。
|
|
148
|
+
|
|
149
|
+
### 输入参数
|
|
150
|
+
|
|
151
|
+
- **dimensions 支持的 bizType**:`Text`、`Email`、`Barcode`、`Number`、`Progress`、`Currency`、`Rating`、`SingleSelect`、`MultiSelect`、`DateTime`、`CreatedTime`、`ModifiedTime`、`Checkbox`、`User`、`CreatedUser`、`ModifiedUser`
|
|
152
|
+
|
|
153
|
+
### 输出结构
|
|
154
|
+
|
|
155
|
+
- 返回 `{ result, hasMore, pageToken }`
|
|
156
|
+
- `result` 是聚合结果数组,每个元素是对象,key 对应 dimensions/measures 中声明字段,value 结构为 `{ value: <actual_value> }`
|
|
157
|
+
|
|
158
|
+
### dimensions value 类型(常见)
|
|
159
|
+
|
|
160
|
+
- `Text` / `Email` / `Barcode`: `{ text: string }`
|
|
161
|
+
- `Number`: `string`
|
|
162
|
+
- `Progress`: `string`
|
|
163
|
+
- `Currency`: `string`
|
|
164
|
+
- `Rating`: `string`
|
|
165
|
+
- `SingleSelect`: `string`
|
|
166
|
+
- `MultiSelect`: `string[]`
|
|
167
|
+
- `DateTime` / `CreatedTime` / `ModifiedTime`: `string`(按多维表格配置格式)
|
|
168
|
+
- `Checkbox`: `string`(`"true"` / `"false"`)
|
|
169
|
+
- `User`: `Array<{ id, name }>`
|
|
170
|
+
- `CreatedUser` / `ModifiedUser`: `{ id, name }`
|
|
171
|
+
|
|
172
|
+
### measures value 类型
|
|
173
|
+
|
|
174
|
+
- 所有 measures 的 `value` 都是 `number`
|
|
175
|
+
|
|
176
|
+
### measures 支持的 bizType
|
|
177
|
+
|
|
178
|
+
| aggregation | 支持的 bizType | 不支持 |
|
|
179
|
+
|-------------|---------------|--------|
|
|
180
|
+
| `count` | 所有类型(但只统计非空值,统计总行数建议用不会为空的 Number 字段) | - |
|
|
181
|
+
| `sum` / `avg` | `Number`、`Currency`、`Progress`、`Rating` | **`Formula`**、`Text` |
|
|
182
|
+
| `max` / `min` | `Number`、`Currency`、`DateTime` | `Formula`、`Text` |
|
|
183
|
+
|
|
184
|
+
### filter
|
|
185
|
+
|
|
186
|
+
aggregateQuery 的 filter 格式与 searchRecords **完全相同**,遵循下方"搜索过滤条件规则"章节的所有规则。
|
|
187
|
+
|
|
188
|
+
### expandArrayDimension
|
|
189
|
+
|
|
190
|
+
- `expandArrayDimension=true` 时会先展开数组维度再聚合统计
|
|
191
|
+
- 典型场景:`MultiSelect`、`User`
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 代码示例
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { capabilityClient } from '@lark-apaas/client-toolkit-lite';
|
|
199
|
+
|
|
200
|
+
// 推荐:load 一次 executor 后复用,每次 cast 两次(详见 SKILL.md 调用规范)
|
|
201
|
+
const bitable = (capabilityClient as any).load('plugin_instance_id');
|
|
202
|
+
|
|
203
|
+
// 搜索记录
|
|
204
|
+
const response = await (bitable as any).call('searchRecords', {
|
|
205
|
+
filter: {
|
|
206
|
+
conjunction: 'and',
|
|
207
|
+
conditions: [{ fieldName: '状态', operator: 'is', value: ['进行中'] }],
|
|
208
|
+
},
|
|
209
|
+
pageSize: 20,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ⚠️ 响应数据直接从 response 访问,不要加 .data 或 .output 前缀
|
|
213
|
+
// ✅ 正确:response.records / response.hasMore / response.total / response.pageToken
|
|
214
|
+
// ❌ 错误:response.data.records / response.data.output.records
|
|
215
|
+
const { records, hasMore, total, pageToken } = response;
|
|
216
|
+
console.log(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${hasMore}`);
|
|
217
|
+
|
|
218
|
+
// 获取单条记录
|
|
219
|
+
const detail = await (bitable as any).call('getRecord', { recordID: 'recXXX' });
|
|
220
|
+
// ✅ 正确:detail.id / detail.record
|
|
221
|
+
// ❌ 错误:detail.data.output.id
|
|
222
|
+
const title = detail.record['标题']?.text;
|
|
223
|
+
|
|
224
|
+
// 批量新增(注意:写入时用写入格式,字段名必须用引号包裹)
|
|
225
|
+
await (bitable as any).call('batchAddRecords', {
|
|
226
|
+
records: [
|
|
227
|
+
{
|
|
228
|
+
record: {
|
|
229
|
+
文本字段: '内容', // Text: string(非 { text: string })
|
|
230
|
+
数字字段: 100, // Number: number
|
|
231
|
+
单选字段: '选项A', // SingleSelect: string(必须匹配 enumValues)
|
|
232
|
+
多选字段: ['标签1', '标签2'], // MultiSelect: string[]
|
|
233
|
+
复选框: true, // Checkbox: boolean
|
|
234
|
+
日期字段: Date.now(), // DateTime: number (毫秒时间戳)
|
|
235
|
+
人员字段: [123456, 789012], // User: number[] (suda user id)
|
|
236
|
+
超链接: { text: '链接文本', link: 'https://example.com' }, // Url
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 批量更新
|
|
243
|
+
await (bitable as any).call('batchUpdateRecords', {
|
|
244
|
+
records: [{ id: 'record_id', record: { 文本字段: '新内容' } }],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 删除记录
|
|
248
|
+
await (bitable as any).call('deleteRecords', { recordIDs: ['id1', 'id2'] });
|
|
249
|
+
|
|
250
|
+
// 读取时注意格式差异
|
|
251
|
+
const textValue = record.record['文本字段']?.text; // 读取时是 { text: string }
|
|
252
|
+
const numberValue = record.record['数字字段']; // number
|
|
253
|
+
const userIds = record.record['人员字段']; // number[]
|
|
254
|
+
|
|
255
|
+
// 聚合查询
|
|
256
|
+
const aggResp = await (bitable as any).call('aggregateQuery', {
|
|
257
|
+
dimensions: ['region'],
|
|
258
|
+
measures: [{ fieldName: 'order_id', aggregation: 'count', alias: 'order_count' }],
|
|
259
|
+
sort: [{ fieldName: 'region', desc: false }],
|
|
260
|
+
pageSize: 100,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const chartData =
|
|
264
|
+
aggResp.result?.map((item: Record<string, { value: unknown }>) => ({
|
|
265
|
+
region: String(item.region?.value || ''),
|
|
266
|
+
order_count: Number(item.order_count?.value || 0),
|
|
267
|
+
})) || [];
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 代码实现注意事项
|
|
271
|
+
|
|
272
|
+
1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,先用 `console.log` 打印完整响应再编码,禁止凭猜测修改数据路径。
|
|
273
|
+
2. 在进行写入操作时,比如确认提交时,应该有loading效果,禁止用户重复点击。
|
|
274
|
+
3. 如果capabilityClient call失败,命中了error逻辑,要打印日志,同时把error message提示给用户。
|
|
275
|
+
4. 在进行更新操作时,只更新需要更新的字段即可,不需要对全量字段进行更新。
|
|
276
|
+
5. **分页必须使用游标分页,禁止全量拉取后切片**。先拉全部数据再 `slice` 会导致性能极差、浪费带宽和内存。每次只拉当前页数据,通过 `pageToken` 翻页:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const ex = (capabilityClient as any).load(pluginInstanceId);
|
|
280
|
+
const result = await (ex as any).call('searchRecords', {
|
|
281
|
+
pageSize: 10,
|
|
282
|
+
pageToken: cursor, // 上一页返回的 pageToken
|
|
283
|
+
});
|
|
284
|
+
// 返回:{ records, pageToken(下一页游标), hasMore, total }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 推荐 UI 组件(vite-react / shadcn-ui)
|
|
290
|
+
|
|
291
|
+
vite-react 用的是 shadcn-ui + Radix Primitives(无 spark UI 的 `<UserDisplay>` / `<UserSelect>` / `<Hyperlink>`)。下表按 shadcn 体系给出推荐组件,**核心读写格式跟 spark 是一致的**,差异在 UI 控件本身。
|
|
292
|
+
|
|
293
|
+
| BizType | 展示 | 录入 |
|
|
294
|
+
| ---------------------------- | -------------------------------------------------------------------- | --------------------------------------------------- |
|
|
295
|
+
| `User` | `<Avatar>` + 名字(按需查询 `getCurrentUserProfile` 等接口) | 自己拼:`<Select>` / `<Combobox>` 加搜索接口 |
|
|
296
|
+
| `Url` | `<a href={val.link} target="_blank">{val.text}</a>` | 两个 `<Input>`(一个填 text,一个填 link) |
|
|
297
|
+
| `SingleSelect`/`MultiSelect` | 文本 / `<Badge>` 标签 | `<Select>` 选项来自 enumValues |
|
|
298
|
+
| `DateTime` | `new Date(ts).toLocaleDateString()` | `<Popover>` + `<Calendar>`(shadcn date picker) |
|
|
299
|
+
| `Checkbox` | `value ? '是' : '否'` | `<Checkbox />` |
|
|
300
|
+
| `Currency` | `¥${value.toFixed(2)}` | `<Input type="number" />` |
|
|
301
|
+
| `Progress` | `<Progress value={value} />` 或 `${value}%` | `<Input type="number" min={0} max={100} />` 或 `<Slider />` |
|
|
302
|
+
| `Rating` | `'★'.repeat(value) + '☆'.repeat(5-value)` | `<Input type="number" min={0} max={5} />` |
|
|
303
|
+
|
|
304
|
+
> spark 体系下的 `<UserDisplay>` / `<UserSelect>` / `<Hyperlink>` 在 vite-react 不可用。`@lark-apaas/client-toolkit-lite` 没有暴露这些组件 —— 需要自己用 shadcn 的 `<Avatar>` / `<Select>` / `<Combobox>` 拼装。人员数据的获取需要走业务 API(视具体 capability 而定)。
|
|
305
|
+
|
|
306
|
+
### 组件代码示例
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
310
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
311
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
312
|
+
|
|
313
|
+
// 单选下拉(选项必须来自 enumValues)
|
|
314
|
+
<Select value={value} onValueChange={setValue}>
|
|
315
|
+
<SelectTrigger><SelectValue placeholder="请选择" /></SelectTrigger>
|
|
316
|
+
<SelectContent>
|
|
317
|
+
{enumValues.map(opt => (
|
|
318
|
+
<SelectItem key={opt} value={opt}>{opt}</SelectItem>
|
|
319
|
+
))}
|
|
320
|
+
</SelectContent>
|
|
321
|
+
</Select>
|
|
322
|
+
|
|
323
|
+
// 人员展示(最小实现:Avatar + 名字)
|
|
324
|
+
<div className="flex items-center gap-2">
|
|
325
|
+
<Avatar className="size-6">
|
|
326
|
+
<AvatarImage src={user.avatarUrl} />
|
|
327
|
+
<AvatarFallback>{user.name?.[0]}</AvatarFallback>
|
|
328
|
+
</Avatar>
|
|
329
|
+
<span>{user.name}</span>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
// 超链接展示
|
|
333
|
+
<a href={record['超链接']?.link} target="_blank" rel="noreferrer" className="text-primary underline">
|
|
334
|
+
{record['超链接']?.text}
|
|
335
|
+
</a>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## 表格列渲染示例
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
const columns = [
|
|
344
|
+
// 文本字段 - 注意读取格式是 { text: string }
|
|
345
|
+
{ title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' },
|
|
346
|
+
// 人员字段 - vite-react 自行用 Avatar + 名字渲染(数据来源走业务接口)
|
|
347
|
+
{
|
|
348
|
+
title: '负责人',
|
|
349
|
+
key: 'user',
|
|
350
|
+
render: (_, record) => {
|
|
351
|
+
const ids = (record.record['人员字段'] as number[]) || [];
|
|
352
|
+
return ids.map((id) => <UserChip key={id} userId={id} />);
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
// 日期字段 - 格式化显示
|
|
356
|
+
{
|
|
357
|
+
title: '创建日期',
|
|
358
|
+
dataIndex: ['record', '日期字段'],
|
|
359
|
+
key: 'date',
|
|
360
|
+
render: (v) => (v ? new Date(v).toLocaleDateString() : '-'),
|
|
361
|
+
},
|
|
362
|
+
// 货币 - 格式化显示
|
|
363
|
+
{
|
|
364
|
+
title: '金额',
|
|
365
|
+
dataIndex: ['record', '货币字段'],
|
|
366
|
+
key: 'currency',
|
|
367
|
+
render: (v) => (v != null ? `¥${v.toFixed(2)}` : '-'),
|
|
368
|
+
},
|
|
369
|
+
// 超链接 - vite-react 直接渲染 <a>
|
|
370
|
+
{
|
|
371
|
+
title: '链接',
|
|
372
|
+
key: 'hyperlink',
|
|
373
|
+
render: (_, record) => {
|
|
374
|
+
const v = record.record['超链接'] as { text?: string; link?: string } | undefined;
|
|
375
|
+
return v?.link ? (
|
|
376
|
+
<a href={v.link} target="_blank" rel="noreferrer" className="text-primary underline">
|
|
377
|
+
{v.text || v.link}
|
|
378
|
+
</a>
|
|
379
|
+
) : '-';
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
// 多选 - 逗号分隔显示
|
|
383
|
+
{
|
|
384
|
+
title: '标签',
|
|
385
|
+
dataIndex: ['record', '多选字段'],
|
|
386
|
+
key: 'multiSelect',
|
|
387
|
+
render: (v) => v?.join(', ') || '-',
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 搜索过滤条件规则
|
|
395
|
+
|
|
396
|
+
### 核心规则
|
|
397
|
+
|
|
398
|
+
1. **value 始终是字符串数组**:数字需转为字符串,如 `["23.4"]`
|
|
399
|
+
2. **空值判断**:`isEmpty`/`isNotEmpty` 时 value 填 `[]`
|
|
400
|
+
3. **Formula 字段不支持筛选**
|
|
401
|
+
4. **Text 字段 `is`/`isNot` 的 value 只能传一个元素**(见下方常见错误)
|
|
402
|
+
|
|
403
|
+
### 各 BizType 的 value 格式与支持的操作符
|
|
404
|
+
|
|
405
|
+
| BizType | value 格式 | 支持的操作符 |
|
|
406
|
+
| ---------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
407
|
+
| `Text`/`Email`/`Barcode`/`Phone` | `["内容"]`(只能一个元素) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
|
|
408
|
+
| `Number`/`Currency`/`Progress`/`Rating`/`AutoNumber` | `["23.4"]`(数字转字符串) | `is`, `isNot`, `isGreater`, `isGreaterEqual`, `isLess`, `isLessEqual`, `isEmpty`, `isNotEmpty` |
|
|
409
|
+
| `SingleSelect`/`MultiSelect` | `["选项A", "选项B"]`(`is`/`isNot` 填一个,其他可多个) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
|
|
410
|
+
| `Checkbox` | `["true"]` 或 `["false"]` | `is`(仅此一个) |
|
|
411
|
+
| `User`/`CreatedUser`/`ModifiedUser` | `["123456"]`(suda user id) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
|
|
412
|
+
| `Url` | `["显示名称"]`(填显示文本,非 URL) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
|
|
413
|
+
| `GroupChat` | `["328472133423"]`(群组 ID) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
|
|
414
|
+
| `Location` | `["地址文本"]`(只能一个元素) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
|
|
415
|
+
| `Attachment` | `[]`(仅支持空值判断) | `isEmpty`, `isNotEmpty` |
|
|
416
|
+
| `DateTime`/`CreatedTime`/`ModifiedTime` | 见下方 | `is`, `isEmpty`, `isNotEmpty`, `isGreater`, `isLess` |
|
|
417
|
+
|
|
418
|
+
### 日期字段 value
|
|
419
|
+
|
|
420
|
+
| value | 含义 | operator 限制 |
|
|
421
|
+
| ------------------------------------------------------------------------- | ---------------------- | ------------- |
|
|
422
|
+
| `["ExactDate", "1702449755000"]` | 具体日期(毫秒时间戳) | - |
|
|
423
|
+
| `["Today"]`/`["Tomorrow"]`/`["Yesterday"]` | 今天/明天/昨天 | - |
|
|
424
|
+
| `["CurrentWeek"]`/`["LastWeek"]`/`["CurrentMonth"]`/`["LastMonth"]` | 本周/上周/本月/上月 | 仅 `is` |
|
|
425
|
+
| `["TheLastWeek"]`/`["TheNextWeek"]`/`["TheLastMonth"]`/`["TheNextMonth"]` | 过去/未来七天/三十天 | 仅 `is` |
|
|
426
|
+
|
|
427
|
+
### ⚠️ 常见错误(必读)
|
|
428
|
+
|
|
429
|
+
#### 1. Text 字段 `is`/`isNot` 传多个 value → 查询返回空结果
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// ❌ 错误:Text 类型 is 只能传一个元素,传多个会静默返回空
|
|
433
|
+
{ fieldName: '状态', operator: 'is', value: ['红色', '黑色'] }
|
|
434
|
+
|
|
435
|
+
// ✅ 排除少数值:多条 isNot + conjunction: 'and'
|
|
436
|
+
{
|
|
437
|
+
conjunction: 'and',
|
|
438
|
+
conditions: [
|
|
439
|
+
{ fieldName: '进展', operator: 'isNot', value: ['已完成'] },
|
|
440
|
+
{ fieldName: '进展', operator: 'isNot', value: ['已终止'] },
|
|
441
|
+
],
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ✅ 匹配多个值:用 contains 模糊匹配(适合有公共子串的场景)
|
|
445
|
+
{ fieldName: '进展', operator: 'contains', value: ['付款'] } // 匹配"付款审批"和"付款动作"
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### 2. Formula 字段不支持 filter 和 sum/avg 聚合
|
|
449
|
+
|
|
450
|
+
同一数据常有两个字段:原始 Number 字段(如 `金额`,单位元)和计算 Formula 字段(如 `金额(万元)`)。聚合和过滤**必须用 Number 字段**,在代码里做单位转换:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// ❌ 错误:金额(万元) 是 Formula,不能聚合也不能过滤
|
|
454
|
+
{ fieldName: '金额(万元)', aggregation: 'sum' }
|
|
455
|
+
{ fieldName: '金额(万元)', operator: 'isGreater', value: ['0'] }
|
|
456
|
+
|
|
457
|
+
// ✅ 正确:用 Number 类型的 金额 字段,代码里除以 10000 转万元
|
|
458
|
+
{ fieldName: '金额', aggregation: 'sum', alias: 'total_amount' }
|
|
459
|
+
// 结果处理:const amountWan = totalAmount / 10000;
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
#### 3. aggregateQuery 的 `isNot` 会排掉字段为空/null 的记录
|
|
463
|
+
|
|
464
|
+
与 searchRecords 不同,aggregateQuery 中 `isNot` 会把字段值为空的记录也排除。如果很多记录的该字段为空,结果会远少于预期甚至为 0。
|
|
465
|
+
|
|
466
|
+
解决方案:不在 filter 里用 `isNot` 排除 Text 值,改为在 dimensions 里加上该字段,在结果侧用 if 跳过不要的分组:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// ❌ 错误:isNot 会把「目前进展」为空的记录也排掉
|
|
470
|
+
filter: { conditions: [{ fieldName: '目前进展', operator: 'isNot', value: ['已完成'] }] }
|
|
471
|
+
|
|
472
|
+
// ✅ 正确:不加 filter,在 dimensions 里加「目前进展」,结果侧排除
|
|
473
|
+
dimensions: ['当前状态', '目前进展'],
|
|
474
|
+
// 解析时:if (progress === '已完成' || progress === '已终止') continue;
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### 4. count 聚合只统计非空值
|
|
478
|
+
|
|
479
|
+
统计总行数时用不会为空的 Number 字段(如"行号"),不要用可能为空的 Text 字段。
|
|
480
|
+
|
|
481
|
+
### 筛选条件构建示例
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
const filter = {
|
|
485
|
+
conjunction: 'and', // 或 'or'
|
|
486
|
+
conditions: [
|
|
487
|
+
{ fieldName: '标题', operator: 'contains', value: ['关键词'] }, // 文本包含
|
|
488
|
+
{ fieldName: '状态', operator: 'is', value: ['进行中'] }, // 单选匹配
|
|
489
|
+
{ fieldName: '金额', operator: 'isGreater', value: ['1000'] }, // 数字比较
|
|
490
|
+
{ fieldName: '创建日期', operator: 'is', value: ['Today'] }, // 日期-今天
|
|
491
|
+
{ fieldName: '截止日期', operator: 'isLess', value: ['ExactDate', Date.now().toString()] }, // 日期-具体
|
|
492
|
+
{ fieldName: '负责人', operator: 'is', value: ['123456'] }, // 人员筛选
|
|
493
|
+
{ fieldName: '备注', operator: 'isEmpty', value: [] }, // 空值检查
|
|
494
|
+
{ fieldName: '已归档', operator: 'is', value: ['true'] }, // 复选框
|
|
495
|
+
],
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const response = await capabilityClient
|
|
499
|
+
.load('plugin_instance_id')
|
|
500
|
+
.call<SearchResponse>('searchRecords', { filter, pageSize: 20 });
|
|
501
|
+
```
|
|
@@ -13,23 +13,64 @@
|
|
|
13
13
|
|
|
14
14
|
```
|
|
15
15
|
my-app/
|
|
16
|
-
├──
|
|
17
|
-
│ ├──
|
|
18
|
-
│
|
|
19
|
-
│ │
|
|
20
|
-
│
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
├── src/
|
|
17
|
+
│ ├── assets/ # 代码 import 的静态资源(图标、组件用图)
|
|
18
|
+
│ ├── components/ # 业务组件
|
|
19
|
+
│ │ └── ui/ # shadcn-ui 基础组件(不要直接改)
|
|
20
|
+
│ ├── pages/ # 路由页面
|
|
21
|
+
│ ├── hooks/ # 自定义 hooks
|
|
22
|
+
│ └── lib/ # 工具函数
|
|
23
|
+
├── public/ # 固定 URL 的公开静态资源(favicon 等)
|
|
24
|
+
├── shared/
|
|
25
|
+
│ ├── static/ # 鉴权访问的静态资源(私有源托管)
|
|
26
|
+
│ └── capabilities/ # capabilities 声明
|
|
27
|
+
├── index.html
|
|
25
28
|
├── vite.config.ts
|
|
26
|
-
├── tsconfig.json
|
|
27
29
|
└── package.json
|
|
28
30
|
```
|
|
29
31
|
|
|
32
|
+
路径别名:
|
|
33
|
+
|
|
34
|
+
- `@/*` → `./src/*`
|
|
35
|
+
- `@shared/*` → `./shared/*`
|
|
36
|
+
|
|
37
|
+
## 静态资源放哪——判定规则
|
|
38
|
+
|
|
39
|
+
按问题树走,**从上往下选第一个 yes**:
|
|
40
|
+
|
|
41
|
+
1. **代码里要 `import` 它吗?**(组件用的图标、被引用的图片、CSS 里的小图)
|
|
42
|
+
→ `src/assets/`,用 `import url from '@/assets/foo.png'`。
|
|
43
|
+
Vite 会 hash + 优化,进 CDN。**绝大多数情况选这条。**
|
|
44
|
+
|
|
45
|
+
2. **需要鉴权才能访问吗?**(仅登录用户可见的图片、PDF、配置)
|
|
46
|
+
→ `shared/static/`。通过平台运行时 SDK 拿带 token 的 URL,**不要**用 `/shared/static/...` 这种相对路径直接引。
|
|
47
|
+
|
|
48
|
+
3. **必须是固定 URL 文件名吗?**(favicon、被外部系统按 URL 抓取、第三方 SDK 写死路径)
|
|
49
|
+
→ `public/`。
|
|
50
|
+
- HTML 里:`<link rel="icon" href="/favicon.svg" />`(Vite 自动重写到 BASE_URL)
|
|
51
|
+
- JSX/CSS 里**不要**写 `<img src="/foo.png" />`——绝对路径在生产会解析到 HTML 域名而不是 CDN,会 404。必须用 `${import.meta.env.BASE_URL}foo.png`。
|
|
52
|
+
|
|
53
|
+
⚠️ 写代码默认走 `src/assets/`。`public/` 只在 1、2 都不适用时才用,并且必须用 `BASE_URL` 拼路径。
|
|
54
|
+
|
|
55
|
+
## 动态选择一组资源(国旗、头像类)
|
|
56
|
+
|
|
57
|
+
不要用 `public/flags/` + 拼字符串。用 `src/assets/` + `import.meta.glob`:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const flags = import.meta.glob('@/assets/flags/*.svg', {
|
|
61
|
+
eager: true,
|
|
62
|
+
query: '?url',
|
|
63
|
+
import: 'default',
|
|
64
|
+
}) as Record<string, string>
|
|
65
|
+
|
|
66
|
+
const url = flags[`/src/assets/flags/${code}.svg`]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
这样能拿到 hash 后的 CDN URL,且 Vite 会 tree-shake 未引用项。
|
|
70
|
+
|
|
30
71
|
## 核心约定
|
|
31
72
|
|
|
32
|
-
1. **shadcn-ui 组件不直接改源码**——需要变化时通过 className / variant props
|
|
33
|
-
2. **样式优先用 Tailwind utility**,避免写 CSS module
|
|
34
|
-
3. **表单始终用 react-hook-form + zod**——不要直接操作 input value
|
|
35
|
-
4. **图表 ECharts 优先 echarts-for-react**——React
|
|
73
|
+
1. **shadcn-ui 组件不直接改源码**——需要变化时通过 `className` / `variant` props 控制。
|
|
74
|
+
2. **样式优先用 Tailwind utility**,避免写 CSS module。
|
|
75
|
+
3. **表单始终用 react-hook-form + zod**——不要直接操作 input value。
|
|
76
|
+
4. **图表 ECharts 优先 echarts-for-react**——React 19 严格模式下注意 echarts instance 的初始化时机。
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: react-hook-best-practices
|
|
3
|
-
description: "Use when writing or reviewing React hooks: useEffect dependencies, useMemo/useCallback, custom hooks. 触发词:useEffect, useMemo, useCallback, 自定义 hook, React hook 规范"
|
|
4
|
-
steering: true
|
|
5
|
-
steering-topic: react_hook_best_practices
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# React Hook 最佳实践
|
|
9
|
-
|
|
10
|
-
## 一、useEffect 依赖
|
|
11
|
-
|
|
12
|
-
```tsx
|
|
13
|
-
// ❌ 错:缺依赖
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
fetchUser(userId);
|
|
16
|
-
}, []);
|
|
17
|
-
|
|
18
|
-
// ✅ 对:完整依赖
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
fetchUser(userId);
|
|
21
|
-
}, [userId]);
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
- 始终用 `eslint-plugin-react-hooks` 自动检查依赖
|
|
25
|
-
- 不要为了"避免重复执行"省略依赖——用 cleanup function 或 ref 处理
|
|
26
|
-
|
|
27
|
-
## 二、useMemo / useCallback
|
|
28
|
-
|
|
29
|
-
只在以下场景使用:
|
|
30
|
-
|
|
31
|
-
- 计算昂贵(实测 >5ms)
|
|
32
|
-
- 引用稳定性影响下游(passed as prop to memoized child)
|
|
33
|
-
|
|
34
|
-
默认**不预先优化**——大多数计算不需要 memo。
|
|
35
|
-
|
|
36
|
-
## 三、自定义 hook
|
|
37
|
-
|
|
38
|
-
- 命名以 `use` 开头:`useDebounce` / `useLocalStorage`
|
|
39
|
-
- 单一职责,<50 行
|
|
40
|
-
- 内部不直接操作 DOM(用 ref + useEffect)
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tailwind-conventions
|
|
3
|
-
description: "Use when styling components with Tailwind CSS: class organization, custom utilities, dark mode, responsive design. 触发词:Tailwind, className 顺序, 暗色模式, 响应式设计"
|
|
4
|
-
steering: true
|
|
5
|
-
steering-topic: tailwind_conventions
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Tailwind 约定
|
|
9
|
-
|
|
10
|
-
## 一、className 顺序
|
|
11
|
-
|
|
12
|
-
按下列顺序写 class:
|
|
13
|
-
|
|
14
|
-
1. layout(display / position / flex / grid)
|
|
15
|
-
2. spacing(margin / padding / gap)
|
|
16
|
-
3. sizing(width / height)
|
|
17
|
-
4. typography(text-* / font-*)
|
|
18
|
-
5. color(bg-* / text-* / border-*)
|
|
19
|
-
6. interactive(hover: / focus: / active:)
|
|
20
|
-
7. responsive(md: / lg:)
|
|
21
|
-
|
|
22
|
-
```tsx
|
|
23
|
-
<div className="flex items-center gap-2 px-4 py-2 text-sm text-gray-900 bg-white hover:bg-gray-50 md:px-6">
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## 二、暗色模式
|
|
27
|
-
|
|
28
|
-
- 用 `next-themes` 提供 `dark` class 切换
|
|
29
|
-
- 颜色用 `bg-white dark:bg-gray-900` 双写
|
|
30
|
-
|
|
31
|
-
## 三、避免硬编码颜色
|
|
32
|
-
|
|
33
|
-
❌ `text-[#3b82f6]`
|
|
34
|
-
✅ `text-blue-500`
|
|
35
|
-
|
|
36
|
-
如确需自定义色,加到 `tailwind.config.js` 的 theme.colors。
|