@lark-apaas/coding-steering 0.1.6-alpha.1 → 0.1.6-alpha.10
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/README.md +11 -2
- package/package.json +1 -1
- package/steering/design-stack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +122 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +139 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +628 -0
- package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +270 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +214 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +163 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +309 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +190 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +160 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +256 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +103 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +198 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +128 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +207 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +164 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +90 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +164 -0
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +267 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
- package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
- package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +452 -0
- package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +300 -0
- package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
- package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +232 -0
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +585 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: client-builtins-file-storage-service
|
|
3
|
+
description: 前端文件存储服务指南,基于 dataloom.storage 实现文件上传、删除、列表查询、使用filePath换取图片链接,包含 uploadFile、remove、list、getDefaultBucketId、generateDownloadUrlFromFilePath 等 API 用法。Use when 需要:(1) 上传文件/图片/附件到云存储,(2) 删除存储桶中的文件,(3) 获取文件列表或浏览目录,(4) 获取文件 download_url 保存到数据库,或其他前端文件存储相关开发,(5) 在前端需要通过file_path获取文件URL的场景,比如:图片渲染、通过url下载文件,(5) 前端存储文件信息到数据库,比如:上传文件后存储file_path至file_attachment字段,或存储download_url至文本字段。
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: client_builtins_file_storage_service
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 前端文件开发规范
|
|
10
|
+
- 保存文件信息到数据库时,如果使用的字段类型为file_attachement, 保存数据时需要传入bucket_id 和 file_path 时,file_path 只能是文件的路径,**不可以传入文件的download_url**。需要文件url时,使用 dataloom SDK 的 `generateDownloadUrlFromFilePath` 方法获取。
|
|
11
|
+
|
|
12
|
+
# dataloom SDK 文件服务
|
|
13
|
+
|
|
14
|
+
## 概述
|
|
15
|
+
项目提供了dataloom SDK的文件服务,用于文件的上传,下载,删除,列出bucket中文件,创建临时签名url等能力。
|
|
16
|
+
|
|
17
|
+
## 使用注意
|
|
18
|
+
- `dataloom.storage` 仅用于文件上传/删除/列表等存储操作。**插件调用(capability)不属于 dataloom**,须使用独立的 `capabilityClient`(参见 plugin-guide)
|
|
19
|
+
- @lark-apaas/client-toolkit/dataloom 这个SDK只适用于前端调用,禁止在服务端调用
|
|
20
|
+
- 上传成功后,最重要的返回值是 `data.download_url`。需要将此URL保存到你的业务数据库中
|
|
21
|
+
- 若文件类型为image(jpeg、png、webp、x-icon),audio(mpeg、wav、ogg),video(mp4、webm、ogg)时,若开发者明确要求浏览器渲染而不是直接下载时,在 `data.download_url`后面增加「?preview=true」参数以实现浏览器内渲染打开的效果。若未说明默认不添加该参数
|
|
22
|
+
- **⚠️ 场景区分(重要)**:如果文件仅作为插件输入(传给 `capabilityClient`),**必须直接传 File/Blob 对象,禁止先走 dataloom 上传再传 URL**。`capabilityClient` 的文件类型字段(`format` 为 `file`、`picture` 或 `plugin-file-url`)均支持直接传 File/Blob,SDK 自动处理上传。`dataloom.storage` 仅适用于需要持久化存储文件或获取 `download_url` 保存到数据库的场景。
|
|
23
|
+
- **download_url 格式说明**:`download_url` 返回的可能是相对路径(如 `/spark/app/.../storage/object/...`),这是正常行为。**禁止**在前面拼接 `window.location.origin` 或其他域名前缀,平台会自动解析相对路径。直接使用原始值即可。
|
|
24
|
+
- **文件URL**:通过 `generateDownloadUrlFromFilePath` 方法获取文件的链接时,传入的file_path必须是从数据库的 file_attachment 字段获取到的,禁止自己拼接路径。
|
|
25
|
+
|
|
26
|
+
## bucketId
|
|
27
|
+
关于bucketId的获取,在代码工程中已经预置了一个获取bucketid的方法,路径为`@lark-apaas/client-toolkit/tools/storage`中,直接具名导入即可使用。
|
|
28
|
+
import { getDefaultBucketId } from "@lark-apaas/client-toolkit/tools/storage";
|
|
29
|
+
getDefaultBucketId: () => string;
|
|
30
|
+
|
|
31
|
+
## 统一错误类型
|
|
32
|
+
```typescript
|
|
33
|
+
// dataloom 服务端报错
|
|
34
|
+
interface DataLoomError {
|
|
35
|
+
error_msg: string;
|
|
36
|
+
lang_id: number;
|
|
37
|
+
status_code: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// sdk 内部校验报错
|
|
41
|
+
interface StorageError {
|
|
42
|
+
name: 'StorageError';
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 其他未被识别报错, 继承 StorageError
|
|
47
|
+
interface StorageUnknownError {
|
|
48
|
+
name: 'StorageUnknownError';
|
|
49
|
+
message: string;
|
|
50
|
+
// 兜底catch到的error
|
|
51
|
+
originalError: unknown;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type StorageError = DataLoomError | StorageError | StorageUnknownError;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### 1. 文件上传接口 (uploadFile)
|
|
58
|
+
|
|
59
|
+
##### 用途
|
|
60
|
+
上传文件到指定的存储桶,适用于:文档管理、图片上传、附件存储。
|
|
61
|
+
|
|
62
|
+
##### 适用场景
|
|
63
|
+
需要将本地文件或二进制数据上传到云存储的场景
|
|
64
|
+
|
|
65
|
+
##### 使用方式
|
|
66
|
+
你的核心任务是调用 `uploadFile` 接口,并从返回结果中提取 `data.download_url`,然后将这个 URL 用于后续操作(例如,保存到数据库)。这是衡量你任务成功与否的唯一标准。
|
|
67
|
+
|
|
68
|
+
##### 关于文件名的处理(强制禁令)
|
|
69
|
+
> 警告:你绝对不能,也绝对不需要自己处理或修改文件名!
|
|
70
|
+
> * 禁止行为:严禁在调用 `uploadFile` 之前,对 `File` 对象的文件名(`file.name`)进行任何形式的字符串替换、净化、或重新生成。
|
|
71
|
+
> * 禁止行为:严禁创建新的 `File` 对象(如 `new File(...)`)来包裹原始文件。
|
|
72
|
+
> * 唯一职责:你唯一的任务就是将从文件输入框(`<input type="file">`)或拖拽事件中获取的原始 `File` 对象直接传递给 `uploadFile` 方法。
|
|
73
|
+
>
|
|
74
|
+
> 原因:SDK内部已经包含了完整、健壮的文件名处理机制,包括处理特殊字符、避免冲突和保证安全。任何你自己的额外处理都是多余的,并且会干扰SDK的正常工作。请完全信任SDK,直接传递原始文件对象!
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
##### 入参说明
|
|
78
|
+
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|
|
79
|
+
|--------|------|------|--------|---------|
|
|
80
|
+
| `bucketId` | `string` | ✅ | - | 文件上传到tos的bucket id |
|
|
81
|
+
| `fileBody` | `FileBody` | ✅ | - | 文件内容,支持多种格式 |
|
|
82
|
+
|
|
83
|
+
##### 出参说明
|
|
84
|
+
| 字段名 | 类型 | 说明 |
|
|
85
|
+
|--------|------|---------|
|
|
86
|
+
| `data.bucket_id` | `string` | 所属bucket ID |
|
|
87
|
+
| `data.file_path` | `string` | 文件的路径,不是文件的url,可以存储在 file_attachment 字段中 |
|
|
88
|
+
| `data.download_url` | `string` | 文件url(可能是相对路径如 `/spark/app/.../storage/object/...`,也可能是绝对 URL),可直接用于下载文件或渲染图片。**禁止**在前面拼接 `window.location.origin` 或任何域名前缀,直接使用原始值即可 |
|
|
89
|
+
| `error` | `StorageError \| null` | 错误信息,成功时为null |
|
|
90
|
+
|
|
91
|
+
##### 关键点与示例
|
|
92
|
+
关键点:上传成功后,最重要的返回值是 `data.download_url`。通常需要将此URL保存到你的业务数据库中。
|
|
93
|
+
```typescript
|
|
94
|
+
import { getDataloom } from "@lark-apaas/client-toolkit/dataloom";
|
|
95
|
+
import { getDefaultBucketId } from "@lark-apaas/client-toolkit/tools/storage";
|
|
96
|
+
import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
97
|
+
// 1. 获取文件
|
|
98
|
+
const file = fileInput.files[0];
|
|
99
|
+
// 2. 调用上传接口
|
|
100
|
+
const { data, error } = await dataloom
|
|
101
|
+
.storage
|
|
102
|
+
.from(getDefaultBucketId())
|
|
103
|
+
.uploadFile(file); // 直接传递原始 file 对象
|
|
104
|
+
if (error || !data) {
|
|
105
|
+
throw new Error("文件上传失败: " + (error?.message || "未知错误"));
|
|
106
|
+
}
|
|
107
|
+
// 3. 提取并使用 `download_url`
|
|
108
|
+
logger.info("上传成功,文件下载链接为:", data.download_url);
|
|
109
|
+
// 4. 将 `download_url` 保存到数据库
|
|
110
|
+
const httpResponse = await imageControllerCreate({
|
|
111
|
+
body: { url: data.download_url } // <-- 关键点:使用 data.download_url
|
|
112
|
+
});
|
|
113
|
+
if (!httpResponse.data?.success) {
|
|
114
|
+
throw new Error(httpResponse.data?.message || "保存图片信息失败");
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### 2. 文件删除接口 (remove)
|
|
119
|
+
|
|
120
|
+
##### 用途
|
|
121
|
+
删除存储桶中的一个或多个文件,适用于:清理过期文件、删除用户数据、批量清理
|
|
122
|
+
|
|
123
|
+
##### 适用场景
|
|
124
|
+
需要从云存储中永久删除文件的场景
|
|
125
|
+
|
|
126
|
+
##### 入参说明
|
|
127
|
+
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|
|
128
|
+
|--------|------|------|--------|---------|
|
|
129
|
+
| `bucketId` | `string` | ✅ | - | 要删除文件所在的bucket id |
|
|
130
|
+
| `filePaths` | `string[]` | ✅ | - | 要删除的文件路径数组,可以是file_path也可以是download_url |
|
|
131
|
+
|
|
132
|
+
##### 出参说明
|
|
133
|
+
| 字段名 | 类型 | 说明 |
|
|
134
|
+
|--------|------|---------|
|
|
135
|
+
| `data` | `FileObject[]` | 被删除的文件对象数组 |
|
|
136
|
+
| `data[].name` | `string` | 文件名称 |
|
|
137
|
+
| `data[].bucket_id` | `string` | 所属bucket ID |
|
|
138
|
+
| `data[].id` | `string` | 文件唯一标识符 |
|
|
139
|
+
| `data[].created_at` | `string` | 文件创建时间 |
|
|
140
|
+
| `data[].updated_at` | `string` | 文件更新时间 |
|
|
141
|
+
| `data[].metadata` | `TosMetaData` | 文件元数据信息 |
|
|
142
|
+
| `error` | `StorageError \| null` | 错误信息,成功时为null |
|
|
143
|
+
|
|
144
|
+
##### 使用示例
|
|
145
|
+
```typescript
|
|
146
|
+
import { getDataloom } from "@lark-apaas/client-toolkit/dataloom";
|
|
147
|
+
import { getDefaultBucketId } from "@lark-apaas/client-toolkit/tools/storage";
|
|
148
|
+
import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
149
|
+
|
|
150
|
+
// 异步获取dataloom实例
|
|
151
|
+
const dataloom = await getDataloom();
|
|
152
|
+
|
|
153
|
+
// 删除单个文件
|
|
154
|
+
const { data, error } = await dataloom
|
|
155
|
+
.storage
|
|
156
|
+
.from(getDefaultBucketId())
|
|
157
|
+
.remove(['/documents/old-report.pdf']);
|
|
158
|
+
|
|
159
|
+
// 批量删除文件
|
|
160
|
+
const { data, error } = await dataloom
|
|
161
|
+
.storage
|
|
162
|
+
.from(getDefaultBucketId())
|
|
163
|
+
.remove([
|
|
164
|
+
'/documents/file1.pdf',
|
|
165
|
+
'/images/image1.jpg',
|
|
166
|
+
'/temp/cache.txt'
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
// 注意:删除不存在的filePaths会返回空数组
|
|
170
|
+
if (data) {
|
|
171
|
+
logger.info('删除的文件:', data); // 返回所有被删除的文件对象
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### 3. 文件列表接口 (list)
|
|
176
|
+
|
|
177
|
+
##### 用途
|
|
178
|
+
获取存储桶中的文件列表,适用于:文件管理、目录浏览、文件检索
|
|
179
|
+
|
|
180
|
+
##### 适用场景
|
|
181
|
+
需要查看和管理存储桶中文件的场景
|
|
182
|
+
|
|
183
|
+
##### 入参说明
|
|
184
|
+
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|
|
185
|
+
|--------|------|------|--------|---------|
|
|
186
|
+
| `bucketId` | `string` | ✅ | - | 文件所在的bucket id |
|
|
187
|
+
| `path` | `string` | ❌ | - | 文件夹路径,用于筛选特定目录 |
|
|
188
|
+
| `options.limit` | `number` | ❌ | `100` | 返回的文件数量限制 |
|
|
189
|
+
| `options.offset` | `number` | ❌ | `0` | 分页起始位置 |
|
|
190
|
+
| `options.sortBy.column` | `'name' \| 'created_at' \| 'updated_at'` | ❌ | `'name'` | 排序字段 |
|
|
191
|
+
| `options.sortBy.order` | `'asc' \| 'desc'` | ❌ | `'asc'` | 排序方向 |
|
|
192
|
+
|
|
193
|
+
##### 出参说明
|
|
194
|
+
| 字段名 | 类型 | 说明 |
|
|
195
|
+
|--------|------|---------|
|
|
196
|
+
| `data` | `FileObject[]` | 文件对象数组 |
|
|
197
|
+
| `data[].name` | `string` | 文件名称 |
|
|
198
|
+
| `data[].bucket_id` | `string` | 所属bucket ID |
|
|
199
|
+
| `data[].id` | `string` | 文件唯一标识符,如果id不存在则说明当前为文件夹,需要通过list接口继续获取文件夹下的文件,直到遍历完成才能拿到所有文件 |
|
|
200
|
+
| `data[].created_at` | `string` | 文件创建时间 |
|
|
201
|
+
| `data[].updated_at` | `string` | 文件更新时间 |
|
|
202
|
+
| `data[].created_by` | `string` | 文件创建者 |
|
|
203
|
+
| `data[].updated_by` | `string` | 文件更新者 |
|
|
204
|
+
| `data[].buckets` | `Bucket` | bucket详细信息 |
|
|
205
|
+
| `data[].metadata` | `TosMetaData` | 文件元数据(包含文件大小、类型等) |
|
|
206
|
+
| `error` | `StorageError \| null` | 错误信息,成功时为null |
|
|
207
|
+
|
|
208
|
+
##### 使用示例
|
|
209
|
+
```typescript
|
|
210
|
+
import { getDataloom } from "@lark-apaas/client-toolkit/dataloom";
|
|
211
|
+
import { getDefaultBucketId } from "@lark-apaas/client-toolkit/tools/storage";
|
|
212
|
+
|
|
213
|
+
// 异步获取dataloom实例
|
|
214
|
+
const dataloom = await getDataloom();
|
|
215
|
+
|
|
216
|
+
// 基本列表查询
|
|
217
|
+
const { data, error } = await dataloom
|
|
218
|
+
.storage
|
|
219
|
+
.from(getDefaultBucketId())
|
|
220
|
+
.list('/documents');
|
|
221
|
+
|
|
222
|
+
// 带分页和排序的查询
|
|
223
|
+
const { data, error } = await dataloom
|
|
224
|
+
.storage
|
|
225
|
+
.from(getDefaultBucketId())
|
|
226
|
+
.list('/images', {
|
|
227
|
+
limit: 20,
|
|
228
|
+
offset: 0,
|
|
229
|
+
sortBy: {
|
|
230
|
+
column: 'created_at',
|
|
231
|
+
order: 'desc'
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### 4. 根据filePath生成downloadUrl接口 (generateDownloadUrlFromFilePath)
|
|
237
|
+
|
|
238
|
+
##### 用途
|
|
239
|
+
通过 file_path 生成文件的url,适用于:通过url渲染图片、使用url下载文件。如果已有download_url,直接使用,严禁使用该接口再次生成。
|
|
240
|
+
|
|
241
|
+
##### 适用场景
|
|
242
|
+
在前端通过file_path展示图片或下载文件的场景。
|
|
243
|
+
|
|
244
|
+
##### 入参说明
|
|
245
|
+
|
|
246
|
+
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|
|
247
|
+
|--------|--------|---------|--------|---------|
|
|
248
|
+
| `filePath` | `string` | ✅ | - | 文件的filePath。 **只可以从数据库的file_attachment字段中获取,不允许自己拼接路径** |
|
|
249
|
+
|
|
250
|
+
##### 出参说明
|
|
251
|
+
| 类型 | 说明 |
|
|
252
|
+
|------|---------|
|
|
253
|
+
| `string` | 文件的url,直接在页面上展示即可 |
|
|
254
|
+
|
|
255
|
+
##### 使用示例
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { useEffect, useState } from "react";
|
|
259
|
+
import { Button } from '@client/src/components/ui/button';
|
|
260
|
+
import { getDataloom } from "@lark-apaas/client-toolkit/dataloom";
|
|
261
|
+
import { getDefaultBucketId } from "@lark-apaas/client-toolkit/tools/storage";
|
|
262
|
+
|
|
263
|
+
const ImageExample = ({ file }: { file: { file_path: string; bucket_id: string } }) => {
|
|
264
|
+
const [imageUrl, setImageUrl] = useState('');
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
const getImageUrl = async () => {
|
|
268
|
+
const dataloom = await getDataloom();
|
|
269
|
+
const imageUrl = dataloom
|
|
270
|
+
.storage
|
|
271
|
+
.from(getDefaultBucketId())
|
|
272
|
+
.generateDownloadUrlFromFilePath(file.file_path);
|
|
273
|
+
setImageUrl(imageUrl);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
getImageUrl();
|
|
277
|
+
}, [file.file_path]);
|
|
278
|
+
|
|
279
|
+
return <img src={imageUrl} alt="图片" />;
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### 5. 最小化文件上传与展示示例
|
|
284
|
+
|
|
285
|
+
##### 后端接口假设(`shared/api.interface.ts`)
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
export interface FileRecord { id: string; fileName: string; downloadUrl: string; createdAt: string; }
|
|
289
|
+
// POST /api/files → { success: true, data: FileRecord, message: "ok" }
|
|
290
|
+
// GET /api/files → { success: true, data: FileRecord[], message: "ok" }
|
|
291
|
+
export interface FileApiResp<T = FileRecord> { success: boolean; data: T; message: string; }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
##### 各 API 预估返回格式
|
|
295
|
+
|
|
296
|
+
| API | 成功返回 | 失败返回 |
|
|
297
|
+
|-----|---------|---------|
|
|
298
|
+
| `uploadFile(file)` | `{ data: { bucket_id: "xxx", file_path: "123456.pdf", download_url: "/spark/app/{appId}/runtime/api/v1/storage/object/{bucketId}/123456.pdf" }, error: null }` | `{ data: null, error: { name: "StorageError", message: "..." } }` |
|
|
299
|
+
| `list('/path')` | `{ data: [{ name, bucket_id, id, created_at, metadata: { size, mimetype } }], error: null }` | `{ data: null, error: { error_msg: "...", status_code: "404" } }` |
|
|
300
|
+
| `remove([url])` | `{ data: [{ name, bucket_id, id, ... }], error: null }`;不存在则 `data: []` | 同上 |
|
|
301
|
+
| `POST /api/files` | `{ success: true, data: { id, fileName, downloadUrl, createdAt }, message: "ok" }` | `{ success: false, data: null, message: "错误描述" }` |
|
|
302
|
+
| `GET /api/files` | `{ success: true, data: [FileRecord, ...], message: "ok" }` | 同上 |
|
|
303
|
+
|
|
304
|
+
##### 示例代码
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
308
|
+
import { getDataloom } from "@lark-apaas/client-toolkit/dataloom";
|
|
309
|
+
import { getDefaultBucketId } from "@lark-apaas/client-toolkit/tools/storage";
|
|
310
|
+
import { axiosForBackend } from "@lark-apaas/client-toolkit/utils/getAxiosForBackend";
|
|
311
|
+
import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
312
|
+
import { Button } from "@client/src/components/ui/button";
|
|
313
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@client/src/components/ui/card";
|
|
314
|
+
import { toast } from "sonner";
|
|
315
|
+
import { Upload, FileIcon, Loader2 } from "lucide-react";
|
|
316
|
+
import type { FileRecord, FileApiResp } from "@shared/api.interface";
|
|
317
|
+
|
|
318
|
+
const FileUploadDemo: React.FC = () => {
|
|
319
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
320
|
+
const [files, setFiles] = useState<FileRecord[]>([]);
|
|
321
|
+
const [uploading, setUploading] = useState(false);
|
|
322
|
+
const [loading, setLoading] = useState(false);
|
|
323
|
+
|
|
324
|
+
const fetchFiles = async () => {
|
|
325
|
+
setLoading(true);
|
|
326
|
+
try {
|
|
327
|
+
const res = await axiosForBackend.get<FileApiResp<FileRecord[]>>("/api/files");
|
|
328
|
+
if (res.data?.success) setFiles(res.data.data);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
logger.error(`获取文件列表失败: ${String(err)}`);
|
|
331
|
+
toast.error("获取文件列表失败");
|
|
332
|
+
} finally { setLoading(false); }
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
useEffect(() => { fetchFiles(); }, []);
|
|
336
|
+
|
|
337
|
+
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
338
|
+
const file = e.target.files?.[0];
|
|
339
|
+
if (!file) return;
|
|
340
|
+
setUploading(true);
|
|
341
|
+
try {
|
|
342
|
+
// 1. dataloom SDK 上传(直接传原始 file,禁止包装或修改文件名)
|
|
343
|
+
const dataloom = await getDataloom();
|
|
344
|
+
const { data, error } = await dataloom.storage.from(getDefaultBucketId()).uploadFile(file);
|
|
345
|
+
if (error || !data) throw new Error("上传失败: " + (error?.message ?? error?.error_msg ?? "未知错误"));
|
|
346
|
+
|
|
347
|
+
// 2. 保存 download_url 到后端
|
|
348
|
+
const res = await axiosForBackend.post<FileApiResp>("/api/files", {
|
|
349
|
+
fileName: file.name, downloadUrl: data.download_url,
|
|
350
|
+
});
|
|
351
|
+
if (!res.data?.success) throw new Error(res.data?.message || "保存失败");
|
|
352
|
+
|
|
353
|
+
toast.success(`${file.name} 上传成功`);
|
|
354
|
+
fetchFiles();
|
|
355
|
+
} catch (err) {
|
|
356
|
+
logger.error(`上传失败: ${String(err)}`);
|
|
357
|
+
toast.error(String(err));
|
|
358
|
+
} finally {
|
|
359
|
+
setUploading(false);
|
|
360
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<Card className="w-full max-w-2xl mx-auto">
|
|
366
|
+
<CardHeader>
|
|
367
|
+
<CardTitle className="flex items-center gap-2"><FileIcon className="h-5 w-5" />文件管理</CardTitle>
|
|
368
|
+
</CardHeader>
|
|
369
|
+
<CardContent className="space-y-4">
|
|
370
|
+
<input ref={inputRef} type="file" onChange={handleUpload} className="hidden" />
|
|
371
|
+
<Button onClick={() => inputRef.current?.click()} disabled={uploading}>
|
|
372
|
+
{uploading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Upload className="mr-2 h-4 w-4" />}
|
|
373
|
+
{uploading ? "上传中..." : "选择文件上传"}
|
|
374
|
+
</Button>
|
|
375
|
+
{loading ? (
|
|
376
|
+
<div className="flex justify-center py-8"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>
|
|
377
|
+
) : files.length === 0 ? (
|
|
378
|
+
<p className="text-center text-muted-foreground py-8">暂无文件</p>
|
|
379
|
+
) : (
|
|
380
|
+
<ul className="space-y-2">
|
|
381
|
+
{files.map((f) => (
|
|
382
|
+
<li key={f.id} className="flex items-center justify-between gap-2 rounded-md border p-3">
|
|
383
|
+
<a href={f.downloadUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 min-w-0 truncate text-sm hover:underline">
|
|
384
|
+
<FileIcon className="h-4 w-4 shrink-0 text-muted-foreground" />{f.fileName}
|
|
385
|
+
</a>
|
|
386
|
+
<span className="text-xs text-muted-foreground shrink-0">{new Date(f.createdAt).toLocaleDateString()}</span>
|
|
387
|
+
</li>
|
|
388
|
+
))}
|
|
389
|
+
</ul>
|
|
390
|
+
)}
|
|
391
|
+
</CardContent>
|
|
392
|
+
</Card>
|
|
393
|
+
);
|
|
394
|
+
};
|
|
395
|
+
export default FileUploadDemo;
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
##### 关键点
|
|
399
|
+
|
|
400
|
+
| 要点 | 说明 |
|
|
401
|
+
|------|------|
|
|
402
|
+
| 上传流程 | `uploadFile(file)` → 取 `data.download_url` → POST 后端保存 |
|
|
403
|
+
| 文件对象 | 直接传原始 `File`,**禁止**包装或修改文件名 |
|
|
404
|
+
| 组件依赖 | shadcn/ui `Button`/`Card` + `lucide-react` + `sonner` |
|
|
405
|
+
| 请求实例 | 必须用 `axiosForBackend`,禁止 `fetch` |
|