listpage_cli 0.0.297 → 0.0.299
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/bin/adapters/cli-interaction.js +38 -10
- package/bin/app/parse-args.js +2 -0
- package/bin/commands/deploy-project-command.js +1 -0
- package/bin/services/deploy-project-service.js +28 -22
- package/package.json +1 -1
- package/templates/backend-template/package.json.tmpl +1 -1
- package/templates/frontend-template/package.json.tmpl +2 -2
- package/templates/skills-template/listpage-http/SKILL.md +68 -0
- package/templates/skills-template/listpage-http/api.md +102 -0
- package/templates/skills-template/listpage-http/examples.md +167 -0
|
@@ -134,18 +134,46 @@ async function askOverwrite() {
|
|
|
134
134
|
}
|
|
135
135
|
return (0, interaction_result_1.interactionValue)(result.value.ok);
|
|
136
136
|
}
|
|
137
|
+
const HELP_ANSI = {
|
|
138
|
+
reset: "\u001b[0m",
|
|
139
|
+
dim: "\u001b[2m",
|
|
140
|
+
cyan: "\u001b[36m",
|
|
141
|
+
green: "\u001b[32m",
|
|
142
|
+
yellow: "\u001b[33m",
|
|
143
|
+
};
|
|
144
|
+
const HELP_USE_COLOR = process.env.NO_COLOR === undefined;
|
|
145
|
+
function helpColor(text, color) {
|
|
146
|
+
if (!HELP_USE_COLOR) {
|
|
147
|
+
return text;
|
|
148
|
+
}
|
|
149
|
+
return `${HELP_ANSI[color]}${text}${HELP_ANSI.reset}`;
|
|
150
|
+
}
|
|
137
151
|
function printHelp() {
|
|
138
152
|
const h = [
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
153
|
+
helpColor("listpage_cli 命令说明", "cyan"),
|
|
154
|
+
"",
|
|
155
|
+
` ${helpColor("init", "green")}`,
|
|
156
|
+
` 用法: ${helpColor("listpage_cli init", "dim")}`,
|
|
157
|
+
" 说明: 进入中文引导式交互,按提示填写即可",
|
|
158
|
+
"",
|
|
159
|
+
` ${helpColor("install-skill", "green")}`,
|
|
160
|
+
` 用法: ${helpColor("listpage_cli install-skill [skillName] [--project]", "dim")}`,
|
|
161
|
+
" 说明: 安装技能到 Cursor;默认 skillName 为 test,安装到当前命令执行目录的 .cursor/skills/",
|
|
162
|
+
"",
|
|
163
|
+
` ${helpColor("build-project", "green")}`,
|
|
164
|
+
` 用法: ${helpColor("listpage_cli build-project", "dim")}`,
|
|
165
|
+
" 说明: 非交互校验当前目录是否为有效项目根(需存在 listpage.config.json)",
|
|
166
|
+
"",
|
|
167
|
+
` ${helpColor("release-project", "green")}`,
|
|
168
|
+
` 用法: ${helpColor("listpage_cli release-project [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE]", "dim")}`,
|
|
169
|
+
" 说明: 先校验 .listpage/output 产物,再按 login/build/tag/push 执行 Docker 发布",
|
|
170
|
+
` 备注: ${helpColor("参数优先级为 CLI > profile > base", "yellow")}`,
|
|
171
|
+
"",
|
|
172
|
+
` ${helpColor("deploy-project", "green")}`,
|
|
173
|
+
` 用法: ${helpColor("listpage_cli deploy-project [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE] [--skip-image]", "dim")}`,
|
|
174
|
+
" 说明: 使用 docker.remote + docker.container 执行部署,支持 ports[] 与 envFile/env 合并",
|
|
175
|
+
` 备注: ${helpColor("默认会清理并拉取镜像;传入 --skip-image 时只执行容器相关步骤(假定镜像已是最新且已存在)", "yellow")}`,
|
|
176
|
+
` ${helpColor("参数优先级为 CLI > profile > base", "yellow")}`,
|
|
149
177
|
].join("\n");
|
|
150
178
|
console.log(h);
|
|
151
179
|
}
|
package/bin/app/parse-args.js
CHANGED
|
@@ -46,10 +46,12 @@ function parseCommandOptions(rawArgs) {
|
|
|
46
46
|
const platform = readSingleOption(args, "platform");
|
|
47
47
|
const envEntries = readMultiOption(args, "env");
|
|
48
48
|
const env = parseEnvEntries(envEntries);
|
|
49
|
+
const skipImage = args.includes("--skip-image");
|
|
49
50
|
return {
|
|
50
51
|
profile,
|
|
51
52
|
platform,
|
|
52
53
|
env: Object.keys(env).length > 0 ? env : undefined,
|
|
54
|
+
skipImage: skipImage || undefined,
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
function parseCommandPositionals(rawArgs) {
|
|
@@ -90,6 +90,7 @@ async function runDeployProjectFlow(deps) {
|
|
|
90
90
|
executionInput,
|
|
91
91
|
resolve: deps.fs.resolve,
|
|
92
92
|
readText: deps.fs.readText,
|
|
93
|
+
skipImage: deps.skipImage === true,
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
96
|
function resolveRuntimeConfig(config, cliOverrides) {
|
|
@@ -142,28 +143,6 @@ async function runDockerDeploy(input) {
|
|
|
142
143
|
const { client, executionInput } = input;
|
|
143
144
|
const remoteImage = executionInput.remoteImage;
|
|
144
145
|
logDeployStep("run", `开始部署镜像: ${remoteImage}`);
|
|
145
|
-
try {
|
|
146
|
-
logDeployStep("run", `检查本地镜像是否存在: ${remoteImage}`);
|
|
147
|
-
const exists = await client.imageExists(remoteImage);
|
|
148
|
-
logDeployStep("run", `本地镜像检查结果: exists=${exists ? "yes" : "no"}`);
|
|
149
|
-
if (exists) {
|
|
150
|
-
logDeployStep("run", `删除旧镜像: ${remoteImage}`);
|
|
151
|
-
await client.removeImage(remoteImage);
|
|
152
|
-
logDeployStep("run", `旧镜像删除完成: ${remoteImage}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}][run] 清理镜像缓存失败: ${toErrorMessage(error)}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
const auth = buildPullAuth(executionInput.docker);
|
|
160
|
-
logDeployStep("run", `开始拉取镜像: ${remoteImage} (auth=${auth ? "yes" : "no"})`);
|
|
161
|
-
await client.pullImage(remoteImage, auth);
|
|
162
|
-
logDeployStep("run", `镜像拉取完成: ${remoteImage}`);
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}][run] 拉取镜像失败: ${toErrorMessage(error)}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
166
|
-
}
|
|
167
146
|
try {
|
|
168
147
|
logDeployStep("run", `检查已有容器: ${executionInput.runtime.containerName}`);
|
|
169
148
|
const existingContainerId = await client.findContainerIdByName(executionInput.runtime.containerName);
|
|
@@ -179,6 +158,33 @@ async function runDockerDeploy(input) {
|
|
|
179
158
|
catch (error) {
|
|
180
159
|
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}][run] 替换容器失败: ${toErrorMessage(error)}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
181
160
|
}
|
|
161
|
+
if (input.skipImage !== true) {
|
|
162
|
+
try {
|
|
163
|
+
logDeployStep("run", `检查本地镜像是否存在: ${remoteImage}`);
|
|
164
|
+
const exists = await client.imageExists(remoteImage);
|
|
165
|
+
logDeployStep("run", `本地镜像检查结果: exists=${exists ? "yes" : "no"}`);
|
|
166
|
+
if (exists) {
|
|
167
|
+
logDeployStep("run", `删除旧镜像: ${remoteImage}`);
|
|
168
|
+
await client.removeImage(remoteImage);
|
|
169
|
+
logDeployStep("run", `旧镜像删除完成: ${remoteImage}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}][run] 清理镜像缓存失败: ${toErrorMessage(error)}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const auth = buildPullAuth(executionInput.docker);
|
|
177
|
+
logDeployStep("run", `开始拉取镜像: ${remoteImage} (auth=${auth ? "yes" : "no"})`);
|
|
178
|
+
await client.pullImage(remoteImage, auth);
|
|
179
|
+
logDeployStep("run", `镜像拉取完成: ${remoteImage}`);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}][run] 拉取镜像失败: ${toErrorMessage(error)}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
logDeployStep("run", `跳过镜像处理: 按照 --skip-image 假定镜像已存在且为最新`);
|
|
187
|
+
}
|
|
182
188
|
let createContainerInput;
|
|
183
189
|
try {
|
|
184
190
|
createContainerInput = (0, dockerode_client_1.toRuntimeContainerInput)({
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"react": "^19.2.0",
|
|
14
14
|
"react-dom": "^19.2.0",
|
|
15
|
-
"listpage-next": "~0.0.
|
|
15
|
+
"listpage-next": "~0.0.299",
|
|
16
16
|
"react-router-dom": ">=6.0.0",
|
|
17
17
|
"@ant-design/v5-patch-for-react-19": "~1.0.3",
|
|
18
18
|
"ahooks": "^3.9.5",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"styled-components": "^6.1.19",
|
|
24
24
|
"mobx": "~6.15.0",
|
|
25
25
|
"@ant-design/icons": "~6.0.2",
|
|
26
|
-
"listpage-components": "~0.0.
|
|
26
|
+
"listpage-components": "~0.0.299",
|
|
27
27
|
"lucide-react": "~0.575.0"
|
|
28
28
|
"mobx-react-lite": "~4.1.1"
|
|
29
29
|
},
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: listpage-http
|
|
3
|
+
description: 使用 listpage-http 在前端项目中接管所有 HTTP 请求,统一通过 src/api 目录的 request-config 与 client 调用。Use when user mentions listpage-http, request-config, api client or统一 HTTP 请求管理.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# listpage-http 使用技能
|
|
7
|
+
|
|
8
|
+
详细 API 说明与代码示例见 [api.md](api.md)、[examples.md](examples.md)。
|
|
9
|
+
|
|
10
|
+
## 总体目标
|
|
11
|
+
|
|
12
|
+
在新项目中用 `listpage-http` 接管所有 HTTP 请求,形成统一、可追踪的接口层:
|
|
13
|
+
|
|
14
|
+
- 所有后端接口 **必须在一个 request-config 映射表中集中注册**。
|
|
15
|
+
- 所有业务代码 **只能通过统一的 client 调用接口**。
|
|
16
|
+
- 每个业务模块在 `src/api` 下有对应的「类型文件」,只负责声明请求/响应类型。
|
|
17
|
+
- **接口报错已由此 API 层接管**(通过 `onError`、未授权跳转等统一处理),业务层**不需要也不应再**在调用处捕获接口错误(如对 `api.xxx.yyy()` 包一层 try/catch 专门处理接口失败)。
|
|
18
|
+
|
|
19
|
+
## 目录规范
|
|
20
|
+
|
|
21
|
+
推荐统一目录结构(可按项目实际略调,但语义不变):
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/
|
|
25
|
+
api/
|
|
26
|
+
request-config.ts # 用 defineEndpoint 注册所有接口
|
|
27
|
+
index.ts # createApiClient,默认导出 client
|
|
28
|
+
user.ts # user 模块:只放接口的 Req/Res 类型
|
|
29
|
+
order.ts # 其他模块同理……
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
规范要求:
|
|
33
|
+
|
|
34
|
+
1. **`src/api/*.ts`(如 `user.ts`)**
|
|
35
|
+
- 只定义对应模块的 **请求体类型 (ReqXXX)** 和 **响应数据类型 (ResXXX)**。
|
|
36
|
+
- 如果类型过多,可以拆分到子目录,最终由该模块 `index.ts` 统一导出类型。
|
|
37
|
+
- 不在这里直接发请求,不引入 `createApiClient`。
|
|
38
|
+
|
|
39
|
+
2. **`src/api/request-config.ts`**
|
|
40
|
+
- 作为**唯一的接口映射表**。
|
|
41
|
+
- 使用 `defineEndpoint<Req, Res>()` 注册所有后端接口。
|
|
42
|
+
- 新增接口时,必须先在这里补配置,保证接口「一表可查」。
|
|
43
|
+
|
|
44
|
+
3. **`src/api/index.ts`**
|
|
45
|
+
- 使用 `createApiClient(requestConfig, clientOptions)` 创建客户端。
|
|
46
|
+
- **默认导出 client**,供业务代码通过 `import api from '@/api'` 使用。
|
|
47
|
+
- 负责配置 baseURL、tokenKey、默认超时、成功码、未授权处理、**统一错误处理(onError)**等;接口报错由此层统一接管,业务层无需再捕获。
|
|
48
|
+
|
|
49
|
+
## 使用流程(6 步)
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Task Progress:
|
|
53
|
+
- [ ] 步骤 1:为模块创建类型文件(如 src/api/user.ts),定义 ReqXXX / ResXXX
|
|
54
|
+
- [ ] 步骤 2:在 src/api/request-config.ts 中用 defineEndpoint 注册接口
|
|
55
|
+
- [ ] 步骤 3:在 src/api/index.ts 中用 createApiClient 创建并导出 client
|
|
56
|
+
- [ ] 步骤 4:在 service / hooks 中通过 api.xxx.yyy 调用接口
|
|
57
|
+
- [ ] 步骤 5:禁止业务代码直接使用 fetch / axios 等绕过 api 目录
|
|
58
|
+
- [ ] 步骤 6:新增接口时,始终遵循「类型 → request-config → 业务调用」顺序
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 命名与模块约定
|
|
62
|
+
|
|
63
|
+
- **模块名**:`requestConfig` 中一级 key 建议与模块文件名一致,如 `user`、`order`。
|
|
64
|
+
- **方法名**:使用动词短语小驼峰,如 `login`、`list`、`detail`、`create`、`update`、`remove`。
|
|
65
|
+
- **类型命名**:
|
|
66
|
+
- 请求体:`ReqLogin`、`ReqUserList` 等。
|
|
67
|
+
- 响应数据:`ResLogin`、`ResUserList` 等。
|
|
68
|
+
- **响应 envelope**:后端返回一般为 `{ code, message, data }`,`listpage-http` 用 `ApiEnvelope<T>` 表示,内部会按 `successCodes`、`unauthorizedCodes` 解析。
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# API(listpage-http)
|
|
2
|
+
|
|
3
|
+
基于浏览器原生 `fetch` 的配置驱动 HTTP 客户端,统一处理 JSON 请求与 SSE 流。
|
|
4
|
+
|
|
5
|
+
## 核心类型与方法
|
|
6
|
+
|
|
7
|
+
来自 `listpage-http` 的主要导出(见 `src/index.ts`):
|
|
8
|
+
|
|
9
|
+
- **类型**
|
|
10
|
+
- `ClientOptions`:创建客户端时的全局配置。
|
|
11
|
+
- `RequestOptions`:单次调用时的可选配置(超时、headers、缓存等)。
|
|
12
|
+
- `EndpointConfig<Req, Res>`:单个接口配置泛型。
|
|
13
|
+
- `EndpointMode`:`"json" | "upload" | "download" | "sse"`。
|
|
14
|
+
- `ApiEnvelope<T>`:后端 `{ code, message, data }` 统一模型。
|
|
15
|
+
- **配置工具**
|
|
16
|
+
- `defineEndpoint<Req, Res>(config: EndpointConfig<Req, Res>)`:声明一个接口。
|
|
17
|
+
- **客户端**
|
|
18
|
+
- `createApiClient<Cfg>(config: Cfg, clientOptions: ClientOptions): ApiClient<Cfg>`
|
|
19
|
+
- `ApiClient<Cfg>`:根据 `requestConfig` 推导出的类型安全客户端。
|
|
20
|
+
|
|
21
|
+
### EndpointConfig<Req, Res>
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
export type EndpointConfig<Req, Res> = {
|
|
25
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
26
|
+
path: string;
|
|
27
|
+
defaultOptions?: RequestOptions;
|
|
28
|
+
type?: "http" | "sse";
|
|
29
|
+
mode?: "json" | "upload" | "download" | "sse";
|
|
30
|
+
authRequired?: boolean; // 默认 true
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### defineEndpoint
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export function defineEndpoint<Req, Res>(
|
|
38
|
+
config: EndpointConfig<Req, Res>
|
|
39
|
+
): EndpointConfig<Req, Res>;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
用途:
|
|
43
|
+
|
|
44
|
+
- 声明接口的 HTTP 方法、路径、是否 SSE、是否需要鉴权。
|
|
45
|
+
- 绑定请求泛型 `Req` 与响应泛型 `Res`,后续通过 `ApiClient` 自动推导。
|
|
46
|
+
|
|
47
|
+
### ClientOptions
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
export interface ClientOptions {
|
|
51
|
+
baseURL: string;
|
|
52
|
+
tokenKey?: string;
|
|
53
|
+
defaultTimeout?: number;
|
|
54
|
+
defaultCacheTime?: number;
|
|
55
|
+
defaultHeaders?: Record<string, string>;
|
|
56
|
+
successCodes?: number[];
|
|
57
|
+
unauthorizedCodes?: number[];
|
|
58
|
+
unauthorizedRedirectPath?: string;
|
|
59
|
+
server?: {
|
|
60
|
+
protocol?: string;
|
|
61
|
+
host?: string;
|
|
62
|
+
port?: number;
|
|
63
|
+
publicPath?: string;
|
|
64
|
+
};
|
|
65
|
+
onError?: (code: number, message: string) => void;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
说明:
|
|
70
|
+
|
|
71
|
+
- `baseURL`:通常指 `/api/v1` 这一级;也可以配合 `server` 决定完整 URL。
|
|
72
|
+
- `tokenKey`:约定从何处读取 Token(如 localStorage key),具体读取逻辑视实现而定。
|
|
73
|
+
- `successCodes`:哪些业务 code 视为成功(例如 `[20000, 200]`)。
|
|
74
|
+
- `unauthorizedCodes`:未授权业务码(如 `[401, 403]`),可配合跳登录。
|
|
75
|
+
- `onError`:统一错误处理函数,网络错误、4xx/5xx 业务错误都会透传进来。
|
|
76
|
+
|
|
77
|
+
### RequestOptions
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
export interface RequestOptions {
|
|
81
|
+
timeout?: number; // 单次调用超时
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
cache?: {
|
|
84
|
+
key?: string;
|
|
85
|
+
time?: number; // 缓存时间(ms)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
说明:
|
|
91
|
+
|
|
92
|
+
- 若不配 `timeout`,会 fallback 到 `ClientOptions.defaultTimeout`。
|
|
93
|
+
- 若不配 `cache`,则该次请求不走缓存逻辑。
|
|
94
|
+
|
|
95
|
+
### ApiClient<Cfg>
|
|
96
|
+
|
|
97
|
+
由 `createApiClient` 根据 `requestConfig` 自动推导:
|
|
98
|
+
|
|
99
|
+
- 普通 HTTP 接口:`(params: Req, options?: RequestOptions) => Promise<Res>`
|
|
100
|
+
- SSE 接口:`(params: Req, options?: RequestOptions) => Promise<ReadableStream<any>>`
|
|
101
|
+
|
|
102
|
+
业务调用时通过 `api.{module}.{fn}(params, options?)` 即可,类型由 `Req`/`Res` 自动推断。
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# listpage-http 示例
|
|
2
|
+
|
|
3
|
+
## 1. 定义 user 模块类型(`src/api/user.ts`)
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
export interface ReqLogin {
|
|
7
|
+
email: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResLogin {
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> 只放类型,不在这里发请求。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. 集中注册接口(`src/api/request-config.ts`)
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { defineEndpoint } from "listpage-http";
|
|
24
|
+
import type { ReqLogin, ResLogin } from "./user";
|
|
25
|
+
|
|
26
|
+
export const requestConfig = {
|
|
27
|
+
user: {
|
|
28
|
+
login: defineEndpoint<ReqLogin, ResLogin>({
|
|
29
|
+
method: "POST",
|
|
30
|
+
path: "/auth/login",
|
|
31
|
+
defaultOptions: {
|
|
32
|
+
timeout: 10_000,
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
// type: "http", // 默认 http,可省略
|
|
38
|
+
// mode: "json", // 默认 json,可省略
|
|
39
|
+
// authRequired: false, // 如登录接口可显式关闭鉴权
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
export type RequestConfig = typeof requestConfig;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. 创建并导出 client(`src/api/index.ts`)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createApiClient } from "listpage-http";
|
|
53
|
+
import { requestConfig, type RequestConfig } from "./request-config";
|
|
54
|
+
|
|
55
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
56
|
+
|
|
57
|
+
const api = createApiClient<RequestConfig>(requestConfig, {
|
|
58
|
+
baseURL: isDev ? "http://localhost:3000/api/v1" : "/api/v1",
|
|
59
|
+
tokenKey: "AUTH_TOKEN",
|
|
60
|
+
defaultTimeout: 10_000,
|
|
61
|
+
defaultCacheTime: 5_000,
|
|
62
|
+
successCodes: [20000, 200],
|
|
63
|
+
unauthorizedCodes: [401, 403],
|
|
64
|
+
unauthorizedRedirectPath: "/login",
|
|
65
|
+
onError: (code, message) => {
|
|
66
|
+
// 统一错误处理:可根据 code 决定弹 Toast、跳转登录等
|
|
67
|
+
console.error("[API Error]", code, message);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export default api;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 4. 在 service 中调用(推荐,不直接在组件里写细节)
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/services/auth.ts
|
|
80
|
+
import api from "@/api";
|
|
81
|
+
import type { ReqLogin, ResLogin } from "@/api/user";
|
|
82
|
+
|
|
83
|
+
export async function login(values: ReqLogin): Promise<ResLogin> {
|
|
84
|
+
const data = await api.user.login(values, {
|
|
85
|
+
timeout: 5_000, // 可选:覆盖默认超时
|
|
86
|
+
});
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 5. 在组件中使用
|
|
94
|
+
|
|
95
|
+
接口报错已由 api 层(`onError`)统一处理,业务层**不需要**对 `api` 调用再包 try/catch 来弹错误提示。
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { useState } from "react";
|
|
99
|
+
import { message } from "antd";
|
|
100
|
+
import { login } from "@/services/auth";
|
|
101
|
+
|
|
102
|
+
export function LoginForm() {
|
|
103
|
+
const [loading, setLoading] = useState(false);
|
|
104
|
+
|
|
105
|
+
const handleSubmit = async (values: { email: string; password: string }) => {
|
|
106
|
+
setLoading(true);
|
|
107
|
+
await login(values);
|
|
108
|
+
message.success("登录成功");
|
|
109
|
+
// TODO: 写入 token / 跳转
|
|
110
|
+
setLoading(false);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 此处省略表单 JSX,只保留调用方式示例
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 6. SSE 示例(可选)
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// 在 request-config 中配置 SSE 接口
|
|
124
|
+
import { defineEndpoint } from "listpage-http";
|
|
125
|
+
|
|
126
|
+
export const requestConfig = {
|
|
127
|
+
agent: {
|
|
128
|
+
generateCode: defineEndpoint<{ id: string }, void>({
|
|
129
|
+
method: "POST",
|
|
130
|
+
path: "/agent/generate-code",
|
|
131
|
+
type: "sse",
|
|
132
|
+
mode: "sse",
|
|
133
|
+
defaultOptions: {
|
|
134
|
+
timeout: 3_000, // 建连超时
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
} as const;
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// 调用方式
|
|
143
|
+
import api from "@/api";
|
|
144
|
+
|
|
145
|
+
async function startSse(id: string) {
|
|
146
|
+
const stream = await api.agent.generateCode({ id });
|
|
147
|
+
|
|
148
|
+
const reader = stream.getReader();
|
|
149
|
+
const decoder = new TextDecoder();
|
|
150
|
+
|
|
151
|
+
while (true) {
|
|
152
|
+
const { done, value } = await reader.read();
|
|
153
|
+
if (done) break;
|
|
154
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
155
|
+
console.log("SSE chunk:", chunk);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 7. 新增接口 Checklist
|
|
163
|
+
|
|
164
|
+
1. 在对应模块类型文件(如 `src/api/order.ts`)中添加 `ReqOrderList` / `ResOrderList` 等类型。
|
|
165
|
+
2. 在 `src/api/request-config.ts` 中为该模块新增一个 `defineEndpoint`(例如 `order.list`)。
|
|
166
|
+
3. 在 service/hook 中通过 `api.order.list(params)` 调用。
|
|
167
|
+
4. 禁止在业务代码中直接使用 `fetch`/`axios`,必须通过统一的 `api` 客户端。
|