@gz-fronted/gz-pc 0.1.0 → 0.1.1-beta.2

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 CHANGED
@@ -22,8 +22,9 @@ Axios 和 ahooks 是 gz-pc 的普通 dependencies,安装 gz-pc 时会自动安
22
22
 
23
23
  ## 请求协议
24
24
 
25
- gz-fetch **只有在 HTTP Status 为 200 时才认为请求成功**。201、204 以及其他
26
- 所有状态都会进入统一错误处理。
25
+ gz-fetch 默认将所有 HTTP `2xx` 状态视为成功,包括 200、201、202、204 和 206。
26
+ 非 `2xx` 状态进入统一错误处理并抛出 `GzFetchError`,错误中保留 `status` 和
27
+ `responseData`。
27
28
 
28
29
  成功时直接返回后端原始的 `response.data`,不会返回 `AxiosResponse`,也不会:
29
30
 
@@ -31,6 +32,29 @@ gz-fetch **只有在 HTTP Status 为 200 时才认为请求成功**。201、204
31
32
  - 自动解包 `data`
32
33
  - 转换分页、字段、日期或任何服务端数据
33
34
 
35
+ 例如后端返回以下内容时,调用方取得的是包含 `code`、`data`、`message` 的完整
36
+ 对象:
37
+
38
+ ```ts
39
+ interface CreateOrderResponse {
40
+ code: number;
41
+ data: {
42
+ orderId: string;
43
+ };
44
+ message: string;
45
+ }
46
+
47
+ const response = await gzFetch<CreateOrderResponse, CreateOrderParams>({
48
+ url: '/order/create',
49
+ method: 'POST',
50
+ params,
51
+ });
52
+
53
+ response.code;
54
+ response.data.orderId;
55
+ response.message;
56
+ ```
57
+
34
58
  ```ts
35
59
  import { configureGzFetch, gzFetch } from '@gz-fronted/gz-pc/fetch';
36
60
 
@@ -105,6 +129,7 @@ function gzFetch<TResponse, TRequestParams = unknown>(
105
129
  | `skipAuth` | `boolean` | 否 | 为 `true` 时跳过 Token 注入 |
106
130
  | `responseType` | `'json' \| 'blob' \| 'text'` | 否 | 响应数据类型,默认 `json` |
107
131
  | `signal` | `AbortSignal` | 否 | 取消请求的标准 AbortSignal |
132
+ | `withCredentials` | `boolean` | 否 | 是否携带跨域 Cookie 等凭证 |
108
133
 
109
134
  请求入参字段统一使用 `params`,业务代码不需要区分 Axios 的 `params` 和 `data`:
110
135
 
@@ -112,6 +137,8 @@ function gzFetch<TResponse, TRequestParams = unknown>(
112
137
  - POST、PUT:`params` 转为 Request Body。
113
138
 
114
139
  业务请求配置不支持 `data`,也不得混用 `params` 和 `data`。
140
+ POST、PUT 的 `params` 支持 JSON 对象、`FormData`、`URLSearchParams` 和 `Blob`;
141
+ 自定义请求头通过 `headers` 传入。
115
142
 
116
143
  完整示例:
117
144
 
@@ -153,15 +180,38 @@ const result = await gzFetch<SaveResult, SaveParams>({
153
180
  | ------------------ | ----------------------------------------------------------- | ----------------- | -------------------------- |
154
181
  | `baseURL` | `string` | 无 | 所有业务请求的基础地址 |
155
182
  | `timeout` | `number` | `15000` | 默认请求超时时间,单位毫秒 |
183
+ | `validateStatus` | `(status: number) => boolean` | 所有 `2xx` | 自定义 HTTP 成功状态范围 |
156
184
  | `getToken` | `() => string \| undefined \| Promise<string \| undefined>` | 无 | 每次请求前动态获取 Token |
157
185
  | `showErrorMessage` | `boolean` | `true` | 实例级错误提示开关 |
158
186
  | `auth.headerName` | `string` | `Authorization` | Token 请求头名称 |
159
187
  | `auth.formatToken` | `(token: string) => string` | `Bearer ${token}` | Token 格式化函数 |
188
+ | `unauthorized` | `GzFetchUnauthorizedOptions` | 未开启 | 可选的 HTTP 401 统一处理 |
160
189
  | `middlewares` | `readonly GzFetchMiddleware[]` | `[]` | 请求、响应和错误中间件 |
161
190
 
162
- 当前稳定 API 没有开放 Axios 的 `withCredentials`、`paramsSerializer`、
163
- `onUploadProgress`、`validateStatus` 等配置,也暂不支持 `PATCH`。业务代码只应传入
164
- 上表列出的字段;如果后续确有通用场景,再通过 gz-pc 统一扩展类型和请求核心。
191
+ 需要覆盖默认成功状态时,只在应用初始化层配置:
192
+
193
+ ```ts
194
+ configureGzFetch({
195
+ validateStatus: (status) => status === 200 || status === 304,
196
+ });
197
+ ```
198
+
199
+ 当前稳定 API 没有开放 Axios 的 `paramsSerializer`、`onUploadProgress` 等配置,也
200
+ 暂不支持 `PATCH`。业务代码只应传入上表列出的字段;如果后续确有通用场景,再
201
+ 通过 gz-pc 统一扩展类型和请求核心。
202
+
203
+ ### 跨域凭证
204
+
205
+ 需要浏览器携带跨域 Cookie 等凭证时,在单次请求中开启 `withCredentials`:
206
+
207
+ ```ts
208
+ await gzFetch<QueryPresetResponse, QueryPresetParams>({
209
+ url: '/queryFilterTemplate/list',
210
+ method: 'POST',
211
+ params,
212
+ withCredentials: true,
213
+ });
214
+ ```
165
215
 
166
216
  ## Token
167
217
 
@@ -196,7 +246,7 @@ configureGzFetch({
196
246
 
197
247
  ## 错误提示
198
248
 
199
- HTTP 非 200 时仅尝试读取响应体的 `msg`。没有有效 `msg` 时使用默认文案,最终
249
+ HTTP 非 `2xx` 时仅尝试读取响应体的 `msg`。没有有效 `msg` 时使用默认文案,最终
200
250
  抛出 `GzFetchError`。
201
251
 
202
252
  错误类型包括:
@@ -235,6 +285,72 @@ controller.abort();
235
285
  await promise;
236
286
  ```
237
287
 
288
+ ## HTTP 401 统一处理
289
+
290
+ 401 统一处理默认关闭,未显式开启时继续执行原有 HTTP 错误提示和抛错逻辑,不弹窗、
291
+ 不跳转。需要启用时,在应用初始化配置中传入 `unauthorized.enabled: true`:
292
+
293
+ ```ts
294
+ configureGzFetch({
295
+ baseURL: '/api',
296
+ getToken: () => runtimeToken,
297
+ unauthorized: {
298
+ enabled: true,
299
+ loginUrl: '/login',
300
+ modalTitle: '登录失效',
301
+ modalMessage: '当前登录状态已失效,请重新登录。',
302
+ },
303
+ });
304
+ ```
305
+
306
+ 配置默认值如下:
307
+
308
+ | 字段 | 类型 | 默认值 |
309
+ | ---------------- | ------------ | --------------------------------------- |
310
+ | `enabled` | `boolean` | `false` |
311
+ | `loginUrl` | `string` | `/login` |
312
+ | `modalTitle` | `string` | `登录失效` |
313
+ | `modalMessage` | `string` | `当前登录状态已失效,请重新登录。` |
314
+ | `onUnauthorized` | `() => void` | 无;确认后使用 `window.location.assign` |
315
+
316
+ 为了让弹窗继承宿主项目当前的 gg-ui 主题,需要在应用根部的 `ConfigProvider` 内挂载一次
317
+ `GzFetchUnauthorizedModal`:
318
+
319
+ ```tsx
320
+ import { GzFetchUnauthorizedModal } from '@gz-fronted/gz-pc/fetch';
321
+ import { ConfigProvider } from '@chenhui996/gg-ui';
322
+
323
+ <ConfigProvider themeMode={themeMode}>
324
+ <GzFetchUnauthorizedModal />
325
+ <App />
326
+ </ConfigProvider>;
327
+ ```
328
+
329
+ 该组件使用受控 gg-ui `Modal`,不会调用脱离 React 上下文的静态
330
+ `Modal.confirm`,因此主题切换时会继续读取宿主 `ConfigProvider` 的主题 Token,
331
+ 无需复制亮色或暗色样式。业务页面不需要维护 401 状态或重复渲染弹窗。
332
+
333
+ 所有由同一份 gz-pc 包创建的默认客户端和独立客户端共享一个 401 管理器。首个 401
334
+ 打开弹窗,后续并发 401 不会创建新弹窗;取消或弹窗关闭动画结束后会释放状态,后续
335
+ 独立发生的 401 可以再次打开。确认操作也有同一状态锁,不会重复执行登录跳转。
336
+
337
+ 业务项目需要自定义 SSO、登出或状态清理时传入 `onUnauthorized`。用户确认后优先执行
338
+ 该回调;只有未提供回调时才通过 `window.location.assign(loginUrl)` 跳转:
339
+
340
+ ```ts
341
+ configureGzFetch({
342
+ unauthorized: {
343
+ enabled: true,
344
+ onUnauthorized: () => {
345
+ authService.logout();
346
+ },
347
+ },
348
+ });
349
+ ```
350
+
351
+ 开启该能力后,401 仍会作为 `GzFetchError` 抛给调用方,但请求核心不会再为同一个
352
+ 401 额外调用普通 `message.error`。其他 HTTP 错误、网络错误、超时和取消请求行为不变。
353
+
238
354
  ## 中间件
239
355
 
240
356
  中间件的 `onRequest`、`onResponse` 和 `onError` 都按注册顺序执行,不采用洋葱
@@ -2,6 +2,8 @@
2
2
 
3
3
  var ggUi = require('@chenhui996/gg-ui');
4
4
  var axios = require('axios');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
5
7
 
6
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
9
 
@@ -96,9 +98,107 @@ async function runErrorMiddlewares(error, context, middlewares) {
96
98
  }
97
99
  }
98
100
 
101
+ // src/fetch/unauthorized.ts
102
+ var DEFAULT_UNAUTHORIZED_LOGIN_URL = "/login";
103
+ var DEFAULT_UNAUTHORIZED_MODAL_TITLE = "登录失效";
104
+ var DEFAULT_UNAUTHORIZED_MODAL_MESSAGE = "当前登录状态已失效,请重新登录。";
105
+ var IDLE_SNAPSHOT = { phase: "idle" };
106
+ var snapshot = IDLE_SNAPSHOT;
107
+ var listeners = /* @__PURE__ */ new Set();
108
+ var modalHosts = /* @__PURE__ */ new Set();
109
+ var emitChange = () => {
110
+ for (const listener of listeners) {
111
+ listener();
112
+ }
113
+ };
114
+ var setSnapshot = (nextSnapshot) => {
115
+ snapshot = nextSnapshot;
116
+ emitChange();
117
+ };
118
+ var resolveUnauthorizedOptions = (options) => {
119
+ var _a, _b, _c, _d;
120
+ return {
121
+ enabled: (_a = options == null ? void 0 : options.enabled) != null ? _a : false,
122
+ loginUrl: (_b = options == null ? void 0 : options.loginUrl) != null ? _b : DEFAULT_UNAUTHORIZED_LOGIN_URL,
123
+ modalTitle: (_c = options == null ? void 0 : options.modalTitle) != null ? _c : DEFAULT_UNAUTHORIZED_MODAL_TITLE,
124
+ modalMessage: (_d = options == null ? void 0 : options.modalMessage) != null ? _d : DEFAULT_UNAUTHORIZED_MODAL_MESSAGE,
125
+ ...(options == null ? void 0 : options.onUnauthorized) === void 0 ? {} : { onUnauthorized: options.onUnauthorized }
126
+ };
127
+ };
128
+ var getUnauthorizedSnapshot = () => snapshot;
129
+ var getUnauthorizedServerSnapshot = () => IDLE_SNAPSHOT;
130
+ var subscribeUnauthorized = (listener) => {
131
+ listeners.add(listener);
132
+ return () => listeners.delete(listener);
133
+ };
134
+ var registerUnauthorizedModalHost = (hostId) => {
135
+ modalHosts.add(hostId);
136
+ if (snapshot.activeHostId === void 0) {
137
+ setSnapshot({ ...snapshot, activeHostId: hostId });
138
+ }
139
+ return () => {
140
+ modalHosts.delete(hostId);
141
+ if (snapshot.activeHostId !== hostId) {
142
+ return;
143
+ }
144
+ const nextHost = modalHosts.values().next();
145
+ const nextHostId = nextHost.done ? void 0 : nextHost.value;
146
+ if (snapshot.phase === "closing") {
147
+ setSnapshot(
148
+ nextHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: nextHostId, phase: "idle" }
149
+ );
150
+ return;
151
+ }
152
+ if (nextHostId === void 0) {
153
+ setSnapshot(
154
+ snapshot.options === void 0 ? { phase: snapshot.phase } : { options: snapshot.options, phase: snapshot.phase }
155
+ );
156
+ return;
157
+ }
158
+ setSnapshot({ ...snapshot, activeHostId: nextHostId });
159
+ };
160
+ };
161
+ var openUnauthorizedModal = (options) => {
162
+ if (!options.enabled || snapshot.phase !== "idle") {
163
+ return false;
164
+ }
165
+ setSnapshot({
166
+ ...snapshot.activeHostId === void 0 ? {} : { activeHostId: snapshot.activeHostId },
167
+ options,
168
+ phase: "open"
169
+ });
170
+ return true;
171
+ };
172
+ var beginCloseUnauthorizedModal = () => {
173
+ if (snapshot.phase !== "open" || snapshot.options === void 0) {
174
+ return void 0;
175
+ }
176
+ const { options } = snapshot;
177
+ setSnapshot({ ...snapshot, phase: "closing" });
178
+ return options;
179
+ };
180
+ var cancelUnauthorizedModal = () => beginCloseUnauthorizedModal() !== void 0;
181
+ var confirmUnauthorizedModal = () => beginCloseUnauthorizedModal();
182
+ var completeUnauthorizedModal = () => {
183
+ if (snapshot.phase !== "closing") {
184
+ return;
185
+ }
186
+ setSnapshot(
187
+ snapshot.activeHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: snapshot.activeHostId, phase: "idle" }
188
+ );
189
+ };
190
+ var executeUnauthorizedAction = (options, navigate = (loginUrl) => window.location.assign(loginUrl)) => {
191
+ if (options.onUnauthorized) {
192
+ options.onUnauthorized();
193
+ return;
194
+ }
195
+ navigate(options.loginUrl);
196
+ };
197
+
99
198
  // src/fetch/client.ts
100
199
  var DEFAULT_TIMEOUT = 15e3;
101
200
  var DEFAULT_AUTH_HEADER = "Authorization";
201
+ var DEFAULT_VALIDATE_STATUS = (status) => status >= 200 && status < 300;
102
202
  function toHeaderRecord(headers) {
103
203
  const result = {};
104
204
  for (const [name, value] of Object.entries(headers)) {
@@ -109,15 +209,16 @@ function toHeaderRecord(headers) {
109
209
  return result;
110
210
  }
111
211
  function createGzFetch(options = {}) {
112
- var _a, _b, _c, _d, _e, _f, _g;
212
+ var _a, _b, _c, _d, _e, _f, _g, _h;
113
213
  const middlewares = (_a = options.middlewares) != null ? _a : [];
114
214
  const showInstanceErrorMessage = (_b = options.showErrorMessage) != null ? _b : true;
115
215
  const headerName = (_d = (_c = options.auth) == null ? void 0 : _c.headerName) != null ? _d : DEFAULT_AUTH_HEADER;
116
216
  const formatToken = (_f = (_e = options.auth) == null ? void 0 : _e.formatToken) != null ? _f : ((token) => `Bearer ${token}`);
217
+ const unauthorizedOptions = resolveUnauthorizedOptions(options.unauthorized);
117
218
  const instance = axios__default.default.create({
118
219
  ...options.baseURL === void 0 ? {} : { baseURL: options.baseURL },
119
220
  timeout: (_g = options.timeout) != null ? _g : DEFAULT_TIMEOUT,
120
- validateStatus: (status) => status === 200
221
+ validateStatus: (_h = options.validateStatus) != null ? _h : DEFAULT_VALIDATE_STATUS
121
222
  });
122
223
  async function request(initialConfig) {
123
224
  var _a2;
@@ -143,7 +244,8 @@ function createGzFetch(options = {}) {
143
244
  ...usesRequestBody || currentConfig.params === void 0 ? {} : { params: currentConfig.params },
144
245
  ...currentConfig.timeout === void 0 ? {} : { timeout: currentConfig.timeout },
145
246
  ...currentConfig.responseType === void 0 ? {} : { responseType: currentConfig.responseType },
146
- ...currentConfig.signal === void 0 ? {} : { signal: currentConfig.signal }
247
+ ...currentConfig.signal === void 0 ? {} : { signal: currentConfig.signal },
248
+ ...currentConfig.withCredentials === void 0 ? {} : { withCredentials: currentConfig.withCredentials }
147
249
  };
148
250
  const response = await instance.request(axiosConfig);
149
251
  const context = {
@@ -154,9 +256,13 @@ function createGzFetch(options = {}) {
154
256
  return await runResponseMiddlewares(response.data, context, middlewares);
155
257
  } catch (originalError) {
156
258
  const error = toGzFetchError(originalError);
259
+ const shouldHandleUnauthorized = unauthorizedOptions.enabled && error.type === "HTTP_ERROR" && error.status === 401;
260
+ if (shouldHandleUnauthorized) {
261
+ openUnauthorizedModal(unauthorizedOptions);
262
+ }
157
263
  await runErrorMiddlewares(error, { config: currentConfig }, middlewares);
158
264
  const shouldShowError = (_a2 = currentConfig.showErrorMessage) != null ? _a2 : showInstanceErrorMessage;
159
- if (shouldShowError && error.type !== "CANCELED_ERROR") {
265
+ if (shouldShowError && error.type !== "CANCELED_ERROR" && !shouldHandleUnauthorized) {
160
266
  ggUi.message.error(error.message);
161
267
  }
162
268
  throw error;
@@ -192,8 +298,46 @@ async function gzFetch(config) {
192
298
  }
193
299
  return defaultClient(config);
194
300
  }
301
+ var GzFetchUnauthorizedModal = () => {
302
+ var _a, _b, _c;
303
+ const hostIdRef = react.useRef(void 0);
304
+ (_a = hostIdRef.current) != null ? _a : hostIdRef.current = /* @__PURE__ */ Symbol("gz-fetch-unauthorized-modal-host");
305
+ const hostId = hostIdRef.current;
306
+ const snapshot2 = react.useSyncExternalStore(
307
+ subscribeUnauthorized,
308
+ getUnauthorizedSnapshot,
309
+ getUnauthorizedServerSnapshot
310
+ );
311
+ react.useEffect(() => registerUnauthorizedModalHost(hostId), [hostId]);
312
+ if (snapshot2.activeHostId !== hostId) {
313
+ return null;
314
+ }
315
+ const handleConfirm = () => {
316
+ const options = confirmUnauthorizedModal();
317
+ if (options) {
318
+ executeUnauthorizedAction(options);
319
+ }
320
+ };
321
+ return /* @__PURE__ */ jsxRuntime.jsx(
322
+ ggUi.Modal,
323
+ {
324
+ afterClose: completeUnauthorizedModal,
325
+ cancelText: "取消",
326
+ closable: true,
327
+ okText: "重新登录",
328
+ open: snapshot2.phase === "open",
329
+ title: (_b = snapshot2.options) == null ? void 0 : _b.modalTitle,
330
+ onCancel: cancelUnauthorizedModal,
331
+ onOk: handleConfirm,
332
+ children: (_c = snapshot2.options) == null ? void 0 : _c.modalMessage
333
+ }
334
+ );
335
+ };
336
+ GzFetchUnauthorizedModal.displayName = "GzFetchUnauthorizedModal";
337
+ var GzFetchUnauthorizedModal_default = GzFetchUnauthorizedModal;
195
338
 
196
339
  exports.GzFetchError = GzFetchError;
340
+ exports.GzFetchUnauthorizedModal = GzFetchUnauthorizedModal_default;
197
341
  exports.configureGzFetch = configureGzFetch;
198
342
  exports.createGzFetch = createGzFetch;
199
343
  exports.getResponseMessage = getResponseMessage;
@@ -1,4 +1,5 @@
1
1
  import { AxiosHeaders } from 'axios';
2
+ import { FC } from 'react';
2
3
 
3
4
  type GzFetchErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'CANCELED_ERROR' | 'UNKNOWN_ERROR';
4
5
  interface GzFetchErrorOptions {
@@ -31,6 +32,7 @@ interface GzRequestConfig<TRequestParams = unknown> {
31
32
  skipAuth?: boolean;
32
33
  responseType?: GzResponseType;
33
34
  signal?: AbortSignal;
35
+ withCredentials?: boolean;
34
36
  }
35
37
  interface GzInternalRequestConfig extends Omit<GzRequestConfig<unknown>, 'headers'> {
36
38
  headers?: GzRequestHeaders;
@@ -53,12 +55,21 @@ interface GzFetchAuthOptions {
53
55
  headerName?: string;
54
56
  formatToken?: (token: string) => string;
55
57
  }
58
+ interface GzFetchUnauthorizedOptions {
59
+ enabled?: boolean;
60
+ loginUrl?: string;
61
+ modalTitle?: string;
62
+ modalMessage?: string;
63
+ onUnauthorized?: () => void;
64
+ }
56
65
  interface CreateGzFetchOptions {
57
66
  baseURL?: string;
58
67
  timeout?: number;
68
+ validateStatus?: (status: number) => boolean;
59
69
  getToken?: () => string | undefined | Promise<string | undefined>;
60
70
  showErrorMessage?: boolean;
61
71
  auth?: GzFetchAuthOptions;
72
+ unauthorized?: GzFetchUnauthorizedOptions;
62
73
  middlewares?: readonly GzFetchMiddleware[];
63
74
  }
64
75
  interface GzFetchClient {
@@ -75,4 +86,6 @@ declare function createGzFetch(options?: CreateGzFetchOptions): GzFetchClient;
75
86
  declare function configureGzFetch(options: CreateGzFetchOptions): void;
76
87
  declare function gzFetch<TResponse, TRequestParams = unknown>(config: GzRequestConfig<TRequestParams>): Promise<TResponse>;
77
88
 
78
- export { type CreateGzFetchOptions, type GzErrorContext, type GzFetch, type GzFetchAuthOptions, type GzFetchClient, GzFetchError, type GzFetchErrorOptions, type GzFetchErrorType, type GzFetchMiddleware, type GzInternalRequestConfig, type GzRequestConfig, type GzRequestHeaders, type GzRequestMethod, type GzRequestOptions, type GzRequestParams, type GzResponseContext, type GzResponseType, configureGzFetch, createGzFetch, getResponseMessage, gzFetch, toGzFetchError };
89
+ declare const GzFetchUnauthorizedModal: FC;
90
+
91
+ export { type CreateGzFetchOptions, type GzErrorContext, type GzFetch, type GzFetchAuthOptions, type GzFetchClient, GzFetchError, type GzFetchErrorOptions, type GzFetchErrorType, type GzFetchMiddleware, GzFetchUnauthorizedModal, type GzFetchUnauthorizedOptions, type GzInternalRequestConfig, type GzRequestConfig, type GzRequestHeaders, type GzRequestMethod, type GzRequestOptions, type GzRequestParams, type GzResponseContext, type GzResponseType, configureGzFetch, createGzFetch, getResponseMessage, gzFetch, toGzFetchError };
@@ -1,4 +1,5 @@
1
1
  import { AxiosHeaders } from 'axios';
2
+ import { FC } from 'react';
2
3
 
3
4
  type GzFetchErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'CANCELED_ERROR' | 'UNKNOWN_ERROR';
4
5
  interface GzFetchErrorOptions {
@@ -31,6 +32,7 @@ interface GzRequestConfig<TRequestParams = unknown> {
31
32
  skipAuth?: boolean;
32
33
  responseType?: GzResponseType;
33
34
  signal?: AbortSignal;
35
+ withCredentials?: boolean;
34
36
  }
35
37
  interface GzInternalRequestConfig extends Omit<GzRequestConfig<unknown>, 'headers'> {
36
38
  headers?: GzRequestHeaders;
@@ -53,12 +55,21 @@ interface GzFetchAuthOptions {
53
55
  headerName?: string;
54
56
  formatToken?: (token: string) => string;
55
57
  }
58
+ interface GzFetchUnauthorizedOptions {
59
+ enabled?: boolean;
60
+ loginUrl?: string;
61
+ modalTitle?: string;
62
+ modalMessage?: string;
63
+ onUnauthorized?: () => void;
64
+ }
56
65
  interface CreateGzFetchOptions {
57
66
  baseURL?: string;
58
67
  timeout?: number;
68
+ validateStatus?: (status: number) => boolean;
59
69
  getToken?: () => string | undefined | Promise<string | undefined>;
60
70
  showErrorMessage?: boolean;
61
71
  auth?: GzFetchAuthOptions;
72
+ unauthorized?: GzFetchUnauthorizedOptions;
62
73
  middlewares?: readonly GzFetchMiddleware[];
63
74
  }
64
75
  interface GzFetchClient {
@@ -75,4 +86,6 @@ declare function createGzFetch(options?: CreateGzFetchOptions): GzFetchClient;
75
86
  declare function configureGzFetch(options: CreateGzFetchOptions): void;
76
87
  declare function gzFetch<TResponse, TRequestParams = unknown>(config: GzRequestConfig<TRequestParams>): Promise<TResponse>;
77
88
 
78
- export { type CreateGzFetchOptions, type GzErrorContext, type GzFetch, type GzFetchAuthOptions, type GzFetchClient, GzFetchError, type GzFetchErrorOptions, type GzFetchErrorType, type GzFetchMiddleware, type GzInternalRequestConfig, type GzRequestConfig, type GzRequestHeaders, type GzRequestMethod, type GzRequestOptions, type GzRequestParams, type GzResponseContext, type GzResponseType, configureGzFetch, createGzFetch, getResponseMessage, gzFetch, toGzFetchError };
89
+ declare const GzFetchUnauthorizedModal: FC;
90
+
91
+ export { type CreateGzFetchOptions, type GzErrorContext, type GzFetch, type GzFetchAuthOptions, type GzFetchClient, GzFetchError, type GzFetchErrorOptions, type GzFetchErrorType, type GzFetchMiddleware, GzFetchUnauthorizedModal, type GzFetchUnauthorizedOptions, type GzInternalRequestConfig, type GzRequestConfig, type GzRequestHeaders, type GzRequestMethod, type GzRequestOptions, type GzRequestParams, type GzResponseContext, type GzResponseType, configureGzFetch, createGzFetch, getResponseMessage, gzFetch, toGzFetchError };
@@ -1,5 +1,7 @@
1
- import { message } from '@chenhui996/gg-ui';
1
+ import { Modal, message } from '@chenhui996/gg-ui';
2
2
  import axios, { AxiosError, AxiosHeaders } from 'axios';
3
+ import { useRef, useSyncExternalStore, useEffect } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
3
5
 
4
6
  // src/fetch/client.ts
5
7
  var GzFetchError = class extends Error {
@@ -90,9 +92,107 @@ async function runErrorMiddlewares(error, context, middlewares) {
90
92
  }
91
93
  }
92
94
 
95
+ // src/fetch/unauthorized.ts
96
+ var DEFAULT_UNAUTHORIZED_LOGIN_URL = "/login";
97
+ var DEFAULT_UNAUTHORIZED_MODAL_TITLE = "登录失效";
98
+ var DEFAULT_UNAUTHORIZED_MODAL_MESSAGE = "当前登录状态已失效,请重新登录。";
99
+ var IDLE_SNAPSHOT = { phase: "idle" };
100
+ var snapshot = IDLE_SNAPSHOT;
101
+ var listeners = /* @__PURE__ */ new Set();
102
+ var modalHosts = /* @__PURE__ */ new Set();
103
+ var emitChange = () => {
104
+ for (const listener of listeners) {
105
+ listener();
106
+ }
107
+ };
108
+ var setSnapshot = (nextSnapshot) => {
109
+ snapshot = nextSnapshot;
110
+ emitChange();
111
+ };
112
+ var resolveUnauthorizedOptions = (options) => {
113
+ var _a, _b, _c, _d;
114
+ return {
115
+ enabled: (_a = options == null ? void 0 : options.enabled) != null ? _a : false,
116
+ loginUrl: (_b = options == null ? void 0 : options.loginUrl) != null ? _b : DEFAULT_UNAUTHORIZED_LOGIN_URL,
117
+ modalTitle: (_c = options == null ? void 0 : options.modalTitle) != null ? _c : DEFAULT_UNAUTHORIZED_MODAL_TITLE,
118
+ modalMessage: (_d = options == null ? void 0 : options.modalMessage) != null ? _d : DEFAULT_UNAUTHORIZED_MODAL_MESSAGE,
119
+ ...(options == null ? void 0 : options.onUnauthorized) === void 0 ? {} : { onUnauthorized: options.onUnauthorized }
120
+ };
121
+ };
122
+ var getUnauthorizedSnapshot = () => snapshot;
123
+ var getUnauthorizedServerSnapshot = () => IDLE_SNAPSHOT;
124
+ var subscribeUnauthorized = (listener) => {
125
+ listeners.add(listener);
126
+ return () => listeners.delete(listener);
127
+ };
128
+ var registerUnauthorizedModalHost = (hostId) => {
129
+ modalHosts.add(hostId);
130
+ if (snapshot.activeHostId === void 0) {
131
+ setSnapshot({ ...snapshot, activeHostId: hostId });
132
+ }
133
+ return () => {
134
+ modalHosts.delete(hostId);
135
+ if (snapshot.activeHostId !== hostId) {
136
+ return;
137
+ }
138
+ const nextHost = modalHosts.values().next();
139
+ const nextHostId = nextHost.done ? void 0 : nextHost.value;
140
+ if (snapshot.phase === "closing") {
141
+ setSnapshot(
142
+ nextHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: nextHostId, phase: "idle" }
143
+ );
144
+ return;
145
+ }
146
+ if (nextHostId === void 0) {
147
+ setSnapshot(
148
+ snapshot.options === void 0 ? { phase: snapshot.phase } : { options: snapshot.options, phase: snapshot.phase }
149
+ );
150
+ return;
151
+ }
152
+ setSnapshot({ ...snapshot, activeHostId: nextHostId });
153
+ };
154
+ };
155
+ var openUnauthorizedModal = (options) => {
156
+ if (!options.enabled || snapshot.phase !== "idle") {
157
+ return false;
158
+ }
159
+ setSnapshot({
160
+ ...snapshot.activeHostId === void 0 ? {} : { activeHostId: snapshot.activeHostId },
161
+ options,
162
+ phase: "open"
163
+ });
164
+ return true;
165
+ };
166
+ var beginCloseUnauthorizedModal = () => {
167
+ if (snapshot.phase !== "open" || snapshot.options === void 0) {
168
+ return void 0;
169
+ }
170
+ const { options } = snapshot;
171
+ setSnapshot({ ...snapshot, phase: "closing" });
172
+ return options;
173
+ };
174
+ var cancelUnauthorizedModal = () => beginCloseUnauthorizedModal() !== void 0;
175
+ var confirmUnauthorizedModal = () => beginCloseUnauthorizedModal();
176
+ var completeUnauthorizedModal = () => {
177
+ if (snapshot.phase !== "closing") {
178
+ return;
179
+ }
180
+ setSnapshot(
181
+ snapshot.activeHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: snapshot.activeHostId, phase: "idle" }
182
+ );
183
+ };
184
+ var executeUnauthorizedAction = (options, navigate = (loginUrl) => window.location.assign(loginUrl)) => {
185
+ if (options.onUnauthorized) {
186
+ options.onUnauthorized();
187
+ return;
188
+ }
189
+ navigate(options.loginUrl);
190
+ };
191
+
93
192
  // src/fetch/client.ts
94
193
  var DEFAULT_TIMEOUT = 15e3;
95
194
  var DEFAULT_AUTH_HEADER = "Authorization";
195
+ var DEFAULT_VALIDATE_STATUS = (status) => status >= 200 && status < 300;
96
196
  function toHeaderRecord(headers) {
97
197
  const result = {};
98
198
  for (const [name, value] of Object.entries(headers)) {
@@ -103,15 +203,16 @@ function toHeaderRecord(headers) {
103
203
  return result;
104
204
  }
105
205
  function createGzFetch(options = {}) {
106
- var _a, _b, _c, _d, _e, _f, _g;
206
+ var _a, _b, _c, _d, _e, _f, _g, _h;
107
207
  const middlewares = (_a = options.middlewares) != null ? _a : [];
108
208
  const showInstanceErrorMessage = (_b = options.showErrorMessage) != null ? _b : true;
109
209
  const headerName = (_d = (_c = options.auth) == null ? void 0 : _c.headerName) != null ? _d : DEFAULT_AUTH_HEADER;
110
210
  const formatToken = (_f = (_e = options.auth) == null ? void 0 : _e.formatToken) != null ? _f : ((token) => `Bearer ${token}`);
211
+ const unauthorizedOptions = resolveUnauthorizedOptions(options.unauthorized);
111
212
  const instance = axios.create({
112
213
  ...options.baseURL === void 0 ? {} : { baseURL: options.baseURL },
113
214
  timeout: (_g = options.timeout) != null ? _g : DEFAULT_TIMEOUT,
114
- validateStatus: (status) => status === 200
215
+ validateStatus: (_h = options.validateStatus) != null ? _h : DEFAULT_VALIDATE_STATUS
115
216
  });
116
217
  async function request(initialConfig) {
117
218
  var _a2;
@@ -137,7 +238,8 @@ function createGzFetch(options = {}) {
137
238
  ...usesRequestBody || currentConfig.params === void 0 ? {} : { params: currentConfig.params },
138
239
  ...currentConfig.timeout === void 0 ? {} : { timeout: currentConfig.timeout },
139
240
  ...currentConfig.responseType === void 0 ? {} : { responseType: currentConfig.responseType },
140
- ...currentConfig.signal === void 0 ? {} : { signal: currentConfig.signal }
241
+ ...currentConfig.signal === void 0 ? {} : { signal: currentConfig.signal },
242
+ ...currentConfig.withCredentials === void 0 ? {} : { withCredentials: currentConfig.withCredentials }
141
243
  };
142
244
  const response = await instance.request(axiosConfig);
143
245
  const context = {
@@ -148,9 +250,13 @@ function createGzFetch(options = {}) {
148
250
  return await runResponseMiddlewares(response.data, context, middlewares);
149
251
  } catch (originalError) {
150
252
  const error = toGzFetchError(originalError);
253
+ const shouldHandleUnauthorized = unauthorizedOptions.enabled && error.type === "HTTP_ERROR" && error.status === 401;
254
+ if (shouldHandleUnauthorized) {
255
+ openUnauthorizedModal(unauthorizedOptions);
256
+ }
151
257
  await runErrorMiddlewares(error, { config: currentConfig }, middlewares);
152
258
  const shouldShowError = (_a2 = currentConfig.showErrorMessage) != null ? _a2 : showInstanceErrorMessage;
153
- if (shouldShowError && error.type !== "CANCELED_ERROR") {
259
+ if (shouldShowError && error.type !== "CANCELED_ERROR" && !shouldHandleUnauthorized) {
154
260
  message.error(error.message);
155
261
  }
156
262
  throw error;
@@ -186,5 +292,42 @@ async function gzFetch(config) {
186
292
  }
187
293
  return defaultClient(config);
188
294
  }
295
+ var GzFetchUnauthorizedModal = () => {
296
+ var _a, _b, _c;
297
+ const hostIdRef = useRef(void 0);
298
+ (_a = hostIdRef.current) != null ? _a : hostIdRef.current = /* @__PURE__ */ Symbol("gz-fetch-unauthorized-modal-host");
299
+ const hostId = hostIdRef.current;
300
+ const snapshot2 = useSyncExternalStore(
301
+ subscribeUnauthorized,
302
+ getUnauthorizedSnapshot,
303
+ getUnauthorizedServerSnapshot
304
+ );
305
+ useEffect(() => registerUnauthorizedModalHost(hostId), [hostId]);
306
+ if (snapshot2.activeHostId !== hostId) {
307
+ return null;
308
+ }
309
+ const handleConfirm = () => {
310
+ const options = confirmUnauthorizedModal();
311
+ if (options) {
312
+ executeUnauthorizedAction(options);
313
+ }
314
+ };
315
+ return /* @__PURE__ */ jsx(
316
+ Modal,
317
+ {
318
+ afterClose: completeUnauthorizedModal,
319
+ cancelText: "取消",
320
+ closable: true,
321
+ okText: "重新登录",
322
+ open: snapshot2.phase === "open",
323
+ title: (_b = snapshot2.options) == null ? void 0 : _b.modalTitle,
324
+ onCancel: cancelUnauthorizedModal,
325
+ onOk: handleConfirm,
326
+ children: (_c = snapshot2.options) == null ? void 0 : _c.modalMessage
327
+ }
328
+ );
329
+ };
330
+ GzFetchUnauthorizedModal.displayName = "GzFetchUnauthorizedModal";
331
+ var GzFetchUnauthorizedModal_default = GzFetchUnauthorizedModal;
189
332
 
190
- export { GzFetchError, configureGzFetch, createGzFetch, getResponseMessage, gzFetch, toGzFetchError };
333
+ export { GzFetchError, GzFetchUnauthorizedModal_default as GzFetchUnauthorizedModal, configureGzFetch, createGzFetch, getResponseMessage, gzFetch, toGzFetchError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gz-fronted/gz-pc",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-beta.2",
4
4
  "description": "Shared fetch, hooks and utility primitives for GZ frontend applications.",
5
5
  "license": "MIT",
6
6
  "type": "module",