@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.
- package/.vlaude/last-session-id +1 -0
- package/composables/auth/useAuth.ts +22 -2
- package/composables/request/createRequest.ts +1 -0
- package/composables/request/requestBuilder.ts +33 -8
- package/composables/request/transport/xhrTransport.ts +4 -1
- package/dist/composables/request/requestBuilder.d.ts +2 -0
- package/dist/crud.js +69 -20
- package/package.json +1 -1
|
@@ -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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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>() =>
|
|
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]
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
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: () =>
|
|
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
|
};
|