@kine-design/crud 0.0.1-beta.16 → 0.0.1-beta.18

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.
@@ -0,0 +1 @@
1
+ 594292cc-8321-4629-97d9-91ad432e0646
@@ -134,11 +134,24 @@ function buildAuthReturn(
134
134
  clearStorage();
135
135
  }
136
136
 
137
+ let restoring: Promise<void> | null = null;
138
+
137
139
  async function restore(): Promise<void> {
138
140
  if (!adapter) return;
141
+ // 防止并发调用(如 mount + tab 激活同时触发)
142
+ if (restoring) return restoring;
139
143
 
140
- const storedToken = adapter.get<string>(STORAGE_KEY_TOKEN);
141
- const storedUser = adapter.get<AuthState['user']>(STORAGE_KEY_USER);
144
+ restoring = doRestore();
145
+ try {
146
+ await restoring;
147
+ } finally {
148
+ restoring = null;
149
+ }
150
+ }
151
+
152
+ async function doRestore(): Promise<void> {
153
+ const storedToken = adapter!.get<string>(STORAGE_KEY_TOKEN);
154
+ const storedUser = adapter!.get<AuthState['user']>(STORAGE_KEY_USER);
142
155
 
143
156
  // storage 中没有有效数据,跳过
144
157
  if (!storedToken || !storedUser) return;
@@ -213,6 +226,13 @@ export function createAuth(app: App, options: AuthOptions): AuthReturn {
213
226
  }
214
227
  };
215
228
  window.addEventListener('storage', onStorageChange);
229
+
230
+ // app.unmount 时清理监听器,防止泄漏
231
+ const origUnmount = app.unmount.bind(app);
232
+ app.unmount = () => {
233
+ window.removeEventListener('storage', onStorageChange);
234
+ origUnmount();
235
+ };
216
236
  }
217
237
 
218
238
  // 将 auth 实例注入 Vue 组件树,同时挂载 onUnauthorized 回调供 authGuard 使用
@@ -58,6 +58,7 @@ export function createRequest(options: RequestOptions = {}): RequestClient {
58
58
  getToken: options.getToken,
59
59
  onUnauthorized: options.onUnauthorized,
60
60
  responseInterceptor: options.responseInterceptor,
61
+ feedback: options.feedback,
61
62
  registerAbort: (controller: AbortController) => {
62
63
  abortControllers.add(controller);
63
64
  // 完成后自动移除
@@ -11,10 +11,12 @@ import type {
11
11
  CachePolicy,
12
12
  ControlPolicy,
13
13
  Lifecycle,
14
+ NetworkError,
14
15
  RequestConfig,
15
16
  RequestMethod,
16
17
  RetryPolicy,
17
18
  Transport,
19
+ TransportResponse,
18
20
  WrappedResponse,
19
21
  } from './types';
20
22
  import { BusinessError, NetworkRequestError } from './types';
@@ -194,6 +196,8 @@ export interface RequestBuilderContext {
194
196
  getToken?: () => string | null | undefined;
195
197
  onUnauthorized?: () => void;
196
198
  responseInterceptor?: <T>(data: T) => T;
199
+ /** 用户反馈处理器(写操作成功后自动 showSuccess) */
200
+ feedback?: import('./types').UserFeedbackHandler;
197
201
  /** 注册 abort controller,供生命周期管理 */
198
202
  registerAbort?: (controller: AbortController) => void;
199
203
  }
@@ -358,12 +362,24 @@ export class RequestBuilder {
358
362
  // 响应拦截器
359
363
  const finalResult = responseInterceptor ? responseInterceptor(result) : result;
360
364
 
365
+ // 写操作成功后自动反馈
366
+ if (this.context.feedback && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(this.method)) {
367
+ this.context.feedback.showSuccess('操作成功');
368
+ }
369
+
361
370
  return finalResult as T;
362
371
  } catch (error) {
363
372
  // 401 检测
364
373
  if (error instanceof NetworkRequestError && error.detail.type === 'httpError' && error.detail.status === 401) {
365
374
  onUnauthorized?.();
366
375
  }
376
+ // 写操作失败自动反馈错误信息
377
+ if (this.context.feedback) {
378
+ const msg = error instanceof BusinessError ? error.message
379
+ : error instanceof NetworkRequestError ? `请求失败: ${error.message}`
380
+ : '操作失败';
381
+ this.context.feedback.showError(msg);
382
+ }
367
383
  throw error;
368
384
  } finally {
369
385
  if (totalTimeoutId !== undefined) clearTimeout(totalTimeoutId);
@@ -379,14 +395,23 @@ export class RequestBuilder {
379
395
  body: BodyInit | null,
380
396
  signal: AbortSignal,
381
397
  ): Promise<T> {
382
- const response = await transport.send({
383
- url,
384
- method: this.method,
385
- headers,
386
- body,
387
- signal,
388
- timeout: this.config.timeout,
389
- });
398
+ let response: TransportResponse;
399
+ try {
400
+ response = await transport.send({
401
+ url,
402
+ method: this.method,
403
+ headers,
404
+ body,
405
+ signal,
406
+ timeout: this.config.timeout,
407
+ });
408
+ } catch (error) {
409
+ // transport 层抛出的是 NetworkError 裸对象,统一包装为 NetworkRequestError
410
+ if (error && typeof error === 'object' && 'type' in error) {
411
+ throw new NetworkRequestError(error as NetworkError);
412
+ }
413
+ throw new NetworkRequestError({ type: 'unknown', error });
414
+ }
390
415
 
391
416
  return unwrapResponse<T>(response);
392
417
  }
@@ -72,7 +72,10 @@ export class XhrTransport implements Transport {
72
72
  headers,
73
73
  body: null,
74
74
  text: () => Promise.resolve(responseText),
75
- json: <T>() => Promise.resolve(JSON.parse(responseText) as T),
75
+ json: <T>() => {
76
+ try { return Promise.resolve(JSON.parse(responseText) as T); }
77
+ catch (e) { return Promise.reject(e); }
78
+ },
76
79
  blob: () => Promise.resolve(new Blob([responseText])),
77
80
  };
78
81
 
@@ -8,6 +8,8 @@ export interface RequestBuilderContext {
8
8
  getToken?: () => string | null | undefined;
9
9
  onUnauthorized?: () => void;
10
10
  responseInterceptor?: <T>(data: T) => T;
11
+ /** 用户反馈处理器(写操作成功后自动 showSuccess) */
12
+ feedback?: import('./types').UserFeedbackHandler;
11
13
  /** 注册 abort controller,供生命周期管理 */
12
14
  registerAbort?: (controller: AbortController) => void;
13
15
  }
package/dist/crud.js CHANGED
@@ -214,8 +214,8 @@ function useTable() {
214
214
  });
215
215
  /** 从 data[i] 中安全取值 */
216
216
  const getData = (i, param) => {
217
- if (data[i] && data[i][param]) return data[i][param];
218
- return "";
217
+ if (data[i] == null) return "";
218
+ return data[i][param] ?? "";
219
219
  };
220
220
  /** 向每行的 td 列表中追加一列 */
221
221
  const pushTd = (param, bodySlot, style) => {
@@ -7450,6 +7450,16 @@ var KInput_default = /* @__PURE__ */ defineComponent((_props, ctx) => {
7450
7450
  ]
7451
7451
  });
7452
7452
  //#endregion
7453
+ //#region ../ui/constants.ts
7454
+ /** 浮层组件的默认 z-index(Select / AutoComplete / DatePicker / TimePicker / Cascader) */
7455
+ var Z_INDEX_DROPDOWN = "1000";
7456
+ /** 浮层隐藏时使用的占位样式(保持 DOM 挂载以便 floating-ui 计算宽度) */
7457
+ var DROPDOWN_HIDDEN_STYLE = {
7458
+ position: "fixed",
7459
+ visibility: "hidden",
7460
+ zIndex: Z_INDEX_DROPDOWN
7461
+ };
7462
+ //#endregion
7453
7463
  //#region ../ui/components/select/KSelect.tsx
7454
7464
  /**
7455
7465
  * @description kine-ui select 组件
@@ -7469,12 +7479,7 @@ var KSelect_default = /* @__PURE__ */ defineComponent((_props, _ctx) => {
7469
7479
  const { inputValue, isOpen, fetchLoading, inputReadonly, displayOptions, selectedTags, lastOptionRef, optionsContainerRef, onSelect, onDeleteTag, onInput, onFocus, onBlur, tools, updateFetchObserver } = useSelect(props, _ctx);
7470
7480
  const triggerRef = ref(null);
7471
7481
  const dropdownRef = ref(null);
7472
- const HIDDEN_STYLE = {
7473
- position: "fixed",
7474
- visibility: "hidden",
7475
- zIndex: "1000"
7476
- };
7477
- const dropdownStyle = ref(HIDDEN_STYLE);
7482
+ const dropdownStyle = ref(DROPDOWN_HIDDEN_STYLE);
7478
7483
  const cleanupAutoUpdate = ref(null);
7479
7484
  const updateDropdownPosition = async () => {
7480
7485
  if (!triggerRef.value || !dropdownRef.value) return;
@@ -7494,7 +7499,7 @@ var KSelect_default = /* @__PURE__ */ defineComponent((_props, _ctx) => {
7494
7499
  position: "fixed",
7495
7500
  left: `${x}px`,
7496
7501
  top: `${y}px`,
7497
- zIndex: "1000",
7502
+ zIndex: Z_INDEX_DROPDOWN,
7498
7503
  visibility: "visible"
7499
7504
  };
7500
7505
  };
@@ -7511,7 +7516,7 @@ var KSelect_default = /* @__PURE__ */ defineComponent((_props, _ctx) => {
7511
7516
  cleanupAutoUpdate.value?.();
7512
7517
  cleanupAutoUpdate.value = null;
7513
7518
  isOpen.value = false;
7514
- dropdownStyle.value = HIDDEN_STYLE;
7519
+ dropdownStyle.value = DROPDOWN_HIDDEN_STYLE;
7515
7520
  };
7516
7521
  onBeforeUnmount(() => {
7517
7522
  cleanupAutoUpdate.value?.();
@@ -7603,6 +7608,7 @@ var KSelect_default = /* @__PURE__ */ defineComponent((_props, _ctx) => {
7603
7608
  props.loading && createVNode("span", { "class": "k-select-loading-icon" }, null),
7604
7609
  showClear && !props.loading && createVNode("span", {
7605
7610
  "class": "k-select-clear",
7611
+ "aria-label": "清空",
7606
7612
  "onMousedown": (e) => {
7607
7613
  e.preventDefault();
7608
7614
  e.stopPropagation();
@@ -7813,8 +7819,18 @@ function buildAuthReturn(state, options, adapter) {
7813
7819
  state.token = null;
7814
7820
  clearStorage();
7815
7821
  }
7822
+ let restoring = null;
7816
7823
  async function restore() {
7817
7824
  if (!adapter) return;
7825
+ if (restoring) return restoring;
7826
+ restoring = doRestore();
7827
+ try {
7828
+ await restoring;
7829
+ } finally {
7830
+ restoring = null;
7831
+ }
7832
+ }
7833
+ async function doRestore() {
7818
7834
  const storedToken = adapter.get(STORAGE_KEY_TOKEN);
7819
7835
  const storedUser = adapter.get(STORAGE_KEY_USER);
7820
7836
  if (!storedToken || !storedUser) return;
@@ -7881,6 +7897,11 @@ function createAuth(app, options) {
7881
7897
  }
7882
7898
  };
7883
7899
  window.addEventListener("storage", onStorageChange);
7900
+ const origUnmount = app.unmount.bind(app);
7901
+ app.unmount = () => {
7902
+ window.removeEventListener("storage", onStorageChange);
7903
+ origUnmount();
7904
+ };
7884
7905
  }
7885
7906
  app.provide(AUTH_INJECT_KEY, {
7886
7907
  auth,
@@ -8368,23 +8389,44 @@ var RequestBuilder = class {
8368
8389
  data: result,
8369
8390
  timestamp: Date.now()
8370
8391
  });
8371
- return responseInterceptor ? responseInterceptor(result) : result;
8392
+ const finalResult = responseInterceptor ? responseInterceptor(result) : result;
8393
+ if (this.context.feedback && [
8394
+ "POST",
8395
+ "PUT",
8396
+ "PATCH",
8397
+ "DELETE"
8398
+ ].includes(this.method)) this.context.feedback.showSuccess("操作成功");
8399
+ return finalResult;
8372
8400
  } catch (error) {
8373
8401
  if (error instanceof NetworkRequestError && error.detail.type === "httpError" && error.detail.status === 401) onUnauthorized?.();
8402
+ if (this.context.feedback) {
8403
+ const msg = error instanceof BusinessError ? error.message : error instanceof NetworkRequestError ? `请求失败: ${error.message}` : "操作失败";
8404
+ this.context.feedback.showError(msg);
8405
+ }
8374
8406
  throw error;
8375
8407
  } finally {
8376
8408
  if (totalTimeoutId !== void 0) clearTimeout(totalTimeoutId);
8377
8409
  }
8378
8410
  }
8379
8411
  async executeTransport(transport, url, headers, body, signal) {
8380
- return unwrapResponse(await transport.send({
8381
- url,
8382
- method: this.method,
8383
- headers,
8384
- body,
8385
- signal,
8386
- timeout: this.config.timeout
8387
- }));
8412
+ let response;
8413
+ try {
8414
+ response = await transport.send({
8415
+ url,
8416
+ method: this.method,
8417
+ headers,
8418
+ body,
8419
+ signal,
8420
+ timeout: this.config.timeout
8421
+ });
8422
+ } catch (error) {
8423
+ if (error && typeof error === "object" && "type" in error) throw new NetworkRequestError(error);
8424
+ throw new NetworkRequestError({
8425
+ type: "unknown",
8426
+ error
8427
+ });
8428
+ }
8429
+ return unwrapResponse(response);
8388
8430
  }
8389
8431
  };
8390
8432
  //#endregion
@@ -8401,6 +8443,7 @@ function createRequest(options = {}) {
8401
8443
  getToken: options.getToken,
8402
8444
  onUnauthorized: options.onUnauthorized,
8403
8445
  responseInterceptor: options.responseInterceptor,
8446
+ feedback: options.feedback,
8404
8447
  registerAbort: (controller) => {
8405
8448
  abortControllers.add(controller);
8406
8449
  const originalAbort = controller.abort.bind(controller);
@@ -10316,7 +10359,13 @@ var XhrTransport = class {
10316
10359
  headers,
10317
10360
  body: null,
10318
10361
  text: () => Promise.resolve(responseText),
10319
- json: () => Promise.resolve(JSON.parse(responseText)),
10362
+ json: () => {
10363
+ try {
10364
+ return Promise.resolve(JSON.parse(responseText));
10365
+ } catch (e) {
10366
+ return Promise.reject(e);
10367
+ }
10368
+ },
10320
10369
  blob: () => Promise.resolve(new Blob([responseText]))
10321
10370
  });
10322
10371
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kine-design/crud",
3
- "version": "0.0.1-beta.16",
3
+ "version": "0.0.1-beta.18",
4
4
  "type": "module",
5
5
  "main": "./dist/crud.js",
6
6
  "types": "./dist/index.d.ts",