@gz-fronted/gz-pc 0.1.2 → 0.1.3-beta.0

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
@@ -126,7 +126,7 @@ function gzFetch<TResponse, TRequestParams = unknown>(
126
126
  | `paramsInUrl` | `boolean` | 否 | 仅 DELETE 生效;为 `true` 时参数放入 Query |
127
127
  | `headers` | `Record<string, string>` | 否 | 本次请求附加的请求头 |
128
128
  | `timeout` | `number` | 否 | 本次请求超时时间,单位毫秒;优先于实例配置 |
129
- | `showErrorMessage` | `boolean` | 否 | 是否调用 gg-ui 的 `message.error` |
129
+ | `showErrorMessage` | `boolean` | 否 | 是否显示普通消息或 Debug Drawer 错误反馈 |
130
130
  | `skipAuth` | `boolean` | 否 | 为 `true` 时跳过 Token 注入 |
131
131
  | `responseType` | `'json' \| 'blob' \| 'text'` | 否 | 响应数据类型,默认 `json` |
132
132
  | `signal` | `AbortSignal` | 否 | 取消请求的标准 AbortSignal |
@@ -270,7 +270,7 @@ HTTP 非 `2xx` 时仅尝试读取响应体的 `msg`。没有有效 `msg` 时使
270
270
  - `CANCELED_ERROR`
271
271
  - `UNKNOWN_ERROR`
272
272
 
273
- 错误 Message 默认开启,优先级为“单请求配置 > 实例配置 > 默认值 true”:
273
+ 错误反馈默认开启,优先级为“单请求配置 > 实例配置 > 默认值 true”:
274
274
 
275
275
  ```ts
276
276
  configureGzFetch({ showErrorMessage: true });
@@ -283,7 +283,7 @@ await gzFetch<void, SaveParams>({
283
283
  });
284
284
  ```
285
285
 
286
- 取消请求会转换成 `CANCELED_ERROR`,且默认不调用 `message.error`:
286
+ 取消请求会转换成 `CANCELED_ERROR`,且默认不显示错误反馈:
287
287
 
288
288
  ```ts
289
289
  const controller = new AbortController();
@@ -326,7 +326,7 @@ configureGzFetch({
326
326
  | `modalMessage` | `string` | `当前登录状态已失效,请重新登录。` |
327
327
  | `onUnauthorized` | `() => void` | 无;确认后使用 `window.location.assign` |
328
328
 
329
- 为了让普通错误消息和 401 弹窗都继承宿主项目当前的 gg-ui 主题,需要在应用根部的
329
+ 为了让普通错误消息、Debug Drawer 和 401 弹窗都继承宿主项目当前的 gg-ui 主题,需要在应用根部的
330
330
  `ConfigProvider` 内挂载一次 `GzFetchFeedbackProvider`:
331
331
 
332
332
  ```tsx
@@ -340,12 +340,11 @@ import { ConfigProvider } from '@chenhui996/gg-ui';
340
340
  </ConfigProvider>;
341
341
  ```
342
342
 
343
- 该 Provider 使用 `message.useMessage()` 和受控 gg-ui `Modal`,不会在已接入的项目中
344
- 调用脱离 React 上下文的静态反馈 API。因此主题切换时,普通错误消息和 401 弹窗都会
345
- 继续读取宿主 `ConfigProvider` 的主题 Token,无需复制亮色或暗色样式。业务页面不需要
346
- 维护反馈状态。未挂载该 Provider 的已有项目仍会回退到原来的静态 `message.error`,
347
- 不会产生 breaking change。`GzFetchUnauthorizedModal` 继续保留为兼容 API,但新项目
348
- 优先使用统一 Provider。
343
+ 该 Provider 使用 `message.useMessage()`、受控 gg-ui `Modal` 和 `Drawer`,不会在已接入
344
+ 的项目中调用脱离 React 上下文的静态反馈 API。因此主题切换时,三类反馈都会继续读取
345
+ 宿主 `ConfigProvider` 的主题 Token。业务页面不需要维护反馈状态。未挂载该 Provider 的
346
+ 已有项目仍会回退到原来的静态 `message.error`,不会静默丢失错误提示。
347
+ `GzFetchUnauthorizedModal` 继续保留为兼容 API,但新项目优先使用统一 Provider。
349
348
 
350
349
  所有由同一份 gz-pc 包创建的默认客户端和独立客户端共享一个 401 管理器。首个 401
351
350
  打开弹窗,后续并发 401 不会创建新弹窗;取消或弹窗关闭动画结束后会释放状态,后续
@@ -368,6 +367,24 @@ configureGzFetch({
368
367
  开启该能力后,401 仍会作为 `GzFetchError` 抛给调用方,但请求核心不会再为同一个
369
368
  401 额外调用普通 `message.error`。其他 HTTP 错误、网络错误、超时和取消请求行为不变。
370
369
 
370
+ ## Debug 错误反馈
371
+
372
+ 非 401 HTTP 异常的响应体包含对象类型的 `debug`,并且以下任一开关开启时,错误会进入
373
+ Provider 内的 Debug Drawer,不再重复调用普通 `message.error`:
374
+
375
+ - 当前 URL Query 包含 `showDebug=1`;
376
+ - `localStorage.getItem('showDebug') === '1'`。
377
+
378
+ 该判断不依赖构建环境,并会安全处理无 `window`、无 `localStorage` 或存储访问被禁用的
379
+ 运行环境。Drawer 从右侧打开,在同一个列表中追加每一次错误且不去重;可逐条删除、全部
380
+ 清除或手动关闭。关闭 Drawer 不会清空列表,后续新错误会再次打开。每条记录展示错误
381
+ 摘要、请求 method/url,以及完整格式化的 `debug.request`、`debug.stackTrace` 和
382
+ `debug.mdc`;缺失字段显示为空占位。
383
+
384
+ `showErrorMessage: false` 会同时关闭普通错误消息和 Debug Drawer。HTTP 401 始终优先走
385
+ 原有 unauthorized 分支,不进入 Debug Drawer。未挂载 Provider 时会回退普通
386
+ `message.error`,避免吞掉错误反馈。
387
+
371
388
  ## 中间件
372
389
 
373
390
  中间件的 `onRequest`、`onResponse` 和 `onError` 都按注册顺序执行,不采用洋葱
@@ -10,6 +10,106 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
  var axios__default = /*#__PURE__*/_interopDefault(axios);
11
11
 
12
12
  // src/fetch/client.ts
13
+
14
+ // src/fetch/debug.ts
15
+ var EMPTY_DEBUG_DRAWER_SNAPSHOT = {
16
+ activeHostId: void 0,
17
+ errors: [],
18
+ open: false
19
+ };
20
+ var debugErrorSequence = 0;
21
+ var snapshot = EMPTY_DEBUG_DRAWER_SNAPSHOT;
22
+ var listeners = /* @__PURE__ */ new Set();
23
+ var drawerHosts = /* @__PURE__ */ new Set();
24
+ var emitChange = () => {
25
+ for (const listener of listeners) {
26
+ listener();
27
+ }
28
+ };
29
+ var setSnapshot = (nextSnapshot) => {
30
+ snapshot = nextSnapshot;
31
+ emitChange();
32
+ };
33
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
+ var getResponseDebugInfo = (responseData) => {
35
+ if (!isRecord(responseData) || !isRecord(responseData.debug)) {
36
+ return void 0;
37
+ }
38
+ return responseData.debug;
39
+ };
40
+ var isDebugEnabled = () => {
41
+ if (typeof window === "undefined") {
42
+ return false;
43
+ }
44
+ try {
45
+ if (new URLSearchParams(window.location.search).get("showDebug") === "1") {
46
+ return true;
47
+ }
48
+ } catch (e) {
49
+ }
50
+ try {
51
+ return window.localStorage.getItem("showDebug") === "1";
52
+ } catch (e) {
53
+ return false;
54
+ }
55
+ };
56
+ var subscribeDebugDrawer = (listener) => {
57
+ listeners.add(listener);
58
+ return () => listeners.delete(listener);
59
+ };
60
+ var getDebugDrawerSnapshot = () => snapshot;
61
+ var getDebugDrawerServerSnapshot = () => EMPTY_DEBUG_DRAWER_SNAPSHOT;
62
+ var registerDebugDrawerHost = (hostId) => {
63
+ drawerHosts.add(hostId);
64
+ if (snapshot.activeHostId === void 0) {
65
+ setSnapshot({ ...snapshot, activeHostId: hostId });
66
+ }
67
+ return () => {
68
+ drawerHosts.delete(hostId);
69
+ if (snapshot.activeHostId !== hostId) {
70
+ return;
71
+ }
72
+ const nextHost = drawerHosts.values().next();
73
+ setSnapshot({
74
+ ...snapshot,
75
+ activeHostId: nextHost.done ? void 0 : nextHost.value
76
+ });
77
+ };
78
+ };
79
+ var showFetchDebugError = (input) => {
80
+ if (snapshot.activeHostId === void 0) {
81
+ return false;
82
+ }
83
+ debugErrorSequence += 1;
84
+ setSnapshot({
85
+ ...snapshot,
86
+ errors: [
87
+ ...snapshot.errors,
88
+ {
89
+ ...input,
90
+ id: `${Date.now()}-${debugErrorSequence}`
91
+ }
92
+ ],
93
+ open: true
94
+ });
95
+ return true;
96
+ };
97
+ var closeDebugDrawer = () => {
98
+ if (snapshot.open) {
99
+ setSnapshot({ ...snapshot, open: false });
100
+ }
101
+ };
102
+ var removeDebugError = (id) => {
103
+ setSnapshot({
104
+ ...snapshot,
105
+ errors: snapshot.errors.filter((error) => error.id !== id)
106
+ });
107
+ };
108
+ var clearDebugErrors = () => {
109
+ if (snapshot.errors.length > 0) {
110
+ setSnapshot({ ...snapshot, errors: [] });
111
+ }
112
+ };
13
113
  var GzFetchError = class extends Error {
14
114
  constructor(message3, options) {
15
115
  super(message3);
@@ -125,17 +225,17 @@ var DEFAULT_UNAUTHORIZED_LOGIN_URL = "/login";
125
225
  var DEFAULT_UNAUTHORIZED_MODAL_TITLE = "登录失效";
126
226
  var DEFAULT_UNAUTHORIZED_MODAL_MESSAGE = "当前登录状态已失效,请重新登录。";
127
227
  var IDLE_SNAPSHOT = { phase: "idle" };
128
- var snapshot = IDLE_SNAPSHOT;
129
- var listeners = /* @__PURE__ */ new Set();
228
+ var snapshot2 = IDLE_SNAPSHOT;
229
+ var listeners2 = /* @__PURE__ */ new Set();
130
230
  var modalHosts = /* @__PURE__ */ new Set();
131
- var emitChange = () => {
132
- for (const listener of listeners) {
231
+ var emitChange2 = () => {
232
+ for (const listener of listeners2) {
133
233
  listener();
134
234
  }
135
235
  };
136
- var setSnapshot = (nextSnapshot) => {
137
- snapshot = nextSnapshot;
138
- emitChange();
236
+ var setSnapshot2 = (nextSnapshot) => {
237
+ snapshot2 = nextSnapshot;
238
+ emitChange2();
139
239
  };
140
240
  var resolveUnauthorizedOptions = (options) => {
141
241
  var _a, _b, _c, _d;
@@ -147,66 +247,66 @@ var resolveUnauthorizedOptions = (options) => {
147
247
  ...(options == null ? void 0 : options.onUnauthorized) === void 0 ? {} : { onUnauthorized: options.onUnauthorized }
148
248
  };
149
249
  };
150
- var getUnauthorizedSnapshot = () => snapshot;
250
+ var getUnauthorizedSnapshot = () => snapshot2;
151
251
  var getUnauthorizedServerSnapshot = () => IDLE_SNAPSHOT;
152
252
  var subscribeUnauthorized = (listener) => {
153
- listeners.add(listener);
154
- return () => listeners.delete(listener);
253
+ listeners2.add(listener);
254
+ return () => listeners2.delete(listener);
155
255
  };
156
256
  var registerUnauthorizedModalHost = (hostId) => {
157
257
  modalHosts.add(hostId);
158
- if (snapshot.activeHostId === void 0) {
159
- setSnapshot({ ...snapshot, activeHostId: hostId });
258
+ if (snapshot2.activeHostId === void 0) {
259
+ setSnapshot2({ ...snapshot2, activeHostId: hostId });
160
260
  }
161
261
  return () => {
162
262
  modalHosts.delete(hostId);
163
- if (snapshot.activeHostId !== hostId) {
263
+ if (snapshot2.activeHostId !== hostId) {
164
264
  return;
165
265
  }
166
266
  const nextHost = modalHosts.values().next();
167
267
  const nextHostId = nextHost.done ? void 0 : nextHost.value;
168
- if (snapshot.phase === "closing") {
169
- setSnapshot(
268
+ if (snapshot2.phase === "closing") {
269
+ setSnapshot2(
170
270
  nextHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: nextHostId, phase: "idle" }
171
271
  );
172
272
  return;
173
273
  }
174
274
  if (nextHostId === void 0) {
175
- setSnapshot(
176
- snapshot.options === void 0 ? { phase: snapshot.phase } : { options: snapshot.options, phase: snapshot.phase }
275
+ setSnapshot2(
276
+ snapshot2.options === void 0 ? { phase: snapshot2.phase } : { options: snapshot2.options, phase: snapshot2.phase }
177
277
  );
178
278
  return;
179
279
  }
180
- setSnapshot({ ...snapshot, activeHostId: nextHostId });
280
+ setSnapshot2({ ...snapshot2, activeHostId: nextHostId });
181
281
  };
182
282
  };
183
283
  var openUnauthorizedModal = (options) => {
184
- if (!options.enabled || snapshot.phase !== "idle") {
284
+ if (!options.enabled || snapshot2.phase !== "idle") {
185
285
  return false;
186
286
  }
187
- setSnapshot({
188
- ...snapshot.activeHostId === void 0 ? {} : { activeHostId: snapshot.activeHostId },
287
+ setSnapshot2({
288
+ ...snapshot2.activeHostId === void 0 ? {} : { activeHostId: snapshot2.activeHostId },
189
289
  options,
190
290
  phase: "open"
191
291
  });
192
292
  return true;
193
293
  };
194
294
  var beginCloseUnauthorizedModal = () => {
195
- if (snapshot.phase !== "open" || snapshot.options === void 0) {
295
+ if (snapshot2.phase !== "open" || snapshot2.options === void 0) {
196
296
  return void 0;
197
297
  }
198
- const { options } = snapshot;
199
- setSnapshot({ ...snapshot, phase: "closing" });
298
+ const { options } = snapshot2;
299
+ setSnapshot2({ ...snapshot2, phase: "closing" });
200
300
  return options;
201
301
  };
202
302
  var cancelUnauthorizedModal = () => beginCloseUnauthorizedModal() !== void 0;
203
303
  var confirmUnauthorizedModal = () => beginCloseUnauthorizedModal();
204
304
  var completeUnauthorizedModal = () => {
205
- if (snapshot.phase !== "closing") {
305
+ if (snapshot2.phase !== "closing") {
206
306
  return;
207
307
  }
208
- setSnapshot(
209
- snapshot.activeHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: snapshot.activeHostId, phase: "idle" }
308
+ setSnapshot2(
309
+ snapshot2.activeHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: snapshot2.activeHostId, phase: "idle" }
210
310
  );
211
311
  };
212
312
  var executeUnauthorizedAction = (options, navigate = (loginUrl) => window.location.assign(loginUrl)) => {
@@ -285,7 +385,16 @@ function createGzFetch(options = {}) {
285
385
  await runErrorMiddlewares(error, { config: currentConfig }, middlewares);
286
386
  const shouldShowError = (_a2 = currentConfig.showErrorMessage) != null ? _a2 : showInstanceErrorMessage;
287
387
  if (shouldShowError && error.type !== "CANCELED_ERROR" && !shouldHandleUnauthorized) {
288
- showFetchErrorMessage(error.message);
388
+ const debugInfo = error.type === "HTTP_ERROR" && error.status !== 401 && isDebugEnabled() ? getResponseDebugInfo(error.responseData) : void 0;
389
+ const didShowDebugError = debugInfo === void 0 ? false : showFetchDebugError({
390
+ debug: debugInfo,
391
+ message: error.message,
392
+ method: currentConfig.method,
393
+ url: currentConfig.url
394
+ });
395
+ if (!didShowDebugError) {
396
+ showFetchErrorMessage(error.message);
397
+ }
289
398
  }
290
399
  throw error;
291
400
  }
@@ -320,18 +429,133 @@ async function gzFetch(config) {
320
429
  }
321
430
  return defaultClient(config);
322
431
  }
432
+ var EMPTY_DEBUG_VALUE = "(无)";
433
+ var formatDebugValue = (value) => {
434
+ if (typeof value === "string") {
435
+ return value;
436
+ }
437
+ if (value === void 0 || value === null) {
438
+ return EMPTY_DEBUG_VALUE;
439
+ }
440
+ try {
441
+ const serialized = JSON.stringify(value, null, 2);
442
+ return serialized != null ? serialized : "(无法序列化)";
443
+ } catch (e) {
444
+ return "(无法序列化)";
445
+ }
446
+ };
447
+ var codeStyle = {
448
+ background: "rgba(127, 127, 127, 0.08)",
449
+ borderRadius: 6,
450
+ margin: "8px 0 0",
451
+ overflowWrap: "anywhere",
452
+ overflowX: "auto",
453
+ padding: 12,
454
+ whiteSpace: "pre-wrap"
455
+ };
456
+ var DebugSection = ({
457
+ title,
458
+ value
459
+ }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 16 }, children: [
460
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: title }),
461
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { style: codeStyle, children: formatDebugValue(value) })
462
+ ] });
463
+ var DebugError = ({ error }) => /* @__PURE__ */ jsxRuntime.jsxs(
464
+ "section",
465
+ {
466
+ style: {
467
+ border: "1px solid rgba(127, 127, 127, 0.24)",
468
+ borderRadius: 8,
469
+ padding: 16
470
+ },
471
+ children: [
472
+ /* @__PURE__ */ jsxRuntime.jsxs(
473
+ "div",
474
+ {
475
+ style: {
476
+ alignItems: "flex-start",
477
+ display: "flex",
478
+ gap: 12,
479
+ justifyContent: "space-between"
480
+ },
481
+ children: [
482
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: 0 }, children: [
483
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: error.message }),
484
+ error.method && error.url ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 6, overflowWrap: "anywhere" }, children: /* @__PURE__ */ jsxRuntime.jsxs("code", { children: [
485
+ error.method,
486
+ " ",
487
+ error.url
488
+ ] }) }) : null
489
+ ] }),
490
+ /* @__PURE__ */ jsxRuntime.jsx(
491
+ ggUi.Button,
492
+ {
493
+ danger: true,
494
+ size: "small",
495
+ type: "text",
496
+ onClick: () => removeDebugError(error.id),
497
+ children: "删除"
498
+ }
499
+ )
500
+ ]
501
+ }
502
+ ),
503
+ /* @__PURE__ */ jsxRuntime.jsx(DebugSection, { title: "Request", value: error.debug.request }),
504
+ /* @__PURE__ */ jsxRuntime.jsx(DebugSection, { title: "Stack Trace", value: error.debug.stackTrace }),
505
+ /* @__PURE__ */ jsxRuntime.jsx(DebugSection, { title: "MDC", value: error.debug.mdc })
506
+ ]
507
+ }
508
+ );
509
+ var GzFetchDebugDrawer = () => {
510
+ var _a;
511
+ const hostIdRef = react.useRef(void 0);
512
+ (_a = hostIdRef.current) != null ? _a : hostIdRef.current = /* @__PURE__ */ Symbol("gz-fetch-debug-drawer-host");
513
+ const hostId = hostIdRef.current;
514
+ const snapshot3 = react.useSyncExternalStore(
515
+ subscribeDebugDrawer,
516
+ getDebugDrawerSnapshot,
517
+ getDebugDrawerServerSnapshot
518
+ );
519
+ react.useEffect(() => registerDebugDrawerHost(hostId), [hostId]);
520
+ if (snapshot3.activeHostId !== hostId) {
521
+ return null;
522
+ }
523
+ return /* @__PURE__ */ jsxRuntime.jsx(
524
+ ggUi.Drawer,
525
+ {
526
+ extra: /* @__PURE__ */ jsxRuntime.jsx(
527
+ ggUi.Button,
528
+ {
529
+ disabled: snapshot3.errors.length === 0,
530
+ size: "small",
531
+ onClick: clearDebugErrors,
532
+ children: "全部清除"
533
+ }
534
+ ),
535
+ open: snapshot3.open,
536
+ placement: "right",
537
+ styles: { body: { overflowY: "auto" } },
538
+ title: `Debug Error (${snapshot3.errors.length})`,
539
+ width: "min(760px, 100vw)",
540
+ onClose: closeDebugDrawer,
541
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gap: 16 }, children: snapshot3.errors.map((error) => /* @__PURE__ */ jsxRuntime.jsx(DebugError, { error }, error.id)) })
542
+ }
543
+ );
544
+ };
545
+ GzFetchDebugDrawer.displayName = "GzFetchDebugDrawer";
546
+ var GzFetchDebugDrawer_default = GzFetchDebugDrawer;
323
547
  var GzFetchUnauthorizedModal = () => {
324
548
  var _a, _b, _c;
325
549
  const hostIdRef = react.useRef(void 0);
326
550
  (_a = hostIdRef.current) != null ? _a : hostIdRef.current = /* @__PURE__ */ Symbol("gz-fetch-unauthorized-modal-host");
327
551
  const hostId = hostIdRef.current;
328
- const snapshot2 = react.useSyncExternalStore(
552
+ const snapshot3 = react.useSyncExternalStore(
329
553
  subscribeUnauthorized,
330
554
  getUnauthorizedSnapshot,
331
555
  getUnauthorizedServerSnapshot
332
556
  );
333
557
  react.useEffect(() => registerUnauthorizedModalHost(hostId), [hostId]);
334
- if (snapshot2.activeHostId !== hostId) {
558
+ if (snapshot3.activeHostId !== hostId) {
335
559
  return null;
336
560
  }
337
561
  const handleConfirm = () => {
@@ -347,11 +571,11 @@ var GzFetchUnauthorizedModal = () => {
347
571
  cancelText: "取消",
348
572
  closable: true,
349
573
  okText: "重新登录",
350
- open: snapshot2.phase === "open",
351
- title: (_b = snapshot2.options) == null ? void 0 : _b.modalTitle,
574
+ open: snapshot3.phase === "open",
575
+ title: (_b = snapshot3.options) == null ? void 0 : _b.modalTitle,
352
576
  onCancel: cancelUnauthorizedModal,
353
577
  onOk: handleConfirm,
354
- children: (_c = snapshot2.options) == null ? void 0 : _c.modalMessage
578
+ children: (_c = snapshot3.options) == null ? void 0 : _c.modalMessage
355
579
  }
356
580
  );
357
581
  };
@@ -375,6 +599,7 @@ var GzFetchFeedbackProvider = ({ children }) => {
375
599
  );
376
600
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
377
601
  messageContextHolder,
602
+ /* @__PURE__ */ jsxRuntime.jsx(GzFetchDebugDrawer_default, {}),
378
603
  /* @__PURE__ */ jsxRuntime.jsx(GzFetchUnauthorizedModal_default, {}),
379
604
  children
380
605
  ] });
@@ -1,9 +1,109 @@
1
1
  import axios, { AxiosError, AxiosHeaders } from 'axios';
2
- import { Modal, message } from '@chenhui996/gg-ui';
2
+ import { Modal, message, Drawer, Button } from '@chenhui996/gg-ui';
3
3
  import { useRef, useSyncExternalStore, useEffect, useCallback } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/fetch/client.ts
7
+
8
+ // src/fetch/debug.ts
9
+ var EMPTY_DEBUG_DRAWER_SNAPSHOT = {
10
+ activeHostId: void 0,
11
+ errors: [],
12
+ open: false
13
+ };
14
+ var debugErrorSequence = 0;
15
+ var snapshot = EMPTY_DEBUG_DRAWER_SNAPSHOT;
16
+ var listeners = /* @__PURE__ */ new Set();
17
+ var drawerHosts = /* @__PURE__ */ new Set();
18
+ var emitChange = () => {
19
+ for (const listener of listeners) {
20
+ listener();
21
+ }
22
+ };
23
+ var setSnapshot = (nextSnapshot) => {
24
+ snapshot = nextSnapshot;
25
+ emitChange();
26
+ };
27
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
28
+ var getResponseDebugInfo = (responseData) => {
29
+ if (!isRecord(responseData) || !isRecord(responseData.debug)) {
30
+ return void 0;
31
+ }
32
+ return responseData.debug;
33
+ };
34
+ var isDebugEnabled = () => {
35
+ if (typeof window === "undefined") {
36
+ return false;
37
+ }
38
+ try {
39
+ if (new URLSearchParams(window.location.search).get("showDebug") === "1") {
40
+ return true;
41
+ }
42
+ } catch (e) {
43
+ }
44
+ try {
45
+ return window.localStorage.getItem("showDebug") === "1";
46
+ } catch (e) {
47
+ return false;
48
+ }
49
+ };
50
+ var subscribeDebugDrawer = (listener) => {
51
+ listeners.add(listener);
52
+ return () => listeners.delete(listener);
53
+ };
54
+ var getDebugDrawerSnapshot = () => snapshot;
55
+ var getDebugDrawerServerSnapshot = () => EMPTY_DEBUG_DRAWER_SNAPSHOT;
56
+ var registerDebugDrawerHost = (hostId) => {
57
+ drawerHosts.add(hostId);
58
+ if (snapshot.activeHostId === void 0) {
59
+ setSnapshot({ ...snapshot, activeHostId: hostId });
60
+ }
61
+ return () => {
62
+ drawerHosts.delete(hostId);
63
+ if (snapshot.activeHostId !== hostId) {
64
+ return;
65
+ }
66
+ const nextHost = drawerHosts.values().next();
67
+ setSnapshot({
68
+ ...snapshot,
69
+ activeHostId: nextHost.done ? void 0 : nextHost.value
70
+ });
71
+ };
72
+ };
73
+ var showFetchDebugError = (input) => {
74
+ if (snapshot.activeHostId === void 0) {
75
+ return false;
76
+ }
77
+ debugErrorSequence += 1;
78
+ setSnapshot({
79
+ ...snapshot,
80
+ errors: [
81
+ ...snapshot.errors,
82
+ {
83
+ ...input,
84
+ id: `${Date.now()}-${debugErrorSequence}`
85
+ }
86
+ ],
87
+ open: true
88
+ });
89
+ return true;
90
+ };
91
+ var closeDebugDrawer = () => {
92
+ if (snapshot.open) {
93
+ setSnapshot({ ...snapshot, open: false });
94
+ }
95
+ };
96
+ var removeDebugError = (id) => {
97
+ setSnapshot({
98
+ ...snapshot,
99
+ errors: snapshot.errors.filter((error) => error.id !== id)
100
+ });
101
+ };
102
+ var clearDebugErrors = () => {
103
+ if (snapshot.errors.length > 0) {
104
+ setSnapshot({ ...snapshot, errors: [] });
105
+ }
106
+ };
7
107
  var GzFetchError = class extends Error {
8
108
  constructor(message3, options) {
9
109
  super(message3);
@@ -119,17 +219,17 @@ var DEFAULT_UNAUTHORIZED_LOGIN_URL = "/login";
119
219
  var DEFAULT_UNAUTHORIZED_MODAL_TITLE = "登录失效";
120
220
  var DEFAULT_UNAUTHORIZED_MODAL_MESSAGE = "当前登录状态已失效,请重新登录。";
121
221
  var IDLE_SNAPSHOT = { phase: "idle" };
122
- var snapshot = IDLE_SNAPSHOT;
123
- var listeners = /* @__PURE__ */ new Set();
222
+ var snapshot2 = IDLE_SNAPSHOT;
223
+ var listeners2 = /* @__PURE__ */ new Set();
124
224
  var modalHosts = /* @__PURE__ */ new Set();
125
- var emitChange = () => {
126
- for (const listener of listeners) {
225
+ var emitChange2 = () => {
226
+ for (const listener of listeners2) {
127
227
  listener();
128
228
  }
129
229
  };
130
- var setSnapshot = (nextSnapshot) => {
131
- snapshot = nextSnapshot;
132
- emitChange();
230
+ var setSnapshot2 = (nextSnapshot) => {
231
+ snapshot2 = nextSnapshot;
232
+ emitChange2();
133
233
  };
134
234
  var resolveUnauthorizedOptions = (options) => {
135
235
  var _a, _b, _c, _d;
@@ -141,66 +241,66 @@ var resolveUnauthorizedOptions = (options) => {
141
241
  ...(options == null ? void 0 : options.onUnauthorized) === void 0 ? {} : { onUnauthorized: options.onUnauthorized }
142
242
  };
143
243
  };
144
- var getUnauthorizedSnapshot = () => snapshot;
244
+ var getUnauthorizedSnapshot = () => snapshot2;
145
245
  var getUnauthorizedServerSnapshot = () => IDLE_SNAPSHOT;
146
246
  var subscribeUnauthorized = (listener) => {
147
- listeners.add(listener);
148
- return () => listeners.delete(listener);
247
+ listeners2.add(listener);
248
+ return () => listeners2.delete(listener);
149
249
  };
150
250
  var registerUnauthorizedModalHost = (hostId) => {
151
251
  modalHosts.add(hostId);
152
- if (snapshot.activeHostId === void 0) {
153
- setSnapshot({ ...snapshot, activeHostId: hostId });
252
+ if (snapshot2.activeHostId === void 0) {
253
+ setSnapshot2({ ...snapshot2, activeHostId: hostId });
154
254
  }
155
255
  return () => {
156
256
  modalHosts.delete(hostId);
157
- if (snapshot.activeHostId !== hostId) {
257
+ if (snapshot2.activeHostId !== hostId) {
158
258
  return;
159
259
  }
160
260
  const nextHost = modalHosts.values().next();
161
261
  const nextHostId = nextHost.done ? void 0 : nextHost.value;
162
- if (snapshot.phase === "closing") {
163
- setSnapshot(
262
+ if (snapshot2.phase === "closing") {
263
+ setSnapshot2(
164
264
  nextHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: nextHostId, phase: "idle" }
165
265
  );
166
266
  return;
167
267
  }
168
268
  if (nextHostId === void 0) {
169
- setSnapshot(
170
- snapshot.options === void 0 ? { phase: snapshot.phase } : { options: snapshot.options, phase: snapshot.phase }
269
+ setSnapshot2(
270
+ snapshot2.options === void 0 ? { phase: snapshot2.phase } : { options: snapshot2.options, phase: snapshot2.phase }
171
271
  );
172
272
  return;
173
273
  }
174
- setSnapshot({ ...snapshot, activeHostId: nextHostId });
274
+ setSnapshot2({ ...snapshot2, activeHostId: nextHostId });
175
275
  };
176
276
  };
177
277
  var openUnauthorizedModal = (options) => {
178
- if (!options.enabled || snapshot.phase !== "idle") {
278
+ if (!options.enabled || snapshot2.phase !== "idle") {
179
279
  return false;
180
280
  }
181
- setSnapshot({
182
- ...snapshot.activeHostId === void 0 ? {} : { activeHostId: snapshot.activeHostId },
281
+ setSnapshot2({
282
+ ...snapshot2.activeHostId === void 0 ? {} : { activeHostId: snapshot2.activeHostId },
183
283
  options,
184
284
  phase: "open"
185
285
  });
186
286
  return true;
187
287
  };
188
288
  var beginCloseUnauthorizedModal = () => {
189
- if (snapshot.phase !== "open" || snapshot.options === void 0) {
289
+ if (snapshot2.phase !== "open" || snapshot2.options === void 0) {
190
290
  return void 0;
191
291
  }
192
- const { options } = snapshot;
193
- setSnapshot({ ...snapshot, phase: "closing" });
292
+ const { options } = snapshot2;
293
+ setSnapshot2({ ...snapshot2, phase: "closing" });
194
294
  return options;
195
295
  };
196
296
  var cancelUnauthorizedModal = () => beginCloseUnauthorizedModal() !== void 0;
197
297
  var confirmUnauthorizedModal = () => beginCloseUnauthorizedModal();
198
298
  var completeUnauthorizedModal = () => {
199
- if (snapshot.phase !== "closing") {
299
+ if (snapshot2.phase !== "closing") {
200
300
  return;
201
301
  }
202
- setSnapshot(
203
- snapshot.activeHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: snapshot.activeHostId, phase: "idle" }
302
+ setSnapshot2(
303
+ snapshot2.activeHostId === void 0 ? IDLE_SNAPSHOT : { activeHostId: snapshot2.activeHostId, phase: "idle" }
204
304
  );
205
305
  };
206
306
  var executeUnauthorizedAction = (options, navigate = (loginUrl) => window.location.assign(loginUrl)) => {
@@ -279,7 +379,16 @@ function createGzFetch(options = {}) {
279
379
  await runErrorMiddlewares(error, { config: currentConfig }, middlewares);
280
380
  const shouldShowError = (_a2 = currentConfig.showErrorMessage) != null ? _a2 : showInstanceErrorMessage;
281
381
  if (shouldShowError && error.type !== "CANCELED_ERROR" && !shouldHandleUnauthorized) {
282
- showFetchErrorMessage(error.message);
382
+ const debugInfo = error.type === "HTTP_ERROR" && error.status !== 401 && isDebugEnabled() ? getResponseDebugInfo(error.responseData) : void 0;
383
+ const didShowDebugError = debugInfo === void 0 ? false : showFetchDebugError({
384
+ debug: debugInfo,
385
+ message: error.message,
386
+ method: currentConfig.method,
387
+ url: currentConfig.url
388
+ });
389
+ if (!didShowDebugError) {
390
+ showFetchErrorMessage(error.message);
391
+ }
283
392
  }
284
393
  throw error;
285
394
  }
@@ -314,18 +423,133 @@ async function gzFetch(config) {
314
423
  }
315
424
  return defaultClient(config);
316
425
  }
426
+ var EMPTY_DEBUG_VALUE = "(无)";
427
+ var formatDebugValue = (value) => {
428
+ if (typeof value === "string") {
429
+ return value;
430
+ }
431
+ if (value === void 0 || value === null) {
432
+ return EMPTY_DEBUG_VALUE;
433
+ }
434
+ try {
435
+ const serialized = JSON.stringify(value, null, 2);
436
+ return serialized != null ? serialized : "(无法序列化)";
437
+ } catch (e) {
438
+ return "(无法序列化)";
439
+ }
440
+ };
441
+ var codeStyle = {
442
+ background: "rgba(127, 127, 127, 0.08)",
443
+ borderRadius: 6,
444
+ margin: "8px 0 0",
445
+ overflowWrap: "anywhere",
446
+ overflowX: "auto",
447
+ padding: 12,
448
+ whiteSpace: "pre-wrap"
449
+ };
450
+ var DebugSection = ({
451
+ title,
452
+ value
453
+ }) => /* @__PURE__ */ jsxs("div", { style: { marginTop: 16 }, children: [
454
+ /* @__PURE__ */ jsx("strong", { children: title }),
455
+ /* @__PURE__ */ jsx("pre", { style: codeStyle, children: formatDebugValue(value) })
456
+ ] });
457
+ var DebugError = ({ error }) => /* @__PURE__ */ jsxs(
458
+ "section",
459
+ {
460
+ style: {
461
+ border: "1px solid rgba(127, 127, 127, 0.24)",
462
+ borderRadius: 8,
463
+ padding: 16
464
+ },
465
+ children: [
466
+ /* @__PURE__ */ jsxs(
467
+ "div",
468
+ {
469
+ style: {
470
+ alignItems: "flex-start",
471
+ display: "flex",
472
+ gap: 12,
473
+ justifyContent: "space-between"
474
+ },
475
+ children: [
476
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
477
+ /* @__PURE__ */ jsx("strong", { children: error.message }),
478
+ error.method && error.url ? /* @__PURE__ */ jsx("div", { style: { marginTop: 6, overflowWrap: "anywhere" }, children: /* @__PURE__ */ jsxs("code", { children: [
479
+ error.method,
480
+ " ",
481
+ error.url
482
+ ] }) }) : null
483
+ ] }),
484
+ /* @__PURE__ */ jsx(
485
+ Button,
486
+ {
487
+ danger: true,
488
+ size: "small",
489
+ type: "text",
490
+ onClick: () => removeDebugError(error.id),
491
+ children: "删除"
492
+ }
493
+ )
494
+ ]
495
+ }
496
+ ),
497
+ /* @__PURE__ */ jsx(DebugSection, { title: "Request", value: error.debug.request }),
498
+ /* @__PURE__ */ jsx(DebugSection, { title: "Stack Trace", value: error.debug.stackTrace }),
499
+ /* @__PURE__ */ jsx(DebugSection, { title: "MDC", value: error.debug.mdc })
500
+ ]
501
+ }
502
+ );
503
+ var GzFetchDebugDrawer = () => {
504
+ var _a;
505
+ const hostIdRef = useRef(void 0);
506
+ (_a = hostIdRef.current) != null ? _a : hostIdRef.current = /* @__PURE__ */ Symbol("gz-fetch-debug-drawer-host");
507
+ const hostId = hostIdRef.current;
508
+ const snapshot3 = useSyncExternalStore(
509
+ subscribeDebugDrawer,
510
+ getDebugDrawerSnapshot,
511
+ getDebugDrawerServerSnapshot
512
+ );
513
+ useEffect(() => registerDebugDrawerHost(hostId), [hostId]);
514
+ if (snapshot3.activeHostId !== hostId) {
515
+ return null;
516
+ }
517
+ return /* @__PURE__ */ jsx(
518
+ Drawer,
519
+ {
520
+ extra: /* @__PURE__ */ jsx(
521
+ Button,
522
+ {
523
+ disabled: snapshot3.errors.length === 0,
524
+ size: "small",
525
+ onClick: clearDebugErrors,
526
+ children: "全部清除"
527
+ }
528
+ ),
529
+ open: snapshot3.open,
530
+ placement: "right",
531
+ styles: { body: { overflowY: "auto" } },
532
+ title: `Debug Error (${snapshot3.errors.length})`,
533
+ width: "min(760px, 100vw)",
534
+ onClose: closeDebugDrawer,
535
+ children: /* @__PURE__ */ jsx("div", { style: { display: "grid", gap: 16 }, children: snapshot3.errors.map((error) => /* @__PURE__ */ jsx(DebugError, { error }, error.id)) })
536
+ }
537
+ );
538
+ };
539
+ GzFetchDebugDrawer.displayName = "GzFetchDebugDrawer";
540
+ var GzFetchDebugDrawer_default = GzFetchDebugDrawer;
317
541
  var GzFetchUnauthorizedModal = () => {
318
542
  var _a, _b, _c;
319
543
  const hostIdRef = useRef(void 0);
320
544
  (_a = hostIdRef.current) != null ? _a : hostIdRef.current = /* @__PURE__ */ Symbol("gz-fetch-unauthorized-modal-host");
321
545
  const hostId = hostIdRef.current;
322
- const snapshot2 = useSyncExternalStore(
546
+ const snapshot3 = useSyncExternalStore(
323
547
  subscribeUnauthorized,
324
548
  getUnauthorizedSnapshot,
325
549
  getUnauthorizedServerSnapshot
326
550
  );
327
551
  useEffect(() => registerUnauthorizedModalHost(hostId), [hostId]);
328
- if (snapshot2.activeHostId !== hostId) {
552
+ if (snapshot3.activeHostId !== hostId) {
329
553
  return null;
330
554
  }
331
555
  const handleConfirm = () => {
@@ -341,11 +565,11 @@ var GzFetchUnauthorizedModal = () => {
341
565
  cancelText: "取消",
342
566
  closable: true,
343
567
  okText: "重新登录",
344
- open: snapshot2.phase === "open",
345
- title: (_b = snapshot2.options) == null ? void 0 : _b.modalTitle,
568
+ open: snapshot3.phase === "open",
569
+ title: (_b = snapshot3.options) == null ? void 0 : _b.modalTitle,
346
570
  onCancel: cancelUnauthorizedModal,
347
571
  onOk: handleConfirm,
348
- children: (_c = snapshot2.options) == null ? void 0 : _c.modalMessage
572
+ children: (_c = snapshot3.options) == null ? void 0 : _c.modalMessage
349
573
  }
350
574
  );
351
575
  };
@@ -369,6 +593,7 @@ var GzFetchFeedbackProvider = ({ children }) => {
369
593
  );
370
594
  return /* @__PURE__ */ jsxs(Fragment, { children: [
371
595
  messageContextHolder,
596
+ /* @__PURE__ */ jsx(GzFetchDebugDrawer_default, {}),
372
597
  /* @__PURE__ */ jsx(GzFetchUnauthorizedModal_default, {}),
373
598
  children
374
599
  ] });
package/package.json CHANGED
@@ -1,99 +1,99 @@
1
- {
2
- "name": "@gz-fronted/gz-pc",
3
- "version": "0.1.2",
4
- "description": "Shared fetch, hooks and utility primitives for GZ frontend applications.",
5
- "license": "MIT",
6
- "type": "module",
7
- "sideEffects": false,
8
- "files": [
9
- "dist",
10
- "README.md"
11
- ],
12
- "exports": {
13
- "./fetch": {
14
- "import": {
15
- "types": "./dist/fetch/index.d.ts",
16
- "default": "./dist/fetch/index.js"
17
- },
18
- "require": {
19
- "types": "./dist/fetch/index.d.cts",
20
- "default": "./dist/fetch/index.cjs"
21
- }
22
- },
23
- "./hooks": {
24
- "import": {
25
- "types": "./dist/hooks/index.d.ts",
26
- "default": "./dist/hooks/index.js"
27
- },
28
- "require": {
29
- "types": "./dist/hooks/index.d.cts",
30
- "default": "./dist/hooks/index.cjs"
31
- }
32
- },
33
- "./utils": {
34
- "import": {
35
- "types": "./dist/utils/index.d.ts",
36
- "default": "./dist/utils/index.js"
37
- },
38
- "require": {
39
- "types": "./dist/utils/index.d.cts",
40
- "default": "./dist/utils/index.cjs"
41
- }
42
- },
43
- "./package.json": "./package.json"
44
- },
45
- "scripts": {
46
- "build": "node scripts/build.mjs",
47
- "lint": "eslint .",
48
- "typecheck": "tsc --noEmit",
49
- "test": "vitest run",
50
- "test:watch": "vitest",
51
- "verify:exports": "node tests/exports/smoke.mjs",
52
- "prepublishOnly": "npm run build",
53
- "prepare": "husky"
54
- },
55
- "dependencies": {
56
- "ahooks": "^3.9.7",
57
- "axios": "^1.13.6"
58
- },
59
- "peerDependencies": {
60
- "@chenhui996/gg-ui": "^0.1.1",
61
- "react": "^18.0.0 || ^19.0.0"
62
- },
63
- "devDependencies": {
64
- "@chenhui996/gg-ui": "0.1.1",
65
- "@commitlint/cli": "21.2.1",
66
- "@commitlint/config-conventional": "21.2.0",
67
- "@eslint/js": "9.39.4",
68
- "@types/node": "24.10.0",
69
- "@types/react": "19.2.14",
70
- "@types/react-dom": "19.2.3",
71
- "eslint": "9.39.4",
72
- "globals": "16.5.0",
73
- "husky": "9.1.7",
74
- "lint-staged": "17.2.0",
75
- "msw": "2.12.7",
76
- "prettier": "3.9.6",
77
- "react": "19.2.4",
78
- "react-dom": "19.2.4",
79
- "tsup": "8.5.1",
80
- "typescript": "5.9.3",
81
- "typescript-eslint": "8.56.1",
82
- "vitest": "4.1.4"
83
- },
84
- "engines": {
85
- "node": ">=20"
86
- },
87
- "publishConfig": {
88
- "access": "public"
89
- },
90
- "lint-staged": {
91
- "*.{js,jsx,ts,tsx}": [
92
- "eslint --fix",
93
- "prettier --write"
94
- ],
95
- "*.{json,md,yml,yaml,css,less}": [
96
- "prettier --write"
97
- ]
98
- }
99
- }
1
+ {
2
+ "name": "@gz-fronted/gz-pc",
3
+ "version": "0.1.3-beta.0",
4
+ "description": "Shared fetch, hooks and utility primitives for GZ frontend applications.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "exports": {
13
+ "./fetch": {
14
+ "import": {
15
+ "types": "./dist/fetch/index.d.ts",
16
+ "default": "./dist/fetch/index.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/fetch/index.d.cts",
20
+ "default": "./dist/fetch/index.cjs"
21
+ }
22
+ },
23
+ "./hooks": {
24
+ "import": {
25
+ "types": "./dist/hooks/index.d.ts",
26
+ "default": "./dist/hooks/index.js"
27
+ },
28
+ "require": {
29
+ "types": "./dist/hooks/index.d.cts",
30
+ "default": "./dist/hooks/index.cjs"
31
+ }
32
+ },
33
+ "./utils": {
34
+ "import": {
35
+ "types": "./dist/utils/index.d.ts",
36
+ "default": "./dist/utils/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/utils/index.d.cts",
40
+ "default": "./dist/utils/index.cjs"
41
+ }
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "scripts": {
46
+ "build": "node scripts/build.mjs",
47
+ "lint": "eslint .",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "verify:exports": "node tests/exports/smoke.mjs",
52
+ "prepublishOnly": "npm run build",
53
+ "prepare": "husky"
54
+ },
55
+ "dependencies": {
56
+ "ahooks": "^3.9.7",
57
+ "axios": "^1.13.6"
58
+ },
59
+ "peerDependencies": {
60
+ "@chenhui996/gg-ui": "^0.1.1",
61
+ "react": "^18.0.0 || ^19.0.0"
62
+ },
63
+ "devDependencies": {
64
+ "@chenhui996/gg-ui": "0.1.1",
65
+ "@commitlint/cli": "21.2.1",
66
+ "@commitlint/config-conventional": "21.2.0",
67
+ "@eslint/js": "9.39.4",
68
+ "@types/node": "24.10.0",
69
+ "@types/react": "19.2.14",
70
+ "@types/react-dom": "19.2.3",
71
+ "eslint": "9.39.4",
72
+ "globals": "16.5.0",
73
+ "husky": "9.1.7",
74
+ "lint-staged": "17.2.0",
75
+ "msw": "2.12.7",
76
+ "prettier": "3.9.6",
77
+ "react": "19.2.4",
78
+ "react-dom": "19.2.4",
79
+ "tsup": "8.5.1",
80
+ "typescript": "5.9.3",
81
+ "typescript-eslint": "8.56.1",
82
+ "vitest": "4.1.4"
83
+ },
84
+ "engines": {
85
+ "node": ">=20"
86
+ },
87
+ "publishConfig": {
88
+ "access": "public"
89
+ },
90
+ "lint-staged": {
91
+ "*.{js,jsx,ts,tsx}": [
92
+ "eslint --fix",
93
+ "prettier --write"
94
+ ],
95
+ "*.{json,md,yml,yaml,css,less}": [
96
+ "prettier --write"
97
+ ]
98
+ }
99
+ }