@kine-design/crud 0.0.1-beta.1
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/dist/components/layout/KContent.d.ts +2 -0
- package/dist/components/layout/KHeader.d.ts +2 -0
- package/dist/components/layout/KLayout.d.ts +44 -0
- package/dist/components/layout/KSider.d.ts +2 -0
- package/dist/components/layout/index.d.ts +13 -0
- package/dist/components/login/KLoginPage.d.ts +43 -0
- package/dist/components/login/index.d.ts +10 -0
- package/dist/components/navMenu/KNavMenu.d.ts +27 -0
- package/dist/components/navMenu/index.d.ts +2 -0
- package/dist/components/pageHeader/KPageHeader.d.ts +49 -0
- package/dist/components/pageHeader/index.d.ts +9 -0
- package/dist/components/searchTable/KSearchTable.d.ts +99 -0
- package/dist/components/searchTable/index.d.ts +9 -0
- package/dist/components/upload/KFileList.d.ts +27 -0
- package/dist/components/upload/KImageUpload.d.ts +91 -0
- package/dist/components/upload/KUpload.d.ts +82 -0
- package/dist/components/upload/index.d.ts +12 -0
- package/dist/components/upload/types.d.ts +23 -0
- package/dist/composables/auth/authGuard.d.ts +67 -0
- package/dist/composables/auth/index.d.ts +14 -0
- package/dist/composables/auth/types.d.ts +90 -0
- package/dist/composables/auth/useAuth.d.ts +45 -0
- package/dist/composables/auth/vCan.d.ts +44 -0
- package/dist/composables/defineRepository.d.ts +25 -0
- package/dist/composables/error/createErrorHandler.d.ts +22 -0
- package/dist/composables/error/defaultFeedbackHandler.d.ts +3 -0
- package/dist/composables/error/dispatchError.d.ts +6 -0
- package/dist/composables/error/index.d.ts +13 -0
- package/dist/composables/error/types.d.ts +28 -0
- package/dist/composables/error/useErrorHandler.d.ts +26 -0
- package/dist/composables/index.d.ts +15 -0
- package/dist/composables/request/composables.d.ts +44 -0
- package/dist/composables/request/controlGate.d.ts +21 -0
- package/dist/composables/request/createRequest.d.ts +31 -0
- package/dist/composables/request/index.d.ts +23 -0
- package/dist/composables/request/orchestrator.d.ts +16 -0
- package/dist/composables/request/requestBuilder.d.ts +34 -0
- package/dist/composables/request/transport/fetchTransport.d.ts +4 -0
- package/dist/composables/request/transport/xhrTransport.d.ts +4 -0
- package/dist/composables/request/types.d.ts +166 -0
- package/dist/composables/request/upload.d.ts +17 -0
- package/dist/composables/router/createRouterGuard.d.ts +50 -0
- package/dist/composables/router/defineCrudRoutes.d.ts +26 -0
- package/dist/composables/router/index.d.ts +14 -0
- package/dist/composables/router/types.d.ts +110 -0
- package/dist/composables/router/useMenuFromRoutes.d.ts +30 -0
- package/dist/composables/router/useTabStore.d.ts +35 -0
- package/dist/composables/setupCrud.d.ts +17 -0
- package/dist/composables/storage/createStorageAdapter.d.ts +22 -0
- package/dist/composables/storage/index.d.ts +12 -0
- package/dist/composables/storage/types.d.ts +14 -0
- package/dist/composables/storage/useStorage.d.ts +17 -0
- package/dist/composables/store/defineUserStore.d.ts +62 -0
- package/dist/composables/store/index.d.ts +10 -0
- package/dist/composables/types.d.ts +68 -0
- package/dist/crud.css +1308 -0
- package/dist/crud.js +3046 -0
- package/dist/index.d.ts +29 -0
- package/dist/setup.d.ts +95 -0
- package/dist/vite.config.build.d.ts +2 -0
- package/package.json +33 -0
package/dist/crud.js
ADDED
|
@@ -0,0 +1,3046 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { defineComponent, ref, watch, provide, createVNode, inject, computed, Fragment, Comment, createTextVNode, mergeProps, isRef, toRef, reactive, createApp, shallowRef, triggerRef, resolveComponent, h, onMounted, onBeforeUnmount, onUnmounted } from "vue";
|
|
5
|
+
import { PaginationCore, usePagination } from "@kine-design/core";
|
|
6
|
+
import { useQueryClient, useMutation, useQuery, QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
|
|
7
|
+
import { createPinia, defineStore } from "pinia";
|
|
8
|
+
import { useRouter, useRoute } from "vue-router";
|
|
9
|
+
const LAYOUT_COLLAPSED_KEY = Symbol("kCrudLayoutCollapsed");
|
|
10
|
+
const LAYOUT_TOGGLE_KEY = Symbol("kCrudLayoutToggle");
|
|
11
|
+
const LAYOUT_SIDER_WIDTH_KEY = Symbol("kCrudLayoutSiderWidth");
|
|
12
|
+
const LAYOUT_COLLAPSED_WIDTH_KEY = Symbol("kCrudLayoutCollapsedWidth");
|
|
13
|
+
const KLayout = /* @__PURE__ */ defineComponent({
|
|
14
|
+
name: "KLayout",
|
|
15
|
+
props: {
|
|
16
|
+
/** 受控模式:外部传入 collapsed 状态 */
|
|
17
|
+
collapsed: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: void 0
|
|
20
|
+
},
|
|
21
|
+
/** 侧边栏展开宽度 */
|
|
22
|
+
siderWidth: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: "240px"
|
|
25
|
+
},
|
|
26
|
+
/** 侧边栏折叠宽度 */
|
|
27
|
+
collapsedWidth: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: "64px"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
emits: ["update:collapsed"],
|
|
33
|
+
setup(props2, ctx) {
|
|
34
|
+
const innerCollapsed = ref(props2.collapsed ?? false);
|
|
35
|
+
watch(() => props2.collapsed, (val) => {
|
|
36
|
+
if (val !== void 0) {
|
|
37
|
+
innerCollapsed.value = val;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const toggleCollapsed = () => {
|
|
41
|
+
const next = !innerCollapsed.value;
|
|
42
|
+
innerCollapsed.value = next;
|
|
43
|
+
ctx.emit("update:collapsed", next);
|
|
44
|
+
};
|
|
45
|
+
provide(LAYOUT_COLLAPSED_KEY, innerCollapsed);
|
|
46
|
+
provide(LAYOUT_TOGGLE_KEY, toggleCollapsed);
|
|
47
|
+
provide(LAYOUT_SIDER_WIDTH_KEY, props2.siderWidth);
|
|
48
|
+
provide(LAYOUT_COLLAPSED_WIDTH_KEY, props2.collapsedWidth);
|
|
49
|
+
return () => {
|
|
50
|
+
var _a, _b;
|
|
51
|
+
return createVNode("div", {
|
|
52
|
+
"class": "k-crud-layout"
|
|
53
|
+
}, [(_b = (_a = ctx.slots).default) == null ? void 0 : _b.call(_a)]);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const KSider = /* @__PURE__ */ defineComponent({
|
|
58
|
+
name: "KSider",
|
|
59
|
+
setup(_, ctx) {
|
|
60
|
+
const collapsed = inject(LAYOUT_COLLAPSED_KEY);
|
|
61
|
+
const toggle = inject(LAYOUT_TOGGLE_KEY);
|
|
62
|
+
const siderWidth = inject(LAYOUT_SIDER_WIDTH_KEY, "240px");
|
|
63
|
+
const collapsedWidth = inject(LAYOUT_COLLAPSED_WIDTH_KEY, "64px");
|
|
64
|
+
const currentWidth = computed(() => {
|
|
65
|
+
if (!collapsed) return siderWidth;
|
|
66
|
+
return collapsed.value ? collapsedWidth : siderWidth;
|
|
67
|
+
});
|
|
68
|
+
const isCollapsed = computed(() => (collapsed == null ? void 0 : collapsed.value) ?? false);
|
|
69
|
+
const handleTriggerClick = () => {
|
|
70
|
+
toggle == null ? void 0 : toggle();
|
|
71
|
+
};
|
|
72
|
+
const defaultTrigger = () => createVNode("button", {
|
|
73
|
+
"class": "k-crud-sider__trigger-btn",
|
|
74
|
+
"onClick": handleTriggerClick,
|
|
75
|
+
"title": isCollapsed.value ? "展开侧边栏" : "折叠侧边栏"
|
|
76
|
+
}, [createVNode("span", {
|
|
77
|
+
"class": ["k-crud-sider__trigger-icon", isCollapsed.value && "k-crud-sider__trigger-icon--collapsed"]
|
|
78
|
+
}, ["‹"])]);
|
|
79
|
+
return () => {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
return createVNode("aside", {
|
|
82
|
+
"class": ["k-crud-sider", isCollapsed.value && "k-crud-sider--collapsed"],
|
|
83
|
+
"style": {
|
|
84
|
+
width: currentWidth.value
|
|
85
|
+
}
|
|
86
|
+
}, [ctx.slots.logo && createVNode("div", {
|
|
87
|
+
"class": "k-crud-sider__logo"
|
|
88
|
+
}, [ctx.slots.logo()]), createVNode("div", {
|
|
89
|
+
"class": "k-crud-sider__body"
|
|
90
|
+
}, [(_b = (_a = ctx.slots).default) == null ? void 0 : _b.call(_a)]), createVNode("div", {
|
|
91
|
+
"class": "k-crud-sider__trigger"
|
|
92
|
+
}, [ctx.slots.trigger ? ctx.slots.trigger({
|
|
93
|
+
collapsed: isCollapsed.value,
|
|
94
|
+
toggle
|
|
95
|
+
}) : defaultTrigger()])]);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const KHeader = /* @__PURE__ */ defineComponent({
|
|
100
|
+
name: "KHeader",
|
|
101
|
+
setup(_, ctx) {
|
|
102
|
+
return () => {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
return createVNode("header", {
|
|
105
|
+
"class": "k-crud-header"
|
|
106
|
+
}, [createVNode("div", {
|
|
107
|
+
"class": "k-crud-header__main"
|
|
108
|
+
}, [(_b = (_a = ctx.slots).default) == null ? void 0 : _b.call(_a)]), ctx.slots.extra && createVNode("div", {
|
|
109
|
+
"class": "k-crud-header__extra"
|
|
110
|
+
}, [ctx.slots.extra()])]);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const KContent = /* @__PURE__ */ defineComponent({
|
|
115
|
+
name: "KContent",
|
|
116
|
+
setup(_, ctx) {
|
|
117
|
+
return () => {
|
|
118
|
+
var _a, _b;
|
|
119
|
+
return createVNode("main", {
|
|
120
|
+
"class": "k-crud-content"
|
|
121
|
+
}, [(_b = (_a = ctx.slots).default) == null ? void 0 : _b.call(_a)]);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const props$1 = {
|
|
126
|
+
data: { type: Array, default: () => [] },
|
|
127
|
+
height: { type: String, default: null },
|
|
128
|
+
paramClass: { type: Boolean, default: false },
|
|
129
|
+
loading: { type: Boolean, default: false }
|
|
130
|
+
};
|
|
131
|
+
function useTable() {
|
|
132
|
+
const error = (msg) => console.warn(`[水墨UI表格组件] ${msg}`);
|
|
133
|
+
const initTable = (renders, columns, data) => {
|
|
134
|
+
const tbodyTrList = [];
|
|
135
|
+
data.forEach(() => {
|
|
136
|
+
tbodyTrList.push([]);
|
|
137
|
+
});
|
|
138
|
+
const getData = (i, param) => {
|
|
139
|
+
if (data[i] && data[i][param]) {
|
|
140
|
+
return data[i][param];
|
|
141
|
+
}
|
|
142
|
+
return "";
|
|
143
|
+
};
|
|
144
|
+
const pushTd = (param, bodySlot, style) => {
|
|
145
|
+
if (param) {
|
|
146
|
+
tbodyTrList.forEach((t, i) => {
|
|
147
|
+
t.push(renders.tbodyTr({
|
|
148
|
+
data: getData(i, param),
|
|
149
|
+
style,
|
|
150
|
+
param,
|
|
151
|
+
slot: bodySlot,
|
|
152
|
+
slotInfo: {
|
|
153
|
+
data: data[i],
|
|
154
|
+
index: i
|
|
155
|
+
}
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
error("param is undefined, column without param will be ignored!");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const getStyle = (options) => {
|
|
163
|
+
if (!options || !options.width) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
const numberWidth = Number(options.width);
|
|
167
|
+
if (!isNaN(numberWidth)) {
|
|
168
|
+
return { width: numberWidth + "px" };
|
|
169
|
+
}
|
|
170
|
+
return { width: options.width };
|
|
171
|
+
};
|
|
172
|
+
const initTHead = () => {
|
|
173
|
+
const ths = (columns ?? []).filter((column) => {
|
|
174
|
+
if (!column.props) {
|
|
175
|
+
error("column.props is undefined, column without param will be ignored!");
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}).map((column) => {
|
|
180
|
+
const slots = renders.initSlot(column);
|
|
181
|
+
let bodySlot;
|
|
182
|
+
let headSlot;
|
|
183
|
+
const style = getStyle(column.props);
|
|
184
|
+
if (slots) {
|
|
185
|
+
bodySlot = slots.body;
|
|
186
|
+
headSlot = slots.head;
|
|
187
|
+
}
|
|
188
|
+
pushTd(column.props.param, bodySlot, style);
|
|
189
|
+
return renders.theadTh({
|
|
190
|
+
label: column.props.label,
|
|
191
|
+
param: column.props.param,
|
|
192
|
+
slot: headSlot,
|
|
193
|
+
style
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
return renders.thead(ths);
|
|
197
|
+
};
|
|
198
|
+
const thead = initTHead();
|
|
199
|
+
const tbody = tbodyTrList.length > 0 ? renders.tbody(tbodyTrList.map((tds, i) => renders.tbodyTrs(tds, i))) : renders.empty;
|
|
200
|
+
return {
|
|
201
|
+
thead,
|
|
202
|
+
tbody
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
initTable,
|
|
207
|
+
error
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const TableCore = {
|
|
211
|
+
props: props$1
|
|
212
|
+
};
|
|
213
|
+
const {
|
|
214
|
+
props
|
|
215
|
+
} = TableCore;
|
|
216
|
+
const KTable = /* @__PURE__ */ defineComponent((_props, {
|
|
217
|
+
slots
|
|
218
|
+
}) => {
|
|
219
|
+
const p = _props;
|
|
220
|
+
const {
|
|
221
|
+
initTable
|
|
222
|
+
} = useTable();
|
|
223
|
+
return () => {
|
|
224
|
+
var _a, _b;
|
|
225
|
+
const columns = [];
|
|
226
|
+
const defaultSlot = ((_a = slots.default) == null ? void 0 : _a.call(slots)) ?? [];
|
|
227
|
+
defaultSlot.forEach((s) => {
|
|
228
|
+
var _a2;
|
|
229
|
+
if (s.type === Fragment) {
|
|
230
|
+
(_a2 = s.children) == null ? void 0 : _a2.forEach((c) => columns.push(c));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (s.type === Comment) return;
|
|
234
|
+
if (typeof s.type === "object" && s.type.name === "KTableColumn") {
|
|
235
|
+
columns.push(s);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
const style = p.height ? {
|
|
239
|
+
height: p.height
|
|
240
|
+
} : void 0;
|
|
241
|
+
const {
|
|
242
|
+
thead,
|
|
243
|
+
tbody
|
|
244
|
+
} = initTable({
|
|
245
|
+
// 数据为空时的占位渲染
|
|
246
|
+
empty: createVNode("tbody", {
|
|
247
|
+
"class": "m-table-empty k-table-empty"
|
|
248
|
+
}, [createVNode("tr", null, [createVNode("th", {
|
|
249
|
+
"colspan": columns.length
|
|
250
|
+
}, [((_b = slots.empty) == null ? void 0 : _b.call(slots)) ?? "暂无数据"])])]),
|
|
251
|
+
// 渲染单个数据单元格
|
|
252
|
+
tbodyTr: ({
|
|
253
|
+
data,
|
|
254
|
+
param,
|
|
255
|
+
slot,
|
|
256
|
+
style: cellStyle,
|
|
257
|
+
slotInfo
|
|
258
|
+
}) => createVNode("td", {
|
|
259
|
+
"style": cellStyle,
|
|
260
|
+
"class": ["m-td", "k-td", p.paramClass ? `m-td-${param}` : null, p.paramClass ? `k-td-${param}` : null]
|
|
261
|
+
}, [slot ? slot({
|
|
262
|
+
data: slotInfo == null ? void 0 : slotInfo.data,
|
|
263
|
+
index: slotInfo == null ? void 0 : slotInfo.index
|
|
264
|
+
}) : data]),
|
|
265
|
+
// 渲染单个表头单元格
|
|
266
|
+
theadTh: ({
|
|
267
|
+
label,
|
|
268
|
+
param,
|
|
269
|
+
slot,
|
|
270
|
+
style: cellStyle
|
|
271
|
+
}) => createVNode("th", {
|
|
272
|
+
"class": ["m-th", "k-th", p.paramClass ? `m-th-${param}` : null, p.paramClass ? `k-th-${param}` : null],
|
|
273
|
+
"style": cellStyle
|
|
274
|
+
}, [slot ? slot() : label]),
|
|
275
|
+
// 渲染整个 thead
|
|
276
|
+
thead: (ths) => createVNode("thead", {
|
|
277
|
+
"class": "m-thead k-thead"
|
|
278
|
+
}, [createVNode("tr", {
|
|
279
|
+
"class": "m-tr k-tr"
|
|
280
|
+
}, [ths])]),
|
|
281
|
+
// 渲染整个 tbody
|
|
282
|
+
tbody: (trs) => createVNode("tbody", {
|
|
283
|
+
"class": "m-tbody k-tbody"
|
|
284
|
+
}, [trs]),
|
|
285
|
+
// 渲染单行 tr
|
|
286
|
+
tbodyTrs: (tds, i) => createVNode("tr", {
|
|
287
|
+
"class": "m-tr k-tr",
|
|
288
|
+
"key": i
|
|
289
|
+
}, [...tds]),
|
|
290
|
+
// 从 KTableColumn VNode 中提取 body / head 插槽
|
|
291
|
+
initSlot: (tableColumn) => {
|
|
292
|
+
const children = tableColumn.children;
|
|
293
|
+
if (!children || Array.isArray(children) || typeof children !== "object") {
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
body: children.default,
|
|
298
|
+
head: children.head
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}, columns, p.data);
|
|
302
|
+
return createVNode("div", {
|
|
303
|
+
"class": ["m-table", "k-table", p.loading && "k-table-loading-wrap"],
|
|
304
|
+
"style": style
|
|
305
|
+
}, [p.loading && createVNode("div", {
|
|
306
|
+
"class": "k-table-loading-overlay"
|
|
307
|
+
}, [createVNode("div", {
|
|
308
|
+
"class": "k-table-loading-spinner"
|
|
309
|
+
}, null)]), createVNode("table", {
|
|
310
|
+
"class": "m-table-inner k-table-inner"
|
|
311
|
+
}, [thead, tbody])]);
|
|
312
|
+
};
|
|
313
|
+
}, {
|
|
314
|
+
name: "KTable",
|
|
315
|
+
props
|
|
316
|
+
});
|
|
317
|
+
const {
|
|
318
|
+
props: paginationProps
|
|
319
|
+
} = PaginationCore;
|
|
320
|
+
const KPagination = /* @__PURE__ */ defineComponent((_props, {
|
|
321
|
+
emit
|
|
322
|
+
}) => {
|
|
323
|
+
const p = _props;
|
|
324
|
+
const currentValue = ref(p.modelValue ?? p.defaultCurrent ?? 1);
|
|
325
|
+
watch(() => p.modelValue, (val) => {
|
|
326
|
+
if (val !== void 0) {
|
|
327
|
+
currentValue.value = val;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
const {
|
|
331
|
+
getPageNumList,
|
|
332
|
+
getPageBtnLength
|
|
333
|
+
} = usePagination(p, currentValue);
|
|
334
|
+
const changePage = (page) => {
|
|
335
|
+
const total = getPageBtnLength();
|
|
336
|
+
if (page < 1 || page > total) return;
|
|
337
|
+
currentValue.value = page;
|
|
338
|
+
emit("update:modelValue", page);
|
|
339
|
+
emit("change", page);
|
|
340
|
+
};
|
|
341
|
+
const toPrev = () => {
|
|
342
|
+
if (currentValue.value === 1) return;
|
|
343
|
+
changePage(currentValue.value - 1);
|
|
344
|
+
};
|
|
345
|
+
const toNext = () => {
|
|
346
|
+
if (currentValue.value === getPageBtnLength()) return;
|
|
347
|
+
changePage(currentValue.value + 1);
|
|
348
|
+
};
|
|
349
|
+
return () => {
|
|
350
|
+
const pages = getPageNumList();
|
|
351
|
+
const layoutKeys = p.layout.split(",").map((k) => k.trim());
|
|
352
|
+
const totalPages = getPageBtnLength();
|
|
353
|
+
const btnGetter = {
|
|
354
|
+
/** 总条数区域 */
|
|
355
|
+
total: () => createVNode("div", {
|
|
356
|
+
"class": "m-page-total k-page-total"
|
|
357
|
+
}, [createTextVNode("共 "), createVNode("span", null, [p.total]), createTextVNode(" 条")]),
|
|
358
|
+
/** 上一页按钮 */
|
|
359
|
+
prev: () => createVNode("div", {
|
|
360
|
+
"class": ["m-page-prev", "k-page-prev", currentValue.value === 1 ? "m-page-prev-disabled k-page-prev-disabled" : ""],
|
|
361
|
+
"onClick": toPrev
|
|
362
|
+
}, [createTextVNode("‹")]),
|
|
363
|
+
/** 页码列表(含折叠省略号) */
|
|
364
|
+
pager: () => pages.map((page) => createVNode("div", {
|
|
365
|
+
"key": `${page.type}-${page.value}`,
|
|
366
|
+
"class": ["m-pager", "k-pager", page.type === "folded" ? "m-pager-folded k-pager-folded" : "", page.isCurrent ? "m-pager-current k-pager-current" : ""],
|
|
367
|
+
"onClick": () => changePage(page.jump)
|
|
368
|
+
}, [page.value])),
|
|
369
|
+
/** 下一页按钮 */
|
|
370
|
+
next: () => createVNode("div", {
|
|
371
|
+
"class": ["m-page-next", "k-page-next", currentValue.value === totalPages ? "m-page-next-disabled k-page-next-disabled" : ""],
|
|
372
|
+
"onClick": toNext
|
|
373
|
+
}, [createTextVNode("›")])
|
|
374
|
+
};
|
|
375
|
+
return createVNode("div", {
|
|
376
|
+
"class": "m-pagination k-pagination"
|
|
377
|
+
}, [layoutKeys.map((key) => {
|
|
378
|
+
var _a;
|
|
379
|
+
return (_a = btnGetter[key]) == null ? void 0 : _a.call(btnGetter);
|
|
380
|
+
})]);
|
|
381
|
+
};
|
|
382
|
+
}, {
|
|
383
|
+
name: "KPagination",
|
|
384
|
+
props: paginationProps,
|
|
385
|
+
emits: ["update:modelValue", "change"]
|
|
386
|
+
});
|
|
387
|
+
const KSearchTable = /* @__PURE__ */ defineComponent({
|
|
388
|
+
name: "KSearchTable",
|
|
389
|
+
props: {
|
|
390
|
+
/** 加载状态 */
|
|
391
|
+
loading: {
|
|
392
|
+
type: Boolean,
|
|
393
|
+
default: false
|
|
394
|
+
},
|
|
395
|
+
/** 表格数据 */
|
|
396
|
+
data: {
|
|
397
|
+
type: Array,
|
|
398
|
+
default: () => []
|
|
399
|
+
},
|
|
400
|
+
/** 总条数 */
|
|
401
|
+
total: {
|
|
402
|
+
type: Number,
|
|
403
|
+
default: 0
|
|
404
|
+
},
|
|
405
|
+
/** 当前页(受控,配合 v-model:page) */
|
|
406
|
+
page: {
|
|
407
|
+
type: Number,
|
|
408
|
+
default: 1
|
|
409
|
+
},
|
|
410
|
+
/** 每页条数(受控,配合 v-model:pageSize) */
|
|
411
|
+
pageSize: {
|
|
412
|
+
type: Number,
|
|
413
|
+
default: 20
|
|
414
|
+
},
|
|
415
|
+
/** 是否显示搜索区 */
|
|
416
|
+
searchable: {
|
|
417
|
+
type: Boolean,
|
|
418
|
+
default: true
|
|
419
|
+
},
|
|
420
|
+
/** 是否显示分页 */
|
|
421
|
+
pageable: {
|
|
422
|
+
type: Boolean,
|
|
423
|
+
default: true
|
|
424
|
+
},
|
|
425
|
+
/** 透传给 KTable 的额外 props */
|
|
426
|
+
tableProps: {
|
|
427
|
+
type: Object,
|
|
428
|
+
default: () => ({})
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
emits: [
|
|
432
|
+
"update:page",
|
|
433
|
+
"update:pageSize",
|
|
434
|
+
/** 点击搜索按钮 */
|
|
435
|
+
"search",
|
|
436
|
+
/** 点击重置按钮 */
|
|
437
|
+
"reset"
|
|
438
|
+
],
|
|
439
|
+
setup(props2, ctx) {
|
|
440
|
+
const onSearch = () => {
|
|
441
|
+
ctx.emit("search");
|
|
442
|
+
};
|
|
443
|
+
const onReset = () => {
|
|
444
|
+
ctx.emit("reset");
|
|
445
|
+
};
|
|
446
|
+
const onPageChange = (page) => {
|
|
447
|
+
ctx.emit("update:page", page);
|
|
448
|
+
};
|
|
449
|
+
return () => {
|
|
450
|
+
var _a, _b;
|
|
451
|
+
return createVNode("div", {
|
|
452
|
+
"class": ["k-search-table", props2.loading ? "k-search-table--loading" : ""]
|
|
453
|
+
}, [props2.searchable && createVNode("div", {
|
|
454
|
+
"class": "k-search-table-search"
|
|
455
|
+
}, [createVNode("div", {
|
|
456
|
+
"class": "k-search-table-search-form"
|
|
457
|
+
}, [(_b = (_a = ctx.slots).search) == null ? void 0 : _b.call(_a)]), createVNode("div", {
|
|
458
|
+
"class": "k-search-table-search-actions"
|
|
459
|
+
}, [createVNode("button", {
|
|
460
|
+
"type": "button",
|
|
461
|
+
"class": "k-search-table-btn k-search-table-btn--primary",
|
|
462
|
+
"onClick": onSearch
|
|
463
|
+
}, [createTextVNode("搜索")]), createVNode("button", {
|
|
464
|
+
"type": "button",
|
|
465
|
+
"class": "k-search-table-btn",
|
|
466
|
+
"onClick": onReset
|
|
467
|
+
}, [createTextVNode("重置")])])]), ctx.slots.toolbar && createVNode("div", {
|
|
468
|
+
"class": "k-search-table-toolbar"
|
|
469
|
+
}, [ctx.slots.toolbar()]), createVNode("div", {
|
|
470
|
+
"class": "k-search-table-body"
|
|
471
|
+
}, [createVNode(KTable, mergeProps({
|
|
472
|
+
"data": props2.data
|
|
473
|
+
}, props2.tableProps), {
|
|
474
|
+
default: ctx.slots.default,
|
|
475
|
+
empty: ctx.slots.empty
|
|
476
|
+
})]), props2.pageable && createVNode("div", {
|
|
477
|
+
"class": "k-search-table-pagination"
|
|
478
|
+
}, [createVNode(KPagination, {
|
|
479
|
+
"modelValue": props2.page,
|
|
480
|
+
"total": props2.total,
|
|
481
|
+
"pageSize": props2.pageSize,
|
|
482
|
+
"onUpdate:modelValue": onPageChange
|
|
483
|
+
}, null)])]);
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
const KLoginPage = /* @__PURE__ */ defineComponent({
|
|
488
|
+
name: "KLoginPage",
|
|
489
|
+
props: {
|
|
490
|
+
/** 系统名称 */
|
|
491
|
+
title: {
|
|
492
|
+
type: String,
|
|
493
|
+
default: "系统登录"
|
|
494
|
+
},
|
|
495
|
+
/** 登录中状态 */
|
|
496
|
+
loading: {
|
|
497
|
+
type: Boolean,
|
|
498
|
+
default: false
|
|
499
|
+
},
|
|
500
|
+
/** 登录提交回调 */
|
|
501
|
+
onSubmit: {
|
|
502
|
+
type: Function,
|
|
503
|
+
default: void 0
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
setup(props2, ctx) {
|
|
507
|
+
const username = ref("");
|
|
508
|
+
const password = ref("");
|
|
509
|
+
const remember = ref(false);
|
|
510
|
+
const handleSubmit = async (e) => {
|
|
511
|
+
var _a;
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
if (props2.loading) return;
|
|
514
|
+
await ((_a = props2.onSubmit) == null ? void 0 : _a.call(props2, {
|
|
515
|
+
username: username.value,
|
|
516
|
+
password: password.value,
|
|
517
|
+
remember: remember.value
|
|
518
|
+
}));
|
|
519
|
+
};
|
|
520
|
+
return () => createVNode("div", {
|
|
521
|
+
"class": "k-login-page"
|
|
522
|
+
}, [createVNode("div", {
|
|
523
|
+
"class": "k-login-card"
|
|
524
|
+
}, [ctx.slots.logo && createVNode("div", {
|
|
525
|
+
"class": "k-login-logo"
|
|
526
|
+
}, [ctx.slots.logo()]), createVNode("h2", {
|
|
527
|
+
"class": "k-login-title"
|
|
528
|
+
}, [props2.title]), createVNode("form", {
|
|
529
|
+
"class": "k-login-form",
|
|
530
|
+
"onSubmit": handleSubmit
|
|
531
|
+
}, [createVNode("div", {
|
|
532
|
+
"class": "k-login-form-item"
|
|
533
|
+
}, [createVNode("label", null, [createTextVNode("用户名")]), createVNode("input", {
|
|
534
|
+
"type": "text",
|
|
535
|
+
"value": username.value,
|
|
536
|
+
"placeholder": "请输入用户名",
|
|
537
|
+
"autocomplete": "username",
|
|
538
|
+
"required": true,
|
|
539
|
+
"onInput": (e) => {
|
|
540
|
+
username.value = e.target.value;
|
|
541
|
+
}
|
|
542
|
+
}, null)]), createVNode("div", {
|
|
543
|
+
"class": "k-login-form-item"
|
|
544
|
+
}, [createVNode("label", null, [createTextVNode("密码")]), createVNode("input", {
|
|
545
|
+
"type": "password",
|
|
546
|
+
"value": password.value,
|
|
547
|
+
"placeholder": "请输入密码",
|
|
548
|
+
"autocomplete": "current-password",
|
|
549
|
+
"required": true,
|
|
550
|
+
"onInput": (e) => {
|
|
551
|
+
password.value = e.target.value;
|
|
552
|
+
}
|
|
553
|
+
}, null)]), createVNode("label", {
|
|
554
|
+
"class": "k-login-remember"
|
|
555
|
+
}, [createVNode("input", {
|
|
556
|
+
"type": "checkbox",
|
|
557
|
+
"checked": remember.value,
|
|
558
|
+
"onChange": (e) => {
|
|
559
|
+
remember.value = e.target.checked;
|
|
560
|
+
}
|
|
561
|
+
}, null), createTextVNode("记住我")]), createVNode("button", {
|
|
562
|
+
"type": "submit",
|
|
563
|
+
"class": ["k-login-submit", props2.loading && "k-login-submit--loading"],
|
|
564
|
+
"disabled": props2.loading
|
|
565
|
+
}, [props2.loading ? "登录中..." : "登 录"])]), ctx.slots.extra && createVNode("div", {
|
|
566
|
+
"class": "k-login-extra"
|
|
567
|
+
}, [ctx.slots.extra()])]), ctx.slots.footer && createVNode("div", {
|
|
568
|
+
"class": "k-login-footer"
|
|
569
|
+
}, [ctx.slots.footer()])]);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
const KPageHeader = /* @__PURE__ */ defineComponent({
|
|
573
|
+
name: "KPageHeader",
|
|
574
|
+
props: {
|
|
575
|
+
/** 页面标题 */
|
|
576
|
+
title: {
|
|
577
|
+
type: String,
|
|
578
|
+
default: ""
|
|
579
|
+
},
|
|
580
|
+
/** 副标题 */
|
|
581
|
+
subtitle: {
|
|
582
|
+
type: String,
|
|
583
|
+
default: ""
|
|
584
|
+
},
|
|
585
|
+
/** 是否显示返回按钮 */
|
|
586
|
+
backable: {
|
|
587
|
+
type: Boolean,
|
|
588
|
+
default: false
|
|
589
|
+
},
|
|
590
|
+
/** 返回回调 */
|
|
591
|
+
onBack: {
|
|
592
|
+
type: Function,
|
|
593
|
+
default: void 0
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
setup(props2, ctx) {
|
|
597
|
+
const handleBack = () => {
|
|
598
|
+
var _a;
|
|
599
|
+
(_a = props2.onBack) == null ? void 0 : _a.call(props2);
|
|
600
|
+
};
|
|
601
|
+
return () => createVNode("div", {
|
|
602
|
+
"class": "k-page-header"
|
|
603
|
+
}, [ctx.slots.breadcrumb && createVNode("div", {
|
|
604
|
+
"class": "k-page-header-breadcrumb"
|
|
605
|
+
}, [ctx.slots.breadcrumb()]), createVNode("div", {
|
|
606
|
+
"class": "k-page-header-main"
|
|
607
|
+
}, [createVNode("div", {
|
|
608
|
+
"class": "k-page-header-left"
|
|
609
|
+
}, [props2.backable && createVNode("button", {
|
|
610
|
+
"type": "button",
|
|
611
|
+
"class": "k-page-header-back",
|
|
612
|
+
"aria-label": "返回",
|
|
613
|
+
"onClick": handleBack
|
|
614
|
+
}, [createTextVNode("←")]), createVNode("div", {
|
|
615
|
+
"class": "k-page-header-heading"
|
|
616
|
+
}, [props2.title && createVNode("h2", {
|
|
617
|
+
"class": "k-page-header-title"
|
|
618
|
+
}, [props2.title]), props2.subtitle && createVNode("p", {
|
|
619
|
+
"class": "k-page-header-subtitle"
|
|
620
|
+
}, [props2.subtitle])])]), ctx.slots.extra && createVNode("div", {
|
|
621
|
+
"class": "k-page-header-extra"
|
|
622
|
+
}, [ctx.slots.extra()])]), ctx.slots.default && createVNode("div", {
|
|
623
|
+
"class": "k-page-header-content"
|
|
624
|
+
}, [ctx.slots.default()])]);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
function normalizeRef(value) {
|
|
628
|
+
return isRef(value) ? value : toRef(() => value);
|
|
629
|
+
}
|
|
630
|
+
function defineRepository(domain, endpoints) {
|
|
631
|
+
const useList = (params) => {
|
|
632
|
+
const paramsRef = normalizeRef(params ?? {});
|
|
633
|
+
const query = useQuery({
|
|
634
|
+
// query key 包含 params,params 变化时自动重新请求
|
|
635
|
+
queryKey: computed(() => [domain, "list", paramsRef.value]),
|
|
636
|
+
queryFn: () => {
|
|
637
|
+
if (!endpoints.list) {
|
|
638
|
+
return Promise.reject(new Error(`[defineRepository:${domain}] list endpoint 未定义`));
|
|
639
|
+
}
|
|
640
|
+
return endpoints.list(paramsRef.value);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
return {
|
|
644
|
+
data: computed(() => query.data.value),
|
|
645
|
+
isLoading: computed(() => query.isLoading.value),
|
|
646
|
+
isFetching: computed(() => query.isFetching.value),
|
|
647
|
+
isError: computed(() => query.isError.value),
|
|
648
|
+
error: computed(() => query.error.value),
|
|
649
|
+
refetch: () => {
|
|
650
|
+
query.refetch();
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
const useDetail = (id) => {
|
|
655
|
+
const idRef = normalizeRef(id);
|
|
656
|
+
const query = useQuery({
|
|
657
|
+
queryKey: computed(() => [domain, "detail", idRef.value]),
|
|
658
|
+
queryFn: () => {
|
|
659
|
+
if (!endpoints.detail) {
|
|
660
|
+
return Promise.reject(new Error(`[defineRepository:${domain}] detail endpoint 未定义`));
|
|
661
|
+
}
|
|
662
|
+
return endpoints.detail(idRef.value);
|
|
663
|
+
},
|
|
664
|
+
// id 为空时不发请求
|
|
665
|
+
enabled: computed(() => idRef.value !== void 0 && idRef.value !== null && idRef.value !== "")
|
|
666
|
+
});
|
|
667
|
+
return {
|
|
668
|
+
data: computed(() => query.data.value),
|
|
669
|
+
isLoading: computed(() => query.isLoading.value),
|
|
670
|
+
isFetching: computed(() => query.isFetching.value),
|
|
671
|
+
isError: computed(() => query.isError.value),
|
|
672
|
+
error: computed(() => query.error.value),
|
|
673
|
+
refetch: () => {
|
|
674
|
+
query.refetch();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
function buildInvalidateHandler(options) {
|
|
679
|
+
return async (queryClient) => {
|
|
680
|
+
var _a;
|
|
681
|
+
await queryClient.invalidateQueries({ queryKey: [domain] });
|
|
682
|
+
if ((_a = options == null ? void 0 : options.invalidates) == null ? void 0 : _a.length) {
|
|
683
|
+
await Promise.all(
|
|
684
|
+
options.invalidates.map(
|
|
685
|
+
(d) => queryClient.invalidateQueries({ queryKey: [d] })
|
|
686
|
+
)
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const useCreate = (options) => {
|
|
692
|
+
const queryClient = useQueryClient();
|
|
693
|
+
const invalidate = buildInvalidateHandler(options);
|
|
694
|
+
const mutation = useMutation({
|
|
695
|
+
mutationFn: (data) => {
|
|
696
|
+
if (!endpoints.create) {
|
|
697
|
+
return Promise.reject(new Error(`[defineRepository:${domain}] create endpoint 未定义`));
|
|
698
|
+
}
|
|
699
|
+
return endpoints.create(data);
|
|
700
|
+
},
|
|
701
|
+
onSuccess: () => invalidate(queryClient)
|
|
702
|
+
});
|
|
703
|
+
return {
|
|
704
|
+
// 类型断言:确保签名与泛型约束一致
|
|
705
|
+
mutate: (data) => mutation.mutateAsync(data),
|
|
706
|
+
isPending: computed(() => mutation.isPending.value),
|
|
707
|
+
isError: computed(() => mutation.isError.value),
|
|
708
|
+
error: computed(() => mutation.error.value)
|
|
709
|
+
};
|
|
710
|
+
};
|
|
711
|
+
const useUpdate = (options) => {
|
|
712
|
+
const queryClient = useQueryClient();
|
|
713
|
+
const invalidate = buildInvalidateHandler(options);
|
|
714
|
+
const mutation = useMutation({
|
|
715
|
+
mutationFn: ({ id, data }) => {
|
|
716
|
+
if (!endpoints.update) {
|
|
717
|
+
return Promise.reject(new Error(`[defineRepository:${domain}] update endpoint 未定义`));
|
|
718
|
+
}
|
|
719
|
+
return endpoints.update(id, data);
|
|
720
|
+
},
|
|
721
|
+
onSuccess: () => invalidate(queryClient)
|
|
722
|
+
});
|
|
723
|
+
return {
|
|
724
|
+
mutate: (variables) => mutation.mutateAsync(variables),
|
|
725
|
+
isPending: computed(() => mutation.isPending.value),
|
|
726
|
+
isError: computed(() => mutation.isError.value),
|
|
727
|
+
error: computed(() => mutation.error.value)
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
const useRemove = (options) => {
|
|
731
|
+
const queryClient = useQueryClient();
|
|
732
|
+
const invalidate = buildInvalidateHandler(options);
|
|
733
|
+
const mutation = useMutation({
|
|
734
|
+
mutationFn: (id) => {
|
|
735
|
+
if (!endpoints.remove) {
|
|
736
|
+
return Promise.reject(new Error(`[defineRepository:${domain}] remove endpoint 未定义`));
|
|
737
|
+
}
|
|
738
|
+
return endpoints.remove(id);
|
|
739
|
+
},
|
|
740
|
+
onSuccess: () => invalidate(queryClient)
|
|
741
|
+
});
|
|
742
|
+
return {
|
|
743
|
+
mutate: (id) => mutation.mutateAsync(id),
|
|
744
|
+
isPending: computed(() => mutation.isPending.value),
|
|
745
|
+
isError: computed(() => mutation.isError.value),
|
|
746
|
+
error: computed(() => mutation.error.value)
|
|
747
|
+
};
|
|
748
|
+
};
|
|
749
|
+
return { useList, useDetail, useCreate, useUpdate, useRemove };
|
|
750
|
+
}
|
|
751
|
+
function createStorageAdapter(storageType) {
|
|
752
|
+
function resolveStorage() {
|
|
753
|
+
if (typeof window === "undefined") return null;
|
|
754
|
+
return storageType === "local" ? window.localStorage : window.sessionStorage;
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
get(key) {
|
|
758
|
+
const storage = resolveStorage();
|
|
759
|
+
if (!storage) return null;
|
|
760
|
+
const raw = storage.getItem(key);
|
|
761
|
+
if (raw === null) return null;
|
|
762
|
+
try {
|
|
763
|
+
return JSON.parse(raw);
|
|
764
|
+
} catch {
|
|
765
|
+
return raw;
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
set(key, value) {
|
|
769
|
+
const storage = resolveStorage();
|
|
770
|
+
if (!storage) return;
|
|
771
|
+
storage.setItem(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
772
|
+
},
|
|
773
|
+
remove(key) {
|
|
774
|
+
const storage = resolveStorage();
|
|
775
|
+
if (!storage) return;
|
|
776
|
+
storage.removeItem(key);
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const AUTH_INJECT_KEY = Symbol("kine-design:auth");
|
|
781
|
+
const SCOPE_LEVEL = {
|
|
782
|
+
own: 1,
|
|
783
|
+
department: 2,
|
|
784
|
+
all: 3,
|
|
785
|
+
"*": Infinity
|
|
786
|
+
};
|
|
787
|
+
function isScopeSatisfied(permScope, requiredScope) {
|
|
788
|
+
const permLevel = SCOPE_LEVEL[permScope ?? "all"] ?? 0;
|
|
789
|
+
const requiredLevel = SCOPE_LEVEL[requiredScope] ?? 0;
|
|
790
|
+
return permLevel >= requiredLevel;
|
|
791
|
+
}
|
|
792
|
+
function matchesPermission(perm, resource, action, scope) {
|
|
793
|
+
if (perm.resource !== resource) return false;
|
|
794
|
+
const actionMatched = perm.action === "*" || perm.action === action;
|
|
795
|
+
if (!actionMatched) return false;
|
|
796
|
+
if (scope === void 0) return true;
|
|
797
|
+
return isScopeSatisfied(perm.scope, scope);
|
|
798
|
+
}
|
|
799
|
+
const STORAGE_KEY_TOKEN = "kine_auth_token";
|
|
800
|
+
const STORAGE_KEY_USER = "kine_auth_user";
|
|
801
|
+
function buildAuthReturn(state, options, adapter) {
|
|
802
|
+
const user = computed(() => state.user);
|
|
803
|
+
const permissions = computed(() => state.permissions);
|
|
804
|
+
const token = computed(() => state.token);
|
|
805
|
+
const isAuthenticated = computed(() => state.user !== null && state.token !== null);
|
|
806
|
+
function can(resource, action, scope) {
|
|
807
|
+
if (!state.permissions) return false;
|
|
808
|
+
return state.permissions.some((p) => matchesPermission(p, resource, action, scope));
|
|
809
|
+
}
|
|
810
|
+
function persistToStorage() {
|
|
811
|
+
if (!adapter) return;
|
|
812
|
+
adapter.set(STORAGE_KEY_TOKEN, state.token);
|
|
813
|
+
adapter.set(STORAGE_KEY_USER, state.user);
|
|
814
|
+
}
|
|
815
|
+
function clearStorage() {
|
|
816
|
+
if (!adapter) return;
|
|
817
|
+
adapter.remove(STORAGE_KEY_TOKEN);
|
|
818
|
+
adapter.remove(STORAGE_KEY_USER);
|
|
819
|
+
}
|
|
820
|
+
async function login() {
|
|
821
|
+
const result = await options.fetchUser();
|
|
822
|
+
state.user = result.user;
|
|
823
|
+
state.permissions = result.permissions;
|
|
824
|
+
state.token = result.token;
|
|
825
|
+
persistToStorage();
|
|
826
|
+
}
|
|
827
|
+
function loginWith(result) {
|
|
828
|
+
state.user = result.user;
|
|
829
|
+
state.permissions = result.permissions;
|
|
830
|
+
state.token = result.token;
|
|
831
|
+
persistToStorage();
|
|
832
|
+
}
|
|
833
|
+
function logout() {
|
|
834
|
+
state.user = null;
|
|
835
|
+
state.permissions = [];
|
|
836
|
+
state.token = null;
|
|
837
|
+
clearStorage();
|
|
838
|
+
}
|
|
839
|
+
async function restore() {
|
|
840
|
+
if (!adapter) return;
|
|
841
|
+
const storedToken = adapter.get(STORAGE_KEY_TOKEN);
|
|
842
|
+
const storedUser = adapter.get(STORAGE_KEY_USER);
|
|
843
|
+
if (!storedToken || !storedUser) return;
|
|
844
|
+
state.token = storedToken;
|
|
845
|
+
state.user = storedUser;
|
|
846
|
+
try {
|
|
847
|
+
const result = await options.fetchUser();
|
|
848
|
+
state.user = result.user;
|
|
849
|
+
state.permissions = result.permissions;
|
|
850
|
+
state.token = result.token;
|
|
851
|
+
persistToStorage();
|
|
852
|
+
} catch {
|
|
853
|
+
state.user = null;
|
|
854
|
+
state.permissions = [];
|
|
855
|
+
state.token = null;
|
|
856
|
+
clearStorage();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return { user, permissions, token, isAuthenticated, can, login, loginWith, logout, restore };
|
|
860
|
+
}
|
|
861
|
+
function createAuth(app, options) {
|
|
862
|
+
const state = reactive({
|
|
863
|
+
user: null,
|
|
864
|
+
permissions: [],
|
|
865
|
+
token: null
|
|
866
|
+
});
|
|
867
|
+
const storageType = options.storage ?? false;
|
|
868
|
+
const adapter = storageType !== false ? createStorageAdapter(storageType) : null;
|
|
869
|
+
const auth = buildAuthReturn(state, options, adapter);
|
|
870
|
+
if (storageType === "local" && typeof window !== "undefined") {
|
|
871
|
+
const onStorageChange = (e) => {
|
|
872
|
+
if (e.key !== STORAGE_KEY_TOKEN) return;
|
|
873
|
+
if (e.newValue === null) {
|
|
874
|
+
state.user = null;
|
|
875
|
+
state.permissions = [];
|
|
876
|
+
state.token = null;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
window.addEventListener("storage", onStorageChange);
|
|
880
|
+
}
|
|
881
|
+
app.provide(AUTH_INJECT_KEY, { auth, onUnauthorized: options.onUnauthorized });
|
|
882
|
+
return auth;
|
|
883
|
+
}
|
|
884
|
+
function useAuth() {
|
|
885
|
+
const provided = inject(AUTH_INJECT_KEY);
|
|
886
|
+
if (!provided) {
|
|
887
|
+
throw new Error("[useAuth] 未找到 auth 实例,请先在根 App 调用 createAuth()。");
|
|
888
|
+
}
|
|
889
|
+
return provided.auth;
|
|
890
|
+
}
|
|
891
|
+
function createAuthGuard(auth, options) {
|
|
892
|
+
const loginPath = (options == null ? void 0 : options.loginPath) ?? "/login";
|
|
893
|
+
const forbiddenPath = (options == null ? void 0 : options.forbiddenPath) ?? "/403";
|
|
894
|
+
return (to) => {
|
|
895
|
+
const meta = to.meta;
|
|
896
|
+
const requiresAuth = meta.requiresAuth !== false;
|
|
897
|
+
if (requiresAuth && !auth.isAuthenticated.value) {
|
|
898
|
+
if (loginPath === false) return void 0;
|
|
899
|
+
if (to.path === loginPath) return void 0;
|
|
900
|
+
return loginPath;
|
|
901
|
+
}
|
|
902
|
+
if (meta.can) {
|
|
903
|
+
const { resource, action } = meta.can;
|
|
904
|
+
if (!auth.can(resource, action)) {
|
|
905
|
+
if (forbiddenPath === false) return void 0;
|
|
906
|
+
if (to.path === forbiddenPath) return void 0;
|
|
907
|
+
return forbiddenPath;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return void 0;
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function parseBinding(value) {
|
|
914
|
+
if (typeof value === "object") return value;
|
|
915
|
+
const parts = value.split(":");
|
|
916
|
+
const resource = parts[0] ?? "";
|
|
917
|
+
const action = parts[1] ?? "";
|
|
918
|
+
const scope = parts[2];
|
|
919
|
+
return { resource, action, scope };
|
|
920
|
+
}
|
|
921
|
+
function applyVisibility(el, auth, binding) {
|
|
922
|
+
const { resource, action, scope } = parseBinding(binding);
|
|
923
|
+
const allowed = auth.can(resource, action, scope);
|
|
924
|
+
el.style.display = allowed ? "" : "none";
|
|
925
|
+
}
|
|
926
|
+
function createVCan(auth) {
|
|
927
|
+
return {
|
|
928
|
+
mounted(el, binding) {
|
|
929
|
+
applyVisibility(el, auth, binding.value);
|
|
930
|
+
},
|
|
931
|
+
updated(el, binding) {
|
|
932
|
+
applyVisibility(el, auth, binding.value);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function buildCacheKey(method, url, body) {
|
|
937
|
+
const bodyStr = body !== void 0 && body !== null ? stableStringify(body) : "";
|
|
938
|
+
return `${method}:${url}:${bodyStr}`;
|
|
939
|
+
}
|
|
940
|
+
function stableStringify(obj) {
|
|
941
|
+
if (obj === null || obj === void 0) return "";
|
|
942
|
+
if (typeof obj !== "object") return String(obj);
|
|
943
|
+
if (obj instanceof FormData || obj instanceof Blob || obj instanceof File) return "";
|
|
944
|
+
if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(",")}]`;
|
|
945
|
+
const sorted = Object.keys(obj).sort();
|
|
946
|
+
const pairs = sorted.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
|
|
947
|
+
return `{${pairs.join(",")}}`;
|
|
948
|
+
}
|
|
949
|
+
class ControlGate {
|
|
950
|
+
constructor() {
|
|
951
|
+
/** 防抖:取消前一个 timer */
|
|
952
|
+
__publicField(this, "debounceMap", /* @__PURE__ */ new Map());
|
|
953
|
+
/** 节流:记录上次执行时间 */
|
|
954
|
+
__publicField(this, "throttleMap", /* @__PURE__ */ new Map());
|
|
955
|
+
/** 去重:共享同一个 Promise */
|
|
956
|
+
__publicField(this, "deduplicateMap", /* @__PURE__ */ new Map());
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* 通过控制门执行请求。
|
|
960
|
+
* 按配置依次应用防抖、节流、去重策略。
|
|
961
|
+
*/
|
|
962
|
+
async execute(key, fn, policy) {
|
|
963
|
+
if (policy.debounce !== void 0 && policy.debounce > 0) {
|
|
964
|
+
return this.withDebounce(key, fn, policy.debounce);
|
|
965
|
+
}
|
|
966
|
+
if (policy.throttle !== void 0 && policy.throttle > 0) {
|
|
967
|
+
await this.waitThrottle(key, policy.throttle);
|
|
968
|
+
}
|
|
969
|
+
if (policy.deduplicate) {
|
|
970
|
+
return this.withDeduplicate(key, fn);
|
|
971
|
+
}
|
|
972
|
+
return fn();
|
|
973
|
+
}
|
|
974
|
+
/** 清理所有挂起的 timer 和状态 */
|
|
975
|
+
dispose() {
|
|
976
|
+
for (const entry of this.debounceMap.values()) {
|
|
977
|
+
clearTimeout(entry.timer);
|
|
978
|
+
entry.reject(new Error("ControlGate 已销毁"));
|
|
979
|
+
}
|
|
980
|
+
this.debounceMap.clear();
|
|
981
|
+
this.throttleMap.clear();
|
|
982
|
+
this.deduplicateMap.clear();
|
|
983
|
+
}
|
|
984
|
+
// ── 防抖 ──────────────────────────────────────────────────────────────────
|
|
985
|
+
withDebounce(key, fn, delay) {
|
|
986
|
+
const prev = this.debounceMap.get(key);
|
|
987
|
+
if (prev) {
|
|
988
|
+
clearTimeout(prev.timer);
|
|
989
|
+
prev.reject(new Error("请求已被防抖取消"));
|
|
990
|
+
}
|
|
991
|
+
return new Promise((resolve, reject) => {
|
|
992
|
+
const timer = setTimeout(async () => {
|
|
993
|
+
this.debounceMap.delete(key);
|
|
994
|
+
try {
|
|
995
|
+
resolve(await fn());
|
|
996
|
+
} catch (e) {
|
|
997
|
+
reject(e);
|
|
998
|
+
}
|
|
999
|
+
}, delay);
|
|
1000
|
+
this.debounceMap.set(key, { timer, reject });
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
// ── 节流 ──────────────────────────────────────────────────────────────────
|
|
1004
|
+
waitThrottle(key, interval) {
|
|
1005
|
+
const lastTime = this.throttleMap.get(key) ?? 0;
|
|
1006
|
+
const now = Date.now();
|
|
1007
|
+
const elapsed = now - lastTime;
|
|
1008
|
+
if (elapsed >= interval) {
|
|
1009
|
+
this.throttleMap.set(key, now);
|
|
1010
|
+
return Promise.resolve();
|
|
1011
|
+
}
|
|
1012
|
+
const waitTime = interval - elapsed;
|
|
1013
|
+
return new Promise((resolve) => {
|
|
1014
|
+
setTimeout(() => {
|
|
1015
|
+
this.throttleMap.set(key, Date.now());
|
|
1016
|
+
resolve();
|
|
1017
|
+
}, waitTime);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
// ── 去重 ──────────────────────────────────────────────────────────────────
|
|
1021
|
+
withDeduplicate(key, fn) {
|
|
1022
|
+
const existing = this.deduplicateMap.get(key);
|
|
1023
|
+
if (existing) return existing;
|
|
1024
|
+
const promise = fn().finally(() => {
|
|
1025
|
+
this.deduplicateMap.delete(key);
|
|
1026
|
+
});
|
|
1027
|
+
this.deduplicateMap.set(key, promise);
|
|
1028
|
+
return promise;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function headersToRecord(headers) {
|
|
1032
|
+
const record = {};
|
|
1033
|
+
headers.forEach((value, key) => {
|
|
1034
|
+
record[key.toLowerCase()] = value;
|
|
1035
|
+
});
|
|
1036
|
+
return record;
|
|
1037
|
+
}
|
|
1038
|
+
function wrapFetchResponse(response) {
|
|
1039
|
+
return {
|
|
1040
|
+
status: response.status,
|
|
1041
|
+
statusText: response.statusText,
|
|
1042
|
+
headers: headersToRecord(response.headers),
|
|
1043
|
+
body: response.body,
|
|
1044
|
+
text: () => response.text(),
|
|
1045
|
+
json: () => response.json(),
|
|
1046
|
+
blob: () => response.blob()
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
class FetchTransport {
|
|
1050
|
+
async send(request) {
|
|
1051
|
+
const controller = new AbortController();
|
|
1052
|
+
if (request.signal) {
|
|
1053
|
+
if (request.signal.aborted) {
|
|
1054
|
+
controller.abort(request.signal.reason);
|
|
1055
|
+
} else {
|
|
1056
|
+
request.signal.addEventListener("abort", () => {
|
|
1057
|
+
controller.abort(request.signal.reason);
|
|
1058
|
+
}, { once: true });
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
let timeoutId;
|
|
1062
|
+
if (request.timeout !== void 0 && request.timeout > 0) {
|
|
1063
|
+
timeoutId = setTimeout(() => controller.abort("timeout"), request.timeout);
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const response = await fetch(request.url, {
|
|
1067
|
+
method: request.method,
|
|
1068
|
+
headers: request.headers,
|
|
1069
|
+
body: request.body,
|
|
1070
|
+
signal: controller.signal
|
|
1071
|
+
});
|
|
1072
|
+
return wrapFetchResponse(response);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1075
|
+
if (controller.signal.reason === "timeout") {
|
|
1076
|
+
throw { type: "timeout" };
|
|
1077
|
+
}
|
|
1078
|
+
throw { type: "aborted" };
|
|
1079
|
+
}
|
|
1080
|
+
throw error;
|
|
1081
|
+
} finally {
|
|
1082
|
+
if (timeoutId !== void 0) {
|
|
1083
|
+
clearTimeout(timeoutId);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
class BusinessError extends Error {
|
|
1089
|
+
constructor(message) {
|
|
1090
|
+
super(message);
|
|
1091
|
+
__publicField(this, "name", "BusinessError");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
class NetworkRequestError extends Error {
|
|
1095
|
+
constructor(detail) {
|
|
1096
|
+
super(networkErrorMessage(detail));
|
|
1097
|
+
__publicField(this, "name", "NetworkRequestError");
|
|
1098
|
+
__publicField(this, "detail");
|
|
1099
|
+
this.detail = detail;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function networkErrorMessage(e) {
|
|
1103
|
+
switch (e.type) {
|
|
1104
|
+
case "invalidURL":
|
|
1105
|
+
return `无效的 URL: ${e.url}`;
|
|
1106
|
+
case "timeout":
|
|
1107
|
+
return "请求超时";
|
|
1108
|
+
case "httpError":
|
|
1109
|
+
return `HTTP ${e.status} ${e.statusText}`;
|
|
1110
|
+
case "decodingFailed":
|
|
1111
|
+
return `解码失败: ${e.reason}`;
|
|
1112
|
+
case "aborted":
|
|
1113
|
+
return "请求已取消";
|
|
1114
|
+
case "unknown":
|
|
1115
|
+
return `未知错误: ${String(e.error)}`;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const cacheStore = /* @__PURE__ */ new Map();
|
|
1119
|
+
function containsFile(obj) {
|
|
1120
|
+
if (obj instanceof File || obj instanceof Blob || obj instanceof FormData) return true;
|
|
1121
|
+
if (typeof obj === "object" && obj !== null) {
|
|
1122
|
+
return Object.values(obj).some(containsFile);
|
|
1123
|
+
}
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
function toFormData(obj) {
|
|
1127
|
+
const fd = new FormData();
|
|
1128
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1129
|
+
if (value instanceof File || value instanceof Blob) {
|
|
1130
|
+
fd.append(key, value);
|
|
1131
|
+
} else if (Array.isArray(value)) {
|
|
1132
|
+
value.forEach((item) => {
|
|
1133
|
+
if (item instanceof File || item instanceof Blob) {
|
|
1134
|
+
fd.append(key, item);
|
|
1135
|
+
} else {
|
|
1136
|
+
fd.append(key, String(item));
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
} else if (value !== null && value !== void 0) {
|
|
1140
|
+
fd.append(key, String(value));
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return fd;
|
|
1144
|
+
}
|
|
1145
|
+
function toQueryString(params) {
|
|
1146
|
+
const parts = [];
|
|
1147
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1148
|
+
if (value === null || value === void 0) continue;
|
|
1149
|
+
if (Array.isArray(value)) {
|
|
1150
|
+
value.forEach((v) => parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`));
|
|
1151
|
+
} else {
|
|
1152
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return parts.join("&");
|
|
1156
|
+
}
|
|
1157
|
+
async function unwrapResponse(response) {
|
|
1158
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1159
|
+
throw new NetworkRequestError({
|
|
1160
|
+
type: "httpError",
|
|
1161
|
+
status: response.status,
|
|
1162
|
+
statusText: response.statusText
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
const contentType = response.headers["content-type"] ?? "";
|
|
1166
|
+
if (contentType.includes("application/json")) {
|
|
1167
|
+
const json = await response.json();
|
|
1168
|
+
if (isWrappedResponse$1(json)) {
|
|
1169
|
+
if (!json.success) {
|
|
1170
|
+
throw new BusinessError(json.message || "请求失败");
|
|
1171
|
+
}
|
|
1172
|
+
return json.data;
|
|
1173
|
+
}
|
|
1174
|
+
return json;
|
|
1175
|
+
}
|
|
1176
|
+
const text = await response.text();
|
|
1177
|
+
return text;
|
|
1178
|
+
}
|
|
1179
|
+
function isWrappedResponse$1(value) {
|
|
1180
|
+
return typeof value === "object" && value !== null && "success" in value && "data" in value && typeof value.success === "boolean";
|
|
1181
|
+
}
|
|
1182
|
+
async function withRetry(fn, policy, signal) {
|
|
1183
|
+
if (policy.type === "none") return fn();
|
|
1184
|
+
let lastError;
|
|
1185
|
+
const maxAttempts = policy.maxAttempts;
|
|
1186
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1187
|
+
try {
|
|
1188
|
+
return await fn();
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
lastError = e;
|
|
1191
|
+
if (signal == null ? void 0 : signal.aborted) throw e;
|
|
1192
|
+
if (e instanceof BusinessError) throw e;
|
|
1193
|
+
if (attempt < maxAttempts) {
|
|
1194
|
+
const delay = policy.type === "fixed" ? policy.delay : Math.min(policy.initialDelay * Math.pow(policy.multiplier, attempt - 1), policy.maxDelay);
|
|
1195
|
+
await new Promise((resolve, reject) => {
|
|
1196
|
+
const timer = setTimeout(resolve, delay);
|
|
1197
|
+
signal == null ? void 0 : signal.addEventListener("abort", () => {
|
|
1198
|
+
clearTimeout(timer);
|
|
1199
|
+
reject(new NetworkRequestError({ type: "aborted" }));
|
|
1200
|
+
}, { once: true });
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
throw lastError;
|
|
1206
|
+
}
|
|
1207
|
+
class RequestBuilder {
|
|
1208
|
+
constructor(context, method, url, body) {
|
|
1209
|
+
__publicField(this, "method");
|
|
1210
|
+
__publicField(this, "url");
|
|
1211
|
+
__publicField(this, "body");
|
|
1212
|
+
__publicField(this, "config", {});
|
|
1213
|
+
__publicField(this, "context");
|
|
1214
|
+
__publicField(this, "customHeaders", {});
|
|
1215
|
+
this.context = context;
|
|
1216
|
+
this.method = method;
|
|
1217
|
+
this.url = url;
|
|
1218
|
+
this.body = body;
|
|
1219
|
+
}
|
|
1220
|
+
// ── 链式 API ────────────────────────────────────────────────────────────
|
|
1221
|
+
cache(policy) {
|
|
1222
|
+
this.config.cache = policy;
|
|
1223
|
+
return this;
|
|
1224
|
+
}
|
|
1225
|
+
retry(policy) {
|
|
1226
|
+
this.config.retry = policy;
|
|
1227
|
+
return this;
|
|
1228
|
+
}
|
|
1229
|
+
debounce(delay) {
|
|
1230
|
+
this.config.control = { ...this.config.control, debounce: delay };
|
|
1231
|
+
return this;
|
|
1232
|
+
}
|
|
1233
|
+
throttle(interval) {
|
|
1234
|
+
this.config.control = { ...this.config.control, throttle: interval };
|
|
1235
|
+
return this;
|
|
1236
|
+
}
|
|
1237
|
+
deduplicate(enabled = true) {
|
|
1238
|
+
this.config.control = { ...this.config.control, deduplicate: enabled };
|
|
1239
|
+
return this;
|
|
1240
|
+
}
|
|
1241
|
+
timeout(ms) {
|
|
1242
|
+
this.config.timeout = ms;
|
|
1243
|
+
return this;
|
|
1244
|
+
}
|
|
1245
|
+
totalTimeout(ms) {
|
|
1246
|
+
this.config.totalTimeout = ms;
|
|
1247
|
+
return this;
|
|
1248
|
+
}
|
|
1249
|
+
lifecycle(lc) {
|
|
1250
|
+
this.config.lifecycle = lc;
|
|
1251
|
+
return this;
|
|
1252
|
+
}
|
|
1253
|
+
priority(p) {
|
|
1254
|
+
this.config.control = { ...this.config.control, priority: p };
|
|
1255
|
+
return this;
|
|
1256
|
+
}
|
|
1257
|
+
headers(h2) {
|
|
1258
|
+
Object.assign(this.customHeaders, h2);
|
|
1259
|
+
return this;
|
|
1260
|
+
}
|
|
1261
|
+
// ── 执行 ────────────────────────────────────────────────────────────────
|
|
1262
|
+
async execute() {
|
|
1263
|
+
const { transport, controlGate, baseURL, defaultHeaders, getToken, onUnauthorized, responseInterceptor, registerAbort } = this.context;
|
|
1264
|
+
let fullURL = this.url.startsWith("http") ? this.url : `${baseURL}${this.url}`;
|
|
1265
|
+
const abortController = new AbortController();
|
|
1266
|
+
registerAbort == null ? void 0 : registerAbort(abortController);
|
|
1267
|
+
let totalTimeoutId;
|
|
1268
|
+
if (this.config.totalTimeout) {
|
|
1269
|
+
totalTimeoutId = setTimeout(() => abortController.abort("totalTimeout"), this.config.totalTimeout);
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
const reqHeaders = { ...defaultHeaders, ...this.customHeaders };
|
|
1273
|
+
const token = getToken == null ? void 0 : getToken();
|
|
1274
|
+
if (token) {
|
|
1275
|
+
reqHeaders["Authorization"] = `Bearer ${token}`;
|
|
1276
|
+
}
|
|
1277
|
+
let reqBody = null;
|
|
1278
|
+
if (this.method === "GET" && this.body && typeof this.body === "object") {
|
|
1279
|
+
const qs = toQueryString(this.body);
|
|
1280
|
+
if (qs) {
|
|
1281
|
+
fullURL += (fullURL.includes("?") ? "&" : "?") + qs;
|
|
1282
|
+
}
|
|
1283
|
+
} else if (this.body !== void 0 && this.body !== null) {
|
|
1284
|
+
if (this.body instanceof FormData) {
|
|
1285
|
+
reqBody = this.body;
|
|
1286
|
+
} else if (containsFile(this.body)) {
|
|
1287
|
+
reqBody = toFormData(this.body);
|
|
1288
|
+
} else {
|
|
1289
|
+
reqBody = JSON.stringify(this.body);
|
|
1290
|
+
reqHeaders["Content-Type"] = "application/json";
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const cacheKey = buildCacheKey(this.method, fullURL, this.body);
|
|
1294
|
+
const cachePolicy = this.config.cache ?? { type: "none" };
|
|
1295
|
+
if (cachePolicy.type === "cacheFirst" || cachePolicy.type === "staleWhileRevalidate") {
|
|
1296
|
+
const cached = cacheStore.get(cacheKey);
|
|
1297
|
+
if (cached) {
|
|
1298
|
+
const age = (Date.now() - cached.timestamp) / 1e3;
|
|
1299
|
+
if (age < cachePolicy.maxAge) {
|
|
1300
|
+
if (cachePolicy.type === "staleWhileRevalidate") {
|
|
1301
|
+
this.executeTransport(transport, fullURL, reqHeaders, reqBody, abortController.signal).then((data) => {
|
|
1302
|
+
cacheStore.set(cacheKey, { data, timestamp: Date.now() });
|
|
1303
|
+
}).catch(() => {
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
return cached.data;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const doRequest = () => this.executeTransport(transport, fullURL, reqHeaders, reqBody, abortController.signal);
|
|
1311
|
+
const controlPolicy = this.config.control ?? {};
|
|
1312
|
+
const retryPolicy = this.config.retry ?? { type: "none" };
|
|
1313
|
+
const result = await controlGate.execute(cacheKey, async () => {
|
|
1314
|
+
return withRetry(doRequest, retryPolicy, abortController.signal);
|
|
1315
|
+
}, controlPolicy);
|
|
1316
|
+
if (cachePolicy.type !== "none") {
|
|
1317
|
+
cacheStore.set(cacheKey, { data: result, timestamp: Date.now() });
|
|
1318
|
+
}
|
|
1319
|
+
const finalResult = responseInterceptor ? responseInterceptor(result) : result;
|
|
1320
|
+
return finalResult;
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
if (error instanceof NetworkRequestError && error.detail.type === "httpError" && error.detail.status === 401) {
|
|
1323
|
+
onUnauthorized == null ? void 0 : onUnauthorized();
|
|
1324
|
+
}
|
|
1325
|
+
throw error;
|
|
1326
|
+
} finally {
|
|
1327
|
+
if (totalTimeoutId !== void 0) clearTimeout(totalTimeoutId);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// ── 内部传输 ──────────────────────────────────────────────────────────────
|
|
1331
|
+
async executeTransport(transport, url, headers, body, signal) {
|
|
1332
|
+
const response = await transport.send({
|
|
1333
|
+
url,
|
|
1334
|
+
method: this.method,
|
|
1335
|
+
headers,
|
|
1336
|
+
body,
|
|
1337
|
+
signal,
|
|
1338
|
+
timeout: this.config.timeout
|
|
1339
|
+
});
|
|
1340
|
+
return unwrapResponse(response);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function createRequest(options = {}) {
|
|
1344
|
+
const transport = options.transport ?? new FetchTransport();
|
|
1345
|
+
const controlGate = new ControlGate();
|
|
1346
|
+
const abortControllers = /* @__PURE__ */ new Set();
|
|
1347
|
+
const context = {
|
|
1348
|
+
transport,
|
|
1349
|
+
controlGate,
|
|
1350
|
+
baseURL: options.baseURL ?? "",
|
|
1351
|
+
defaultHeaders: options.headers ?? {},
|
|
1352
|
+
getToken: options.getToken,
|
|
1353
|
+
onUnauthorized: options.onUnauthorized,
|
|
1354
|
+
responseInterceptor: options.responseInterceptor,
|
|
1355
|
+
registerAbort: (controller) => {
|
|
1356
|
+
abortControllers.add(controller);
|
|
1357
|
+
const originalAbort = controller.abort.bind(controller);
|
|
1358
|
+
controller.abort = (reason) => {
|
|
1359
|
+
abortControllers.delete(controller);
|
|
1360
|
+
return originalAbort(reason);
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
function createBuilder(method, url, body) {
|
|
1365
|
+
var _a, _b, _c;
|
|
1366
|
+
const builder = new RequestBuilder(context, method, url, body);
|
|
1367
|
+
const dc = options.defaultConfig;
|
|
1368
|
+
if (dc == null ? void 0 : dc.cache) builder.cache(dc.cache);
|
|
1369
|
+
if (dc == null ? void 0 : dc.retry) builder.retry(dc.retry);
|
|
1370
|
+
if (dc == null ? void 0 : dc.timeout) builder.timeout(dc.timeout);
|
|
1371
|
+
if (dc == null ? void 0 : dc.totalTimeout) builder.totalTimeout(dc.totalTimeout);
|
|
1372
|
+
if (dc == null ? void 0 : dc.lifecycle) builder.lifecycle(dc.lifecycle);
|
|
1373
|
+
if ((_a = dc == null ? void 0 : dc.control) == null ? void 0 : _a.debounce) builder.debounce(dc.control.debounce);
|
|
1374
|
+
if ((_b = dc == null ? void 0 : dc.control) == null ? void 0 : _b.throttle) builder.throttle(dc.control.throttle);
|
|
1375
|
+
if ((_c = dc == null ? void 0 : dc.control) == null ? void 0 : _c.deduplicate) builder.deduplicate(dc.control.deduplicate);
|
|
1376
|
+
return builder;
|
|
1377
|
+
}
|
|
1378
|
+
const client = {
|
|
1379
|
+
send: (method, url, body) => createBuilder(method, url, body).execute(),
|
|
1380
|
+
get: (url, params) => createBuilder("GET", url, params).execute(),
|
|
1381
|
+
post: (url, body) => createBuilder("POST", url, body).execute(),
|
|
1382
|
+
put: (url, body) => createBuilder("PUT", url, body).execute(),
|
|
1383
|
+
patch: (url, body) => createBuilder("PATCH", url, body).execute(),
|
|
1384
|
+
delete: (url, body) => createBuilder("DELETE", url, body).execute(),
|
|
1385
|
+
request: (method, url, body) => createBuilder(method, url, body),
|
|
1386
|
+
registerAbortController: (controller) => {
|
|
1387
|
+
abortControllers.add(controller);
|
|
1388
|
+
},
|
|
1389
|
+
abortAll: () => {
|
|
1390
|
+
for (const controller of abortControllers) {
|
|
1391
|
+
controller.abort("客户端取消所有请求");
|
|
1392
|
+
}
|
|
1393
|
+
abortControllers.clear();
|
|
1394
|
+
},
|
|
1395
|
+
dispose: () => {
|
|
1396
|
+
client.abortAll();
|
|
1397
|
+
controlGate.dispose();
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
return client;
|
|
1401
|
+
}
|
|
1402
|
+
function createDefaultRequest(options = {}) {
|
|
1403
|
+
return createRequest({
|
|
1404
|
+
...options,
|
|
1405
|
+
defaultConfig: {
|
|
1406
|
+
retry: { type: "exponential", maxAttempts: 3, initialDelay: 300, multiplier: 2, maxDelay: 5e3 },
|
|
1407
|
+
timeout: 3e4,
|
|
1408
|
+
...options.defaultConfig
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
function createStrictRequest(options = {}) {
|
|
1413
|
+
return createRequest({
|
|
1414
|
+
...options,
|
|
1415
|
+
defaultConfig: {
|
|
1416
|
+
retry: { type: "none" },
|
|
1417
|
+
timeout: 1e4,
|
|
1418
|
+
...options.defaultConfig
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
function createPermissiveRequest(options = {}) {
|
|
1423
|
+
return createRequest({
|
|
1424
|
+
...options,
|
|
1425
|
+
defaultConfig: {
|
|
1426
|
+
retry: { type: "exponential", maxAttempts: 5, initialDelay: 500, multiplier: 2, maxDelay: 3e4 },
|
|
1427
|
+
timeout: 6e4,
|
|
1428
|
+
totalTimeout: 12e4,
|
|
1429
|
+
...options.defaultConfig
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
const ERROR_HANDLER_INJECT_KEY = Symbol("kine-design:error-handler");
|
|
1434
|
+
function tryGetMessageApi() {
|
|
1435
|
+
try {
|
|
1436
|
+
const { useMessage } = require("@kine-design/ui");
|
|
1437
|
+
return useMessage();
|
|
1438
|
+
} catch {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
let messageApi = void 0;
|
|
1443
|
+
function getMessageApi() {
|
|
1444
|
+
if (messageApi === void 0) {
|
|
1445
|
+
messageApi = tryGetMessageApi();
|
|
1446
|
+
}
|
|
1447
|
+
return messageApi;
|
|
1448
|
+
}
|
|
1449
|
+
const defaultFeedbackHandler = {
|
|
1450
|
+
showSuccess(message) {
|
|
1451
|
+
const api = getMessageApi();
|
|
1452
|
+
if (api) {
|
|
1453
|
+
api.success(message);
|
|
1454
|
+
} else {
|
|
1455
|
+
console.log(`[success] ${message}`);
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
showError(message) {
|
|
1459
|
+
const api = getMessageApi();
|
|
1460
|
+
if (api) {
|
|
1461
|
+
api.error(message);
|
|
1462
|
+
} else {
|
|
1463
|
+
console.error(`[error] ${message}`);
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
showWarning(message) {
|
|
1467
|
+
const api = getMessageApi();
|
|
1468
|
+
if (api) {
|
|
1469
|
+
api.warning(message);
|
|
1470
|
+
} else {
|
|
1471
|
+
console.warn(`[warning] ${message}`);
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
handleAuthenticationFailure() {
|
|
1475
|
+
console.warn("[auth] 认证失败,请重新登录");
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
function dispatchError(error, options) {
|
|
1479
|
+
var _a, _b;
|
|
1480
|
+
const feedback = options.feedbackHandler ?? defaultFeedbackHandler;
|
|
1481
|
+
if (error instanceof BusinessError) {
|
|
1482
|
+
(_a = options.onBusinessError) == null ? void 0 : _a.call(options, error);
|
|
1483
|
+
feedback.showError(error.message);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (error instanceof NetworkRequestError) {
|
|
1487
|
+
const detail = error.detail;
|
|
1488
|
+
(_b = options.onNetworkError) == null ? void 0 : _b.call(options, detail);
|
|
1489
|
+
switch (detail.type) {
|
|
1490
|
+
case "httpError":
|
|
1491
|
+
if (detail.status === 401) {
|
|
1492
|
+
feedback.handleAuthenticationFailure();
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
if (detail.status === 403) {
|
|
1496
|
+
feedback.showWarning("权限不足,无法执行此操作");
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
feedback.showError(`请求失败 (${detail.status})`);
|
|
1500
|
+
return;
|
|
1501
|
+
case "timeout":
|
|
1502
|
+
feedback.showWarning("请求超时,请稍后重试");
|
|
1503
|
+
return;
|
|
1504
|
+
case "aborted":
|
|
1505
|
+
return;
|
|
1506
|
+
case "invalidURL":
|
|
1507
|
+
feedback.showError(`无效的请求地址: ${detail.url}`);
|
|
1508
|
+
return;
|
|
1509
|
+
case "decodingFailed":
|
|
1510
|
+
feedback.showError("数据解析失败");
|
|
1511
|
+
return;
|
|
1512
|
+
case "unknown":
|
|
1513
|
+
feedback.showError("网络异常,请检查网络连接");
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1518
|
+
feedback.showError(message || "发生了未知错误");
|
|
1519
|
+
}
|
|
1520
|
+
function createErrorHandler(app, options = {}) {
|
|
1521
|
+
app.config.errorHandler = (err) => {
|
|
1522
|
+
dispatchError(err, options);
|
|
1523
|
+
};
|
|
1524
|
+
app.provide(ERROR_HANDLER_INJECT_KEY, options);
|
|
1525
|
+
}
|
|
1526
|
+
const REQUEST_CLIENT_KEY = Symbol("kine-design:request");
|
|
1527
|
+
function useRequestClient() {
|
|
1528
|
+
const client = inject(REQUEST_CLIENT_KEY);
|
|
1529
|
+
if (!client) {
|
|
1530
|
+
throw new Error("[useRequestClient] 未找到 RequestClient,请在 createCrudApp 中配置 request 选项。");
|
|
1531
|
+
}
|
|
1532
|
+
return client;
|
|
1533
|
+
}
|
|
1534
|
+
function createQueryClient() {
|
|
1535
|
+
return new QueryClient({
|
|
1536
|
+
defaultOptions: {
|
|
1537
|
+
queries: {
|
|
1538
|
+
// 窗口重新聚焦时不自动重新请求,业务层按需覆盖
|
|
1539
|
+
refetchOnWindowFocus: false,
|
|
1540
|
+
// 失败后重试 1 次
|
|
1541
|
+
retry: 1,
|
|
1542
|
+
// 数据保鲜时间 1 分钟
|
|
1543
|
+
staleTime: 1e3 * 60
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
function setupCrud(app, options) {
|
|
1549
|
+
const queryClient = (options == null ? void 0 : options.queryClient) ?? createQueryClient();
|
|
1550
|
+
app.use(VueQueryPlugin, {
|
|
1551
|
+
queryClient,
|
|
1552
|
+
...options == null ? void 0 : options.vueQueryOptions
|
|
1553
|
+
});
|
|
1554
|
+
installPinia(app);
|
|
1555
|
+
}
|
|
1556
|
+
function installPinia(app) {
|
|
1557
|
+
const provides = app._context.provides;
|
|
1558
|
+
const hasPinia = Object.getOwnPropertySymbols(provides).some((sym) => {
|
|
1559
|
+
const val = provides[sym];
|
|
1560
|
+
return val !== null && typeof val === "object" && "_a" in val;
|
|
1561
|
+
});
|
|
1562
|
+
if (!hasPinia) {
|
|
1563
|
+
app.use(createPinia());
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
function createCrudApp(rootComponent, rootPropsOrOptions, crudOptions) {
|
|
1567
|
+
if (rootPropsOrOptions && isCrudAppOptions(rootPropsOrOptions)) {
|
|
1568
|
+
return createCrudAppWithOptions(rootComponent, rootPropsOrOptions);
|
|
1569
|
+
}
|
|
1570
|
+
const app = createApp(rootComponent, rootPropsOrOptions);
|
|
1571
|
+
setupCrud(app, crudOptions);
|
|
1572
|
+
return app;
|
|
1573
|
+
}
|
|
1574
|
+
function isCrudAppOptions(obj) {
|
|
1575
|
+
return "router" in obj || "auth" in obj || "request" in obj || "error" in obj || "query" in obj;
|
|
1576
|
+
}
|
|
1577
|
+
function createCrudAppWithOptions(rootComponent, options) {
|
|
1578
|
+
var _a;
|
|
1579
|
+
const app = createApp(rootComponent);
|
|
1580
|
+
let auth;
|
|
1581
|
+
if (options.auth) {
|
|
1582
|
+
auth = createAuth(app, {
|
|
1583
|
+
fetchUser: options.auth.fetchUser,
|
|
1584
|
+
onUnauthorized: options.auth.onUnauthorized,
|
|
1585
|
+
storage: options.auth.storage
|
|
1586
|
+
});
|
|
1587
|
+
app.directive("can", createVCan(auth));
|
|
1588
|
+
if (options.router) {
|
|
1589
|
+
const guard = createAuthGuard(auth, { loginPath: options.auth.loginPath ?? "/login" });
|
|
1590
|
+
options.router.beforeEach(guard);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (options.router) {
|
|
1594
|
+
app.use(options.router);
|
|
1595
|
+
}
|
|
1596
|
+
setupCrud(app, options.query);
|
|
1597
|
+
if (options.request) {
|
|
1598
|
+
const requestOptions = { ...options.request };
|
|
1599
|
+
if (auth && !requestOptions.getToken) {
|
|
1600
|
+
requestOptions.getToken = () => auth.token.value;
|
|
1601
|
+
}
|
|
1602
|
+
if (auth && !requestOptions.onUnauthorized) {
|
|
1603
|
+
requestOptions.onUnauthorized = (_a = options.auth) == null ? void 0 : _a.onUnauthorized;
|
|
1604
|
+
}
|
|
1605
|
+
const client = createRequest(requestOptions);
|
|
1606
|
+
app.provide(REQUEST_CLIENT_KEY, client);
|
|
1607
|
+
}
|
|
1608
|
+
if (options.error) {
|
|
1609
|
+
createErrorHandler(app, options.error);
|
|
1610
|
+
}
|
|
1611
|
+
const enhancedApp = Object.create(app);
|
|
1612
|
+
enhancedApp.mount = (rootContainer) => {
|
|
1613
|
+
const doMount = () => app.mount(rootContainer);
|
|
1614
|
+
if (auth) {
|
|
1615
|
+
auth.restore().finally(doMount);
|
|
1616
|
+
} else {
|
|
1617
|
+
doMount();
|
|
1618
|
+
}
|
|
1619
|
+
return app;
|
|
1620
|
+
};
|
|
1621
|
+
return enhancedApp;
|
|
1622
|
+
}
|
|
1623
|
+
const DEFAULT_CONFIG = { key: "key", label: "label", children: "children" };
|
|
1624
|
+
function buildNodes(data, config, nodeMap, defaultExpandAll = false, parent) {
|
|
1625
|
+
return data.map((item) => {
|
|
1626
|
+
const key = item[config.key];
|
|
1627
|
+
const childrenKey = config.children;
|
|
1628
|
+
const node = {
|
|
1629
|
+
...item,
|
|
1630
|
+
expand: defaultExpandAll,
|
|
1631
|
+
isActive: false,
|
|
1632
|
+
checked: false,
|
|
1633
|
+
indeterminate: false,
|
|
1634
|
+
parent,
|
|
1635
|
+
isRoot: !parent
|
|
1636
|
+
};
|
|
1637
|
+
nodeMap.set(key, node);
|
|
1638
|
+
const rawChildren = item[childrenKey];
|
|
1639
|
+
if (Array.isArray(rawChildren) && rawChildren.length > 0) {
|
|
1640
|
+
node.children = buildNodes(
|
|
1641
|
+
rawChildren,
|
|
1642
|
+
config,
|
|
1643
|
+
nodeMap,
|
|
1644
|
+
defaultExpandAll,
|
|
1645
|
+
node
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
return node;
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
function useMenu(props2, emit) {
|
|
1652
|
+
const config = { ...DEFAULT_CONFIG, ...props2.config };
|
|
1653
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1654
|
+
const nodesRef = shallowRef([]);
|
|
1655
|
+
const initNodes = (data = []) => {
|
|
1656
|
+
var _a;
|
|
1657
|
+
nodeMap.clear();
|
|
1658
|
+
nodesRef.value = buildNodes(data, config, nodeMap, props2.defaultExpandAll ?? false, null);
|
|
1659
|
+
if ((_a = props2.checkedKeys) == null ? void 0 : _a.length) {
|
|
1660
|
+
props2.checkedKeys.forEach((key) => {
|
|
1661
|
+
const node = nodeMap.get(key);
|
|
1662
|
+
if (node) {
|
|
1663
|
+
node.checked = true;
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
triggerRef(nodesRef);
|
|
1668
|
+
};
|
|
1669
|
+
initNodes(props2.data ?? []);
|
|
1670
|
+
if (props2.active !== void 0) {
|
|
1671
|
+
const activeNode = nodeMap.get(props2.active);
|
|
1672
|
+
if (activeNode) {
|
|
1673
|
+
activeNode.isActive = true;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const toggleExpand = (node) => {
|
|
1677
|
+
node.expand = !node.expand;
|
|
1678
|
+
triggerRef(nodesRef);
|
|
1679
|
+
};
|
|
1680
|
+
const setActive = (node) => {
|
|
1681
|
+
nodeMap.forEach((n) => {
|
|
1682
|
+
n.isActive = false;
|
|
1683
|
+
});
|
|
1684
|
+
node.isActive = true;
|
|
1685
|
+
triggerRef(nodesRef);
|
|
1686
|
+
};
|
|
1687
|
+
const toggleChecked = (node, checked) => {
|
|
1688
|
+
node.checked = checked;
|
|
1689
|
+
node.indeterminate = false;
|
|
1690
|
+
triggerRef(nodesRef);
|
|
1691
|
+
};
|
|
1692
|
+
const getCheckedKeys = () => {
|
|
1693
|
+
const keys = [];
|
|
1694
|
+
nodeMap.forEach((node, key) => {
|
|
1695
|
+
if (node.checked && !node.indeterminate) {
|
|
1696
|
+
keys.push(key);
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
return keys;
|
|
1700
|
+
};
|
|
1701
|
+
const getNodesByKeys = (keys) => {
|
|
1702
|
+
return keys.map((key) => nodeMap.get(key)).filter((n) => n !== void 0);
|
|
1703
|
+
};
|
|
1704
|
+
return {
|
|
1705
|
+
nodesRef,
|
|
1706
|
+
nodeMap,
|
|
1707
|
+
initNodes,
|
|
1708
|
+
toggleExpand,
|
|
1709
|
+
setActive,
|
|
1710
|
+
toggleChecked,
|
|
1711
|
+
getCheckedKeys,
|
|
1712
|
+
getNodesByKeys,
|
|
1713
|
+
config
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
const KMenuItem = /* @__PURE__ */ defineComponent({
|
|
1717
|
+
name: "KMenuItem",
|
|
1718
|
+
inheritAttrs: false,
|
|
1719
|
+
props: {
|
|
1720
|
+
data: {
|
|
1721
|
+
type: Array,
|
|
1722
|
+
default: () => []
|
|
1723
|
+
},
|
|
1724
|
+
config: {
|
|
1725
|
+
type: Object,
|
|
1726
|
+
required: true
|
|
1727
|
+
},
|
|
1728
|
+
checkbox: {
|
|
1729
|
+
type: Boolean,
|
|
1730
|
+
default: false
|
|
1731
|
+
},
|
|
1732
|
+
root: {
|
|
1733
|
+
type: Boolean,
|
|
1734
|
+
default: false
|
|
1735
|
+
},
|
|
1736
|
+
getNodesByKeys: {
|
|
1737
|
+
type: Function,
|
|
1738
|
+
required: true
|
|
1739
|
+
},
|
|
1740
|
+
handleExpand: {
|
|
1741
|
+
type: Function,
|
|
1742
|
+
required: true
|
|
1743
|
+
},
|
|
1744
|
+
handleCheck: {
|
|
1745
|
+
type: Function,
|
|
1746
|
+
required: true
|
|
1747
|
+
},
|
|
1748
|
+
handleItemClick: {
|
|
1749
|
+
type: Function,
|
|
1750
|
+
required: true
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
setup(_props, {
|
|
1754
|
+
slots
|
|
1755
|
+
}) {
|
|
1756
|
+
const props2 = _props;
|
|
1757
|
+
const clickEvent = (e, d) => {
|
|
1758
|
+
props2.handleItemClick(d, e);
|
|
1759
|
+
};
|
|
1760
|
+
const expandEvent = (e, d) => {
|
|
1761
|
+
props2.handleExpand(d, e);
|
|
1762
|
+
e.stopPropagation();
|
|
1763
|
+
};
|
|
1764
|
+
const checkEvent = (e, d) => {
|
|
1765
|
+
const checked = e.target.checked;
|
|
1766
|
+
props2.handleCheck(d, checked);
|
|
1767
|
+
};
|
|
1768
|
+
return () => {
|
|
1769
|
+
const {
|
|
1770
|
+
label: l,
|
|
1771
|
+
key: k,
|
|
1772
|
+
children: c
|
|
1773
|
+
} = props2.config;
|
|
1774
|
+
const KMenuItemComp = resolveComponent("KMenuItem");
|
|
1775
|
+
return createVNode(Fragment, null, [props2.data.map((d) => {
|
|
1776
|
+
const childNodes = d.children ? d.children : (() => {
|
|
1777
|
+
const raw = d[c];
|
|
1778
|
+
return raw ? props2.getNodesByKeys(raw.map((it) => it[k])) : [];
|
|
1779
|
+
})();
|
|
1780
|
+
const hasChildren = childNodes.length > 0;
|
|
1781
|
+
return createVNode("li", {
|
|
1782
|
+
"key": d[k],
|
|
1783
|
+
"class": ["m-menu-item", "k-menu-item", {
|
|
1784
|
+
"m-menu-item-root": props2.root
|
|
1785
|
+
}, {
|
|
1786
|
+
"k-menu-item-root": props2.root
|
|
1787
|
+
}, {
|
|
1788
|
+
"m-menu-item-disabled": !!d.disabled
|
|
1789
|
+
}, {
|
|
1790
|
+
"k-menu-item-disabled": !!d.disabled
|
|
1791
|
+
}, {
|
|
1792
|
+
"m-menu-item-active": !!d.isActive
|
|
1793
|
+
}, {
|
|
1794
|
+
"k-menu-item-active": !!d.isActive
|
|
1795
|
+
}],
|
|
1796
|
+
"onClick": (e) => clickEvent(e, d)
|
|
1797
|
+
}, [createVNode("div", {
|
|
1798
|
+
"class": "m-menu-item-content k-menu-item-content"
|
|
1799
|
+
}, [props2.checkbox ? createVNode("input", {
|
|
1800
|
+
"type": "checkbox",
|
|
1801
|
+
"class": "m-menu-item-checkbox k-menu-item-checkbox",
|
|
1802
|
+
"checked": !!d.checked,
|
|
1803
|
+
"disabled": !!d.disabled,
|
|
1804
|
+
"onChange": (e) => checkEvent(e, d),
|
|
1805
|
+
"onClick": (e) => e.stopPropagation()
|
|
1806
|
+
}, null) : null, createVNode("span", {
|
|
1807
|
+
"class": "m-menu-item-label k-menu-item-label",
|
|
1808
|
+
"onClick": (e) => expandEvent(e, d)
|
|
1809
|
+
}, [slots.default ? slots.default({
|
|
1810
|
+
data: d[l],
|
|
1811
|
+
node: d
|
|
1812
|
+
}) : d[l]]), hasChildren ? createVNode("span", {
|
|
1813
|
+
"class": ["m-menu-item-arrow", "k-menu-item-arrow", {
|
|
1814
|
+
"m-menu-item-arrow-expand": d.expand
|
|
1815
|
+
}, {
|
|
1816
|
+
"k-menu-item-arrow-expand": d.expand
|
|
1817
|
+
}],
|
|
1818
|
+
"onClick": (e) => expandEvent(e, d)
|
|
1819
|
+
}, null) : null]), hasChildren && d.expand ? createVNode("div", {
|
|
1820
|
+
"class": "m-menu-item-children k-menu-item-children"
|
|
1821
|
+
}, [h(KMenuItemComp, {
|
|
1822
|
+
data: childNodes,
|
|
1823
|
+
config: props2.config,
|
|
1824
|
+
checkbox: props2.checkbox,
|
|
1825
|
+
root: false,
|
|
1826
|
+
getNodesByKeys: props2.getNodesByKeys,
|
|
1827
|
+
handleExpand: props2.handleExpand,
|
|
1828
|
+
handleCheck: props2.handleCheck,
|
|
1829
|
+
handleItemClick: props2.handleItemClick
|
|
1830
|
+
}, slots.default ? {
|
|
1831
|
+
default: slots.default
|
|
1832
|
+
} : void 0)]) : null]);
|
|
1833
|
+
})]);
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
function findKeyByPath(data, path) {
|
|
1838
|
+
for (const item of data) {
|
|
1839
|
+
if (item.path === path) return item.key;
|
|
1840
|
+
if (item.children) {
|
|
1841
|
+
const found = findKeyByPath(item.children, path);
|
|
1842
|
+
if (found !== null) return found;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
const KNavMenu = /* @__PURE__ */ defineComponent({
|
|
1848
|
+
name: "KNavMenu",
|
|
1849
|
+
props: {
|
|
1850
|
+
data: {
|
|
1851
|
+
type: Array,
|
|
1852
|
+
default: () => []
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1855
|
+
setup(props2, {
|
|
1856
|
+
slots
|
|
1857
|
+
}) {
|
|
1858
|
+
const router = useRouter();
|
|
1859
|
+
const route = useRoute();
|
|
1860
|
+
const {
|
|
1861
|
+
nodesRef,
|
|
1862
|
+
nodeMap,
|
|
1863
|
+
initNodes,
|
|
1864
|
+
toggleExpand,
|
|
1865
|
+
setActive,
|
|
1866
|
+
getNodesByKeys,
|
|
1867
|
+
config
|
|
1868
|
+
} = useMenu({
|
|
1869
|
+
data: props2.data
|
|
1870
|
+
});
|
|
1871
|
+
watch(() => props2.data, (newData) => {
|
|
1872
|
+
initNodes(newData ?? []);
|
|
1873
|
+
syncActiveByRoute(route.path);
|
|
1874
|
+
}, {
|
|
1875
|
+
deep: true
|
|
1876
|
+
});
|
|
1877
|
+
const syncActiveByRoute = (path) => {
|
|
1878
|
+
const key = findKeyByPath(props2.data, path);
|
|
1879
|
+
if (key === null) return;
|
|
1880
|
+
const node = nodeMap.get(key);
|
|
1881
|
+
if (!node) return;
|
|
1882
|
+
setActive(node);
|
|
1883
|
+
let parent = node.parent;
|
|
1884
|
+
while (parent) {
|
|
1885
|
+
if (!parent.expand) {
|
|
1886
|
+
parent.expand = true;
|
|
1887
|
+
}
|
|
1888
|
+
parent = parent.parent;
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
watch(() => route.path, syncActiveByRoute);
|
|
1892
|
+
onMounted(() => {
|
|
1893
|
+
syncActiveByRoute(route.path);
|
|
1894
|
+
});
|
|
1895
|
+
const handleItemClick = (node, e) => {
|
|
1896
|
+
if (node.disabled) return;
|
|
1897
|
+
const navNode = node;
|
|
1898
|
+
if (navNode.path) {
|
|
1899
|
+
router.push(navNode.path);
|
|
1900
|
+
setActive(node);
|
|
1901
|
+
} else {
|
|
1902
|
+
toggleExpand(node);
|
|
1903
|
+
}
|
|
1904
|
+
e.stopPropagation();
|
|
1905
|
+
};
|
|
1906
|
+
const handleExpand = (node, e) => {
|
|
1907
|
+
if (node.disabled) return;
|
|
1908
|
+
const navNode = node;
|
|
1909
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
1910
|
+
if (hasChildren) {
|
|
1911
|
+
toggleExpand(node);
|
|
1912
|
+
} else if (navNode.path) {
|
|
1913
|
+
router.push(navNode.path);
|
|
1914
|
+
setActive(node);
|
|
1915
|
+
}
|
|
1916
|
+
e.stopPropagation();
|
|
1917
|
+
};
|
|
1918
|
+
const handleCheck = () => {
|
|
1919
|
+
};
|
|
1920
|
+
const defaultSlot = slots.default ? slots.default : (slotProps) => {
|
|
1921
|
+
const label = slotProps.data;
|
|
1922
|
+
const icon = slotProps.node ? slotProps.node.icon : void 0;
|
|
1923
|
+
return [icon ? h("span", {
|
|
1924
|
+
class: "k-nav-menu-icon"
|
|
1925
|
+
}, icon) : null, h("span", {
|
|
1926
|
+
class: "k-nav-menu-label"
|
|
1927
|
+
}, label)];
|
|
1928
|
+
};
|
|
1929
|
+
return () => createVNode("div", {
|
|
1930
|
+
"class": "k-nav-menu"
|
|
1931
|
+
}, [createVNode(KMenuItem, {
|
|
1932
|
+
"data": [...nodesRef.value],
|
|
1933
|
+
"config": config,
|
|
1934
|
+
"checkbox": false,
|
|
1935
|
+
"root": true,
|
|
1936
|
+
"getNodesByKeys": getNodesByKeys,
|
|
1937
|
+
"handleExpand": handleExpand,
|
|
1938
|
+
"handleCheck": handleCheck,
|
|
1939
|
+
"handleItemClick": handleItemClick
|
|
1940
|
+
}, {
|
|
1941
|
+
default: defaultSlot
|
|
1942
|
+
})]);
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
let uidCounter$1 = 0;
|
|
1946
|
+
function genUid$1() {
|
|
1947
|
+
return `upload-${Date.now()}-${++uidCounter$1}`;
|
|
1948
|
+
}
|
|
1949
|
+
function formatSize$1(bytes) {
|
|
1950
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1951
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1952
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1953
|
+
}
|
|
1954
|
+
const KUpload = /* @__PURE__ */ defineComponent({
|
|
1955
|
+
name: "KUpload",
|
|
1956
|
+
props: {
|
|
1957
|
+
request: {
|
|
1958
|
+
type: Function,
|
|
1959
|
+
required: true
|
|
1960
|
+
},
|
|
1961
|
+
accept: {
|
|
1962
|
+
type: String,
|
|
1963
|
+
default: ""
|
|
1964
|
+
},
|
|
1965
|
+
multiple: {
|
|
1966
|
+
type: Boolean,
|
|
1967
|
+
default: false
|
|
1968
|
+
},
|
|
1969
|
+
maxCount: {
|
|
1970
|
+
type: Number,
|
|
1971
|
+
default: Infinity
|
|
1972
|
+
},
|
|
1973
|
+
maxSize: {
|
|
1974
|
+
type: Number,
|
|
1975
|
+
default: Infinity
|
|
1976
|
+
},
|
|
1977
|
+
disabled: {
|
|
1978
|
+
type: Boolean,
|
|
1979
|
+
default: false
|
|
1980
|
+
},
|
|
1981
|
+
fileList: {
|
|
1982
|
+
type: Array,
|
|
1983
|
+
default: () => []
|
|
1984
|
+
},
|
|
1985
|
+
drag: {
|
|
1986
|
+
type: Boolean,
|
|
1987
|
+
default: true
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
emits: ["update:fileList", "change", "error"],
|
|
1991
|
+
setup(props2, ctx) {
|
|
1992
|
+
const inputRef = ref(null);
|
|
1993
|
+
const isDragOver = ref(false);
|
|
1994
|
+
const openFileDialog = () => {
|
|
1995
|
+
var _a;
|
|
1996
|
+
if (props2.disabled) return;
|
|
1997
|
+
(_a = inputRef.value) == null ? void 0 : _a.click();
|
|
1998
|
+
};
|
|
1999
|
+
const validateFile = (file) => {
|
|
2000
|
+
if (props2.maxSize !== Infinity && file.size > props2.maxSize) {
|
|
2001
|
+
return `文件 "${file.name}" 超过大小限制(最大 ${formatSize$1(props2.maxSize)})`;
|
|
2002
|
+
}
|
|
2003
|
+
return null;
|
|
2004
|
+
};
|
|
2005
|
+
const processFiles = async (rawFiles) => {
|
|
2006
|
+
const fileArray = Array.from(rawFiles);
|
|
2007
|
+
const currentCount = props2.fileList.length;
|
|
2008
|
+
const remaining = props2.maxCount - currentCount;
|
|
2009
|
+
if (remaining <= 0) return;
|
|
2010
|
+
const filesToProcess = fileArray.slice(0, remaining);
|
|
2011
|
+
const newList = [...props2.fileList];
|
|
2012
|
+
for (const raw of filesToProcess) {
|
|
2013
|
+
const err = validateFile(raw);
|
|
2014
|
+
if (err) {
|
|
2015
|
+
ctx.emit("error", {
|
|
2016
|
+
file: raw,
|
|
2017
|
+
message: err
|
|
2018
|
+
});
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
const uploadFile = {
|
|
2022
|
+
uid: genUid$1(),
|
|
2023
|
+
name: raw.name,
|
|
2024
|
+
size: raw.size,
|
|
2025
|
+
type: raw.type,
|
|
2026
|
+
status: "pending",
|
|
2027
|
+
percent: 0,
|
|
2028
|
+
raw
|
|
2029
|
+
};
|
|
2030
|
+
newList.push(uploadFile);
|
|
2031
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2032
|
+
ctx.emit("change", uploadFile);
|
|
2033
|
+
const idx = newList.indexOf(uploadFile);
|
|
2034
|
+
newList[idx] = {
|
|
2035
|
+
...uploadFile,
|
|
2036
|
+
status: "uploading"
|
|
2037
|
+
};
|
|
2038
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2039
|
+
try {
|
|
2040
|
+
const result = await props2.request(raw, (percent) => {
|
|
2041
|
+
const current2 = newList.find((f) => f.uid === uploadFile.uid);
|
|
2042
|
+
if (current2) {
|
|
2043
|
+
const i = newList.indexOf(current2);
|
|
2044
|
+
newList[i] = {
|
|
2045
|
+
...current2,
|
|
2046
|
+
percent
|
|
2047
|
+
};
|
|
2048
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2051
|
+
const current = newList.find((f) => f.uid === uploadFile.uid);
|
|
2052
|
+
if (current) {
|
|
2053
|
+
const i = newList.indexOf(current);
|
|
2054
|
+
newList[i] = {
|
|
2055
|
+
...current,
|
|
2056
|
+
status: "success",
|
|
2057
|
+
percent: 100,
|
|
2058
|
+
url: result.url
|
|
2059
|
+
};
|
|
2060
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2061
|
+
ctx.emit("change", newList[i]);
|
|
2062
|
+
}
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
const current = newList.find((f) => f.uid === uploadFile.uid);
|
|
2065
|
+
if (current) {
|
|
2066
|
+
const i = newList.indexOf(current);
|
|
2067
|
+
newList[i] = {
|
|
2068
|
+
...current,
|
|
2069
|
+
status: "error"
|
|
2070
|
+
};
|
|
2071
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2072
|
+
ctx.emit("error", {
|
|
2073
|
+
file: raw,
|
|
2074
|
+
message: String(e)
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
const onInputChange = (e) => {
|
|
2081
|
+
const target = e.target;
|
|
2082
|
+
if (target.files) {
|
|
2083
|
+
processFiles(target.files);
|
|
2084
|
+
target.value = "";
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
const onDragOver = (e) => {
|
|
2088
|
+
if (props2.disabled || !props2.drag) return;
|
|
2089
|
+
e.preventDefault();
|
|
2090
|
+
isDragOver.value = true;
|
|
2091
|
+
};
|
|
2092
|
+
const onDragLeave = () => {
|
|
2093
|
+
isDragOver.value = false;
|
|
2094
|
+
};
|
|
2095
|
+
const onDrop = (e) => {
|
|
2096
|
+
var _a;
|
|
2097
|
+
if (props2.disabled || !props2.drag) return;
|
|
2098
|
+
e.preventDefault();
|
|
2099
|
+
isDragOver.value = false;
|
|
2100
|
+
if ((_a = e.dataTransfer) == null ? void 0 : _a.files) {
|
|
2101
|
+
processFiles(e.dataTransfer.files);
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
return () => {
|
|
2105
|
+
var _a, _b;
|
|
2106
|
+
const classes = ["k-upload", props2.drag ? "k-upload-drag" : "", props2.disabled ? "k-upload-disabled" : "", isDragOver.value ? "k-upload-drag-over" : ""].filter(Boolean);
|
|
2107
|
+
return createVNode("div", {
|
|
2108
|
+
"class": classes,
|
|
2109
|
+
"onClick": openFileDialog,
|
|
2110
|
+
"onDragover": onDragOver,
|
|
2111
|
+
"onDragleave": onDragLeave,
|
|
2112
|
+
"onDrop": onDrop
|
|
2113
|
+
}, [createVNode("input", {
|
|
2114
|
+
"ref": inputRef,
|
|
2115
|
+
"type": "file",
|
|
2116
|
+
"class": "k-upload-input",
|
|
2117
|
+
"accept": props2.accept,
|
|
2118
|
+
"multiple": props2.multiple,
|
|
2119
|
+
"onChange": onInputChange
|
|
2120
|
+
}, null), ((_b = (_a = ctx.slots).default) == null ? void 0 : _b.call(_a)) ?? createVNode("div", {
|
|
2121
|
+
"class": "k-upload-placeholder"
|
|
2122
|
+
}, [createVNode("span", {
|
|
2123
|
+
"class": "k-upload-icon"
|
|
2124
|
+
}, [createTextVNode("↑")]), createVNode("span", {
|
|
2125
|
+
"class": "k-upload-text"
|
|
2126
|
+
}, [props2.drag ? "点击或拖拽文件到此处上传" : "点击上传文件"])])]);
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
let uidCounter = 0;
|
|
2131
|
+
function genUid() {
|
|
2132
|
+
return `img-upload-${Date.now()}-${++uidCounter}`;
|
|
2133
|
+
}
|
|
2134
|
+
const KImageUpload = /* @__PURE__ */ defineComponent({
|
|
2135
|
+
name: "KImageUpload",
|
|
2136
|
+
props: {
|
|
2137
|
+
request: {
|
|
2138
|
+
type: Function,
|
|
2139
|
+
required: true
|
|
2140
|
+
},
|
|
2141
|
+
accept: {
|
|
2142
|
+
type: String,
|
|
2143
|
+
default: "image/*"
|
|
2144
|
+
},
|
|
2145
|
+
multiple: {
|
|
2146
|
+
type: Boolean,
|
|
2147
|
+
default: true
|
|
2148
|
+
},
|
|
2149
|
+
maxCount: {
|
|
2150
|
+
type: Number,
|
|
2151
|
+
default: Infinity
|
|
2152
|
+
},
|
|
2153
|
+
maxSize: {
|
|
2154
|
+
type: Number,
|
|
2155
|
+
default: Infinity
|
|
2156
|
+
},
|
|
2157
|
+
disabled: {
|
|
2158
|
+
type: Boolean,
|
|
2159
|
+
default: false
|
|
2160
|
+
},
|
|
2161
|
+
fileList: {
|
|
2162
|
+
type: Array,
|
|
2163
|
+
default: () => []
|
|
2164
|
+
},
|
|
2165
|
+
drag: {
|
|
2166
|
+
type: Boolean,
|
|
2167
|
+
default: true
|
|
2168
|
+
},
|
|
2169
|
+
previewSize: {
|
|
2170
|
+
type: String,
|
|
2171
|
+
default: "100px"
|
|
2172
|
+
}
|
|
2173
|
+
},
|
|
2174
|
+
emits: ["update:fileList", "change", "error"],
|
|
2175
|
+
setup(props2, ctx) {
|
|
2176
|
+
const inputRef = ref(null);
|
|
2177
|
+
const isDragOver = ref(false);
|
|
2178
|
+
const objectUrlMap = ref(/* @__PURE__ */ new Map());
|
|
2179
|
+
const getPreviewUrl = (file) => {
|
|
2180
|
+
if (file.thumbUrl) return file.thumbUrl;
|
|
2181
|
+
if (file.url) return file.url;
|
|
2182
|
+
if (file.raw) {
|
|
2183
|
+
const existing = objectUrlMap.value.get(file.uid);
|
|
2184
|
+
if (existing) return existing;
|
|
2185
|
+
const url = URL.createObjectURL(file.raw);
|
|
2186
|
+
objectUrlMap.value.set(file.uid, url);
|
|
2187
|
+
return url;
|
|
2188
|
+
}
|
|
2189
|
+
return "";
|
|
2190
|
+
};
|
|
2191
|
+
onBeforeUnmount(() => {
|
|
2192
|
+
objectUrlMap.value.forEach((url) => URL.revokeObjectURL(url));
|
|
2193
|
+
objectUrlMap.value.clear();
|
|
2194
|
+
});
|
|
2195
|
+
const showAddCard = computed(() => !props2.disabled && props2.fileList.length < props2.maxCount);
|
|
2196
|
+
const openFileDialog = () => {
|
|
2197
|
+
var _a;
|
|
2198
|
+
if (props2.disabled) return;
|
|
2199
|
+
(_a = inputRef.value) == null ? void 0 : _a.click();
|
|
2200
|
+
};
|
|
2201
|
+
const processFiles = async (rawFiles) => {
|
|
2202
|
+
const fileArray = Array.from(rawFiles);
|
|
2203
|
+
const currentCount = props2.fileList.length;
|
|
2204
|
+
const remaining = props2.maxCount - currentCount;
|
|
2205
|
+
if (remaining <= 0) return;
|
|
2206
|
+
const filesToProcess = fileArray.slice(0, remaining);
|
|
2207
|
+
const newList = [...props2.fileList];
|
|
2208
|
+
for (const raw of filesToProcess) {
|
|
2209
|
+
if (props2.maxSize !== Infinity && raw.size > props2.maxSize) {
|
|
2210
|
+
ctx.emit("error", {
|
|
2211
|
+
file: raw,
|
|
2212
|
+
message: `文件 "${raw.name}" 超过大小限制`
|
|
2213
|
+
});
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
const uploadFile = {
|
|
2217
|
+
uid: genUid(),
|
|
2218
|
+
name: raw.name,
|
|
2219
|
+
size: raw.size,
|
|
2220
|
+
type: raw.type,
|
|
2221
|
+
status: "uploading",
|
|
2222
|
+
percent: 0,
|
|
2223
|
+
raw
|
|
2224
|
+
};
|
|
2225
|
+
newList.push(uploadFile);
|
|
2226
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2227
|
+
ctx.emit("change", uploadFile);
|
|
2228
|
+
try {
|
|
2229
|
+
const result = await props2.request(raw, (percent) => {
|
|
2230
|
+
const current2 = newList.find((f) => f.uid === uploadFile.uid);
|
|
2231
|
+
if (current2) {
|
|
2232
|
+
const i = newList.indexOf(current2);
|
|
2233
|
+
newList[i] = {
|
|
2234
|
+
...current2,
|
|
2235
|
+
percent
|
|
2236
|
+
};
|
|
2237
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
const current = newList.find((f) => f.uid === uploadFile.uid);
|
|
2241
|
+
if (current) {
|
|
2242
|
+
const localUrl = objectUrlMap.value.get(uploadFile.uid);
|
|
2243
|
+
if (localUrl) {
|
|
2244
|
+
URL.revokeObjectURL(localUrl);
|
|
2245
|
+
objectUrlMap.value.delete(uploadFile.uid);
|
|
2246
|
+
}
|
|
2247
|
+
const i = newList.indexOf(current);
|
|
2248
|
+
newList[i] = {
|
|
2249
|
+
...current,
|
|
2250
|
+
status: "success",
|
|
2251
|
+
percent: 100,
|
|
2252
|
+
url: result.url
|
|
2253
|
+
};
|
|
2254
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2255
|
+
ctx.emit("change", newList[i]);
|
|
2256
|
+
}
|
|
2257
|
+
} catch (e) {
|
|
2258
|
+
const current = newList.find((f) => f.uid === uploadFile.uid);
|
|
2259
|
+
if (current) {
|
|
2260
|
+
const i = newList.indexOf(current);
|
|
2261
|
+
newList[i] = {
|
|
2262
|
+
...current,
|
|
2263
|
+
status: "error"
|
|
2264
|
+
};
|
|
2265
|
+
ctx.emit("update:fileList", [...newList]);
|
|
2266
|
+
ctx.emit("error", {
|
|
2267
|
+
file: raw,
|
|
2268
|
+
message: String(e)
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
const onInputChange = (e) => {
|
|
2275
|
+
const target = e.target;
|
|
2276
|
+
if (target.files) {
|
|
2277
|
+
processFiles(target.files);
|
|
2278
|
+
target.value = "";
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
const onDragOver = (e) => {
|
|
2282
|
+
if (props2.disabled || !props2.drag) return;
|
|
2283
|
+
e.preventDefault();
|
|
2284
|
+
isDragOver.value = true;
|
|
2285
|
+
};
|
|
2286
|
+
const onDragLeave = () => {
|
|
2287
|
+
isDragOver.value = false;
|
|
2288
|
+
};
|
|
2289
|
+
const onDrop = (e) => {
|
|
2290
|
+
var _a;
|
|
2291
|
+
if (props2.disabled || !props2.drag) return;
|
|
2292
|
+
e.preventDefault();
|
|
2293
|
+
isDragOver.value = false;
|
|
2294
|
+
if ((_a = e.dataTransfer) == null ? void 0 : _a.files) {
|
|
2295
|
+
processFiles(e.dataTransfer.files);
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
const removeFile = (uid) => {
|
|
2299
|
+
const localUrl = objectUrlMap.value.get(uid);
|
|
2300
|
+
if (localUrl) {
|
|
2301
|
+
URL.revokeObjectURL(localUrl);
|
|
2302
|
+
objectUrlMap.value.delete(uid);
|
|
2303
|
+
}
|
|
2304
|
+
const next = props2.fileList.filter((f) => f.uid !== uid);
|
|
2305
|
+
ctx.emit("update:fileList", next);
|
|
2306
|
+
};
|
|
2307
|
+
return () => {
|
|
2308
|
+
const cardStyle = {
|
|
2309
|
+
width: props2.previewSize,
|
|
2310
|
+
height: props2.previewSize
|
|
2311
|
+
};
|
|
2312
|
+
const previewCards = props2.fileList.map((file) => {
|
|
2313
|
+
const previewUrl = getPreviewUrl(file);
|
|
2314
|
+
const statusClass = `k-image-upload-card--${file.status}`;
|
|
2315
|
+
return createVNode("div", {
|
|
2316
|
+
"key": file.uid,
|
|
2317
|
+
"class": ["k-image-upload-card", statusClass],
|
|
2318
|
+
"style": cardStyle
|
|
2319
|
+
}, [previewUrl && createVNode("img", {
|
|
2320
|
+
"class": "k-image-upload-preview",
|
|
2321
|
+
"src": previewUrl,
|
|
2322
|
+
"alt": file.name
|
|
2323
|
+
}, null), file.status === "uploading" && createVNode("div", {
|
|
2324
|
+
"class": "k-image-upload-overlay"
|
|
2325
|
+
}, [createVNode("div", {
|
|
2326
|
+
"class": "k-image-upload-progress",
|
|
2327
|
+
"style": {
|
|
2328
|
+
height: `${file.percent}%`
|
|
2329
|
+
}
|
|
2330
|
+
}, null), createVNode("span", {
|
|
2331
|
+
"class": "k-image-upload-percent"
|
|
2332
|
+
}, [file.percent, createTextVNode("%")])]), file.status === "error" && createVNode("div", {
|
|
2333
|
+
"class": "k-image-upload-overlay k-image-upload-overlay--error"
|
|
2334
|
+
}, [createVNode("span", null, [createTextVNode("上传失败")])]), !props2.disabled && createVNode("button", {
|
|
2335
|
+
"class": "k-image-upload-remove",
|
|
2336
|
+
"type": "button",
|
|
2337
|
+
"onClick": (e) => {
|
|
2338
|
+
e.stopPropagation();
|
|
2339
|
+
removeFile(file.uid);
|
|
2340
|
+
}
|
|
2341
|
+
}, [createTextVNode("×")])]);
|
|
2342
|
+
});
|
|
2343
|
+
const addCard = showAddCard.value && createVNode("div", {
|
|
2344
|
+
"class": ["k-image-upload-card", "k-image-upload-card--add", isDragOver.value ? "k-upload-drag-over" : ""].filter(Boolean),
|
|
2345
|
+
"style": cardStyle,
|
|
2346
|
+
"onClick": openFileDialog,
|
|
2347
|
+
"onDragover": onDragOver,
|
|
2348
|
+
"onDragleave": onDragLeave,
|
|
2349
|
+
"onDrop": onDrop
|
|
2350
|
+
}, [createVNode("span", {
|
|
2351
|
+
"class": "k-image-upload-add-icon"
|
|
2352
|
+
}, [createTextVNode("+")])]);
|
|
2353
|
+
return createVNode("div", {
|
|
2354
|
+
"class": ["k-image-upload", props2.disabled ? "k-upload-disabled" : ""].filter(Boolean)
|
|
2355
|
+
}, [createVNode("input", {
|
|
2356
|
+
"ref": inputRef,
|
|
2357
|
+
"type": "file",
|
|
2358
|
+
"class": "k-upload-input",
|
|
2359
|
+
"accept": props2.accept,
|
|
2360
|
+
"multiple": props2.multiple,
|
|
2361
|
+
"onChange": onInputChange
|
|
2362
|
+
}, null), createVNode("div", {
|
|
2363
|
+
"class": "k-image-upload-grid"
|
|
2364
|
+
}, [previewCards, addCard])]);
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
});
|
|
2368
|
+
const STATUS_TEXT = {
|
|
2369
|
+
pending: "等待中",
|
|
2370
|
+
uploading: "上传中",
|
|
2371
|
+
success: "已完成",
|
|
2372
|
+
error: "上传失败"
|
|
2373
|
+
};
|
|
2374
|
+
function formatSize(bytes) {
|
|
2375
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2376
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2377
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2378
|
+
}
|
|
2379
|
+
const KFileList = /* @__PURE__ */ defineComponent({
|
|
2380
|
+
name: "KFileList",
|
|
2381
|
+
props: {
|
|
2382
|
+
files: {
|
|
2383
|
+
type: Array,
|
|
2384
|
+
default: () => []
|
|
2385
|
+
},
|
|
2386
|
+
removable: {
|
|
2387
|
+
type: Boolean,
|
|
2388
|
+
default: true
|
|
2389
|
+
}
|
|
2390
|
+
},
|
|
2391
|
+
emits: ["remove"],
|
|
2392
|
+
setup(props2, ctx) {
|
|
2393
|
+
return () => {
|
|
2394
|
+
if (props2.files.length === 0) return null;
|
|
2395
|
+
return createVNode("ul", {
|
|
2396
|
+
"class": "k-file-list"
|
|
2397
|
+
}, [props2.files.map((file) => {
|
|
2398
|
+
const statusClass = `k-file-list-item--${file.status}`;
|
|
2399
|
+
return createVNode("li", {
|
|
2400
|
+
"key": file.uid,
|
|
2401
|
+
"class": ["k-file-list-item", statusClass]
|
|
2402
|
+
}, [createVNode("span", {
|
|
2403
|
+
"class": "k-file-list-icon",
|
|
2404
|
+
"aria-hidden": "true"
|
|
2405
|
+
}, [file.status === "success" ? "✓" : file.status === "error" ? "✕" : "○"]), createVNode("div", {
|
|
2406
|
+
"class": "k-file-list-info"
|
|
2407
|
+
}, [createVNode("span", {
|
|
2408
|
+
"class": "k-file-list-name",
|
|
2409
|
+
"title": file.name
|
|
2410
|
+
}, [file.url ? createVNode("a", {
|
|
2411
|
+
"class": "k-file-list-link",
|
|
2412
|
+
"href": file.url,
|
|
2413
|
+
"target": "_blank",
|
|
2414
|
+
"rel": "noopener noreferrer"
|
|
2415
|
+
}, [file.name]) : file.name]), createVNode("span", {
|
|
2416
|
+
"class": "k-file-list-meta"
|
|
2417
|
+
}, [formatSize(file.size), createTextVNode(" · "), STATUS_TEXT[file.status]])]), props2.removable && createVNode("button", {
|
|
2418
|
+
"class": "k-file-list-remove",
|
|
2419
|
+
"type": "button",
|
|
2420
|
+
"title": "移除文件",
|
|
2421
|
+
"onClick": () => ctx.emit("remove", file)
|
|
2422
|
+
}, [createTextVNode("×")]), file.status === "uploading" && createVNode("div", {
|
|
2423
|
+
"class": "k-file-list-progress-track"
|
|
2424
|
+
}, [createVNode("div", {
|
|
2425
|
+
"class": "k-file-list-progress-fill",
|
|
2426
|
+
"style": {
|
|
2427
|
+
width: `${file.percent}%`
|
|
2428
|
+
}
|
|
2429
|
+
}, null)])]);
|
|
2430
|
+
})]);
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
function defineCrudRoutes(config) {
|
|
2435
|
+
const { name, path, permission, title, icon } = config;
|
|
2436
|
+
const resource = permission ?? name.toLowerCase();
|
|
2437
|
+
const basePath = path.replace(/\/+$/, "");
|
|
2438
|
+
const opSpecs = [
|
|
2439
|
+
{ op: "list", suffix: "", action: "read", component: config.list, titleSuffix: "", keepAlive: true, hidden: false },
|
|
2440
|
+
{ op: "new", suffix: "/new", action: "create", component: config.new, titleSuffix: " - 新建", keepAlive: false, hidden: true },
|
|
2441
|
+
{ op: "modify", suffix: "/:id/edit", action: "update", component: config.modify, titleSuffix: " - 编辑", keepAlive: false, hidden: true },
|
|
2442
|
+
{ op: "detail", suffix: "/:id", action: "read", component: config.detail, titleSuffix: " - 详情", keepAlive: false, hidden: true }
|
|
2443
|
+
];
|
|
2444
|
+
const routes = [];
|
|
2445
|
+
for (const spec of opSpecs) {
|
|
2446
|
+
if (!spec.component) continue;
|
|
2447
|
+
routes.push({
|
|
2448
|
+
path: basePath + spec.suffix,
|
|
2449
|
+
// 路由名称格式:EntityOp,如 OrderList、OrderNew
|
|
2450
|
+
name: `${name}${capitalize(spec.op)}`,
|
|
2451
|
+
component: spec.component,
|
|
2452
|
+
meta: {
|
|
2453
|
+
crud: {
|
|
2454
|
+
entity: name,
|
|
2455
|
+
op: spec.op
|
|
2456
|
+
},
|
|
2457
|
+
// 仅在显式传入 permission 时生成权限检查
|
|
2458
|
+
...permission ? { can: { resource, action: spec.action } } : void 0,
|
|
2459
|
+
requiresAuth: true,
|
|
2460
|
+
// 自动填充 title:基于 config.title + 操作类型后缀
|
|
2461
|
+
...title ? { title: `${title}${spec.titleSuffix}` } : void 0,
|
|
2462
|
+
// 继承 config.icon
|
|
2463
|
+
...icon ? { icon } : void 0,
|
|
2464
|
+
keepAlive: spec.keepAlive,
|
|
2465
|
+
hidden: spec.hidden
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
return routes;
|
|
2470
|
+
}
|
|
2471
|
+
function capitalize(str) {
|
|
2472
|
+
if (!str) return str;
|
|
2473
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2474
|
+
}
|
|
2475
|
+
const TAB_STORE_KEY = Symbol("tab-store");
|
|
2476
|
+
function createTabStoreInstance() {
|
|
2477
|
+
const tabs = ref([]);
|
|
2478
|
+
const activeTab = ref("");
|
|
2479
|
+
const cachedNames = computed(
|
|
2480
|
+
() => tabs.value.map((tab) => tab.name)
|
|
2481
|
+
);
|
|
2482
|
+
const addTab = (tab) => {
|
|
2483
|
+
const exists = tabs.value.some((t) => t.path === tab.path);
|
|
2484
|
+
if (!exists) {
|
|
2485
|
+
tabs.value.push(tab);
|
|
2486
|
+
}
|
|
2487
|
+
activeTab.value = tab.path;
|
|
2488
|
+
};
|
|
2489
|
+
const addTabFromRoute2 = (route) => {
|
|
2490
|
+
const meta = route.meta;
|
|
2491
|
+
const title = meta.title ?? (typeof route.name === "string" ? route.name : route.path);
|
|
2492
|
+
const name = typeof route.name === "string" ? route.name : route.path;
|
|
2493
|
+
addTab({
|
|
2494
|
+
path: route.path,
|
|
2495
|
+
title,
|
|
2496
|
+
name,
|
|
2497
|
+
closable: true
|
|
2498
|
+
});
|
|
2499
|
+
};
|
|
2500
|
+
const removeTab = (path) => {
|
|
2501
|
+
const index = tabs.value.findIndex((t) => t.path === path);
|
|
2502
|
+
if (index === -1) return void 0;
|
|
2503
|
+
const target = tabs.value[index];
|
|
2504
|
+
if (!target.closable) return void 0;
|
|
2505
|
+
tabs.value.splice(index, 1);
|
|
2506
|
+
if (activeTab.value === path) {
|
|
2507
|
+
const nextTab = tabs.value[index] ?? tabs.value[index - 1];
|
|
2508
|
+
activeTab.value = nextTab ? nextTab.path : "";
|
|
2509
|
+
return activeTab.value || void 0;
|
|
2510
|
+
}
|
|
2511
|
+
return void 0;
|
|
2512
|
+
};
|
|
2513
|
+
const removeOthers = (path) => {
|
|
2514
|
+
tabs.value = tabs.value.filter((t) => !t.closable || t.path === path);
|
|
2515
|
+
if (!tabs.value.some((t) => t.path === activeTab.value)) {
|
|
2516
|
+
activeTab.value = path;
|
|
2517
|
+
}
|
|
2518
|
+
};
|
|
2519
|
+
const removeAll = () => {
|
|
2520
|
+
tabs.value = tabs.value.filter((t) => !t.closable);
|
|
2521
|
+
const first = tabs.value[0];
|
|
2522
|
+
activeTab.value = first ? first.path : "";
|
|
2523
|
+
};
|
|
2524
|
+
const setActive = (path) => {
|
|
2525
|
+
activeTab.value = path;
|
|
2526
|
+
};
|
|
2527
|
+
return {
|
|
2528
|
+
tabs,
|
|
2529
|
+
activeTab,
|
|
2530
|
+
cachedNames,
|
|
2531
|
+
addTab,
|
|
2532
|
+
addTabFromRoute: addTabFromRoute2,
|
|
2533
|
+
removeTab,
|
|
2534
|
+
removeOthers,
|
|
2535
|
+
removeAll,
|
|
2536
|
+
setActive
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
function createTabStore() {
|
|
2540
|
+
const store = createTabStoreInstance();
|
|
2541
|
+
provide(TAB_STORE_KEY, store);
|
|
2542
|
+
return store;
|
|
2543
|
+
}
|
|
2544
|
+
function useTabStore() {
|
|
2545
|
+
const store = inject(TAB_STORE_KEY);
|
|
2546
|
+
if (!store) {
|
|
2547
|
+
throw new Error(
|
|
2548
|
+
"[useTabStore] 未找到 tab store 实例,请确保在父级组件中调用了 createTabStore()"
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
return store;
|
|
2552
|
+
}
|
|
2553
|
+
function useMenuFromRoutes(routes) {
|
|
2554
|
+
return computed(() => transformRoutes(routes));
|
|
2555
|
+
}
|
|
2556
|
+
function transformRoutes(routes) {
|
|
2557
|
+
var _a;
|
|
2558
|
+
const result = [];
|
|
2559
|
+
for (const route of routes) {
|
|
2560
|
+
const meta = route.meta;
|
|
2561
|
+
if (meta == null ? void 0 : meta.hidden) continue;
|
|
2562
|
+
const label = meta == null ? void 0 : meta.title;
|
|
2563
|
+
if (!label) continue;
|
|
2564
|
+
const key = typeof route.name === "string" ? route.name : route.path;
|
|
2565
|
+
const item = {
|
|
2566
|
+
key,
|
|
2567
|
+
label,
|
|
2568
|
+
path: route.path,
|
|
2569
|
+
icon: meta == null ? void 0 : meta.icon
|
|
2570
|
+
};
|
|
2571
|
+
if ((_a = route.children) == null ? void 0 : _a.length) {
|
|
2572
|
+
const children = transformRoutes(route.children);
|
|
2573
|
+
if (children.length > 0) {
|
|
2574
|
+
item.children = children;
|
|
2575
|
+
delete item.path;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
result.push(item);
|
|
2579
|
+
}
|
|
2580
|
+
return result.sort((a, b) => {
|
|
2581
|
+
const sortA = findSort(routes, a.key);
|
|
2582
|
+
const sortB = findSort(routes, b.key);
|
|
2583
|
+
return sortA - sortB;
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
function findSort(routes, key) {
|
|
2587
|
+
var _a;
|
|
2588
|
+
for (const route of routes) {
|
|
2589
|
+
const routeKey = typeof route.name === "string" ? route.name : route.path;
|
|
2590
|
+
if (routeKey === key) {
|
|
2591
|
+
return ((_a = route.meta) == null ? void 0 : _a.sort) ?? Infinity;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
return Infinity;
|
|
2595
|
+
}
|
|
2596
|
+
function createRouterGuard(options) {
|
|
2597
|
+
const {
|
|
2598
|
+
auth,
|
|
2599
|
+
tabStore,
|
|
2600
|
+
loginPath = "/login",
|
|
2601
|
+
forbiddenPath = "/403"
|
|
2602
|
+
} = options;
|
|
2603
|
+
return (to) => {
|
|
2604
|
+
const meta = to.meta;
|
|
2605
|
+
if (to.path === loginPath && auth.isAuthenticated.value) {
|
|
2606
|
+
return "/";
|
|
2607
|
+
}
|
|
2608
|
+
if (meta.can) {
|
|
2609
|
+
const { resource, action } = meta.can;
|
|
2610
|
+
if (!auth.can(resource, action)) {
|
|
2611
|
+
if (to.path === forbiddenPath) return void 0;
|
|
2612
|
+
return forbiddenPath;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
const requiresAuth = meta.requiresAuth !== false;
|
|
2616
|
+
if (requiresAuth && !auth.isAuthenticated.value) {
|
|
2617
|
+
if (to.path === loginPath) return void 0;
|
|
2618
|
+
return loginPath;
|
|
2619
|
+
}
|
|
2620
|
+
if (tabStore) {
|
|
2621
|
+
addTabFromRoute(tabStore, to);
|
|
2622
|
+
}
|
|
2623
|
+
return void 0;
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
function addTabFromRoute(tabStore, route) {
|
|
2627
|
+
const meta = route.meta;
|
|
2628
|
+
const title = meta.title ?? (typeof route.name === "string" ? route.name : route.path);
|
|
2629
|
+
const name = typeof route.name === "string" ? route.name : route.path;
|
|
2630
|
+
tabStore.addTab({
|
|
2631
|
+
path: route.path,
|
|
2632
|
+
title,
|
|
2633
|
+
name,
|
|
2634
|
+
closable: true
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
function parseResponseHeaders(rawHeaders) {
|
|
2638
|
+
const headers = {};
|
|
2639
|
+
if (!rawHeaders) return headers;
|
|
2640
|
+
rawHeaders.split("\r\n").forEach((line) => {
|
|
2641
|
+
const idx = line.indexOf(":");
|
|
2642
|
+
if (idx > 0) {
|
|
2643
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
2644
|
+
const value = line.slice(idx + 1).trim();
|
|
2645
|
+
headers[key] = value;
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
return headers;
|
|
2649
|
+
}
|
|
2650
|
+
class XhrTransport {
|
|
2651
|
+
send(request) {
|
|
2652
|
+
return new Promise((resolve, reject) => {
|
|
2653
|
+
const xhr = new XMLHttpRequest();
|
|
2654
|
+
xhr.open(request.method, request.url, true);
|
|
2655
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
2656
|
+
xhr.setRequestHeader(key, value);
|
|
2657
|
+
}
|
|
2658
|
+
if (request.timeout !== void 0 && request.timeout > 0) {
|
|
2659
|
+
xhr.timeout = request.timeout;
|
|
2660
|
+
}
|
|
2661
|
+
if (request.onUploadProgress) {
|
|
2662
|
+
const onProgress = request.onUploadProgress;
|
|
2663
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
2664
|
+
if (event.lengthComputable) {
|
|
2665
|
+
onProgress({ loaded: event.loaded, total: event.total });
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
if (request.signal) {
|
|
2670
|
+
if (request.signal.aborted) {
|
|
2671
|
+
xhr.abort();
|
|
2672
|
+
reject({ type: "aborted" });
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
request.signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
2676
|
+
}
|
|
2677
|
+
xhr.onload = () => {
|
|
2678
|
+
const headers = parseResponseHeaders(xhr.getAllResponseHeaders());
|
|
2679
|
+
const responseText = xhr.responseText;
|
|
2680
|
+
const response = {
|
|
2681
|
+
status: xhr.status,
|
|
2682
|
+
statusText: xhr.statusText,
|
|
2683
|
+
headers,
|
|
2684
|
+
body: null,
|
|
2685
|
+
text: () => Promise.resolve(responseText),
|
|
2686
|
+
json: () => Promise.resolve(JSON.parse(responseText)),
|
|
2687
|
+
blob: () => Promise.resolve(new Blob([responseText]))
|
|
2688
|
+
};
|
|
2689
|
+
resolve(response);
|
|
2690
|
+
};
|
|
2691
|
+
xhr.onerror = () => {
|
|
2692
|
+
reject({ type: "unknown", error: new Error("网络错误") });
|
|
2693
|
+
};
|
|
2694
|
+
xhr.ontimeout = () => {
|
|
2695
|
+
reject({ type: "timeout" });
|
|
2696
|
+
};
|
|
2697
|
+
xhr.onabort = () => {
|
|
2698
|
+
reject({ type: "aborted" });
|
|
2699
|
+
};
|
|
2700
|
+
xhr.send(request.body);
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function topologicalSort(nodes) {
|
|
2705
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
2706
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
2707
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
2708
|
+
for (const node of nodes) {
|
|
2709
|
+
inDegree.set(node.id, 0);
|
|
2710
|
+
adjacency.set(node.id, []);
|
|
2711
|
+
}
|
|
2712
|
+
for (const node of nodes) {
|
|
2713
|
+
for (const dep of node.dependencies) {
|
|
2714
|
+
if (!nodeMap.has(dep)) {
|
|
2715
|
+
throw new Error(`[orchestrator] 节点 "${node.id}" 依赖的 "${dep}" 不存在`);
|
|
2716
|
+
}
|
|
2717
|
+
adjacency.get(dep).push(node.id);
|
|
2718
|
+
inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
const layers = [];
|
|
2722
|
+
let queue = nodes.filter((n) => inDegree.get(n.id) === 0).map((n) => n.id);
|
|
2723
|
+
while (queue.length > 0) {
|
|
2724
|
+
layers.push([...queue]);
|
|
2725
|
+
const nextQueue = [];
|
|
2726
|
+
for (const id of queue) {
|
|
2727
|
+
for (const neighbor of adjacency.get(id)) {
|
|
2728
|
+
const degree = inDegree.get(neighbor) - 1;
|
|
2729
|
+
inDegree.set(neighbor, degree);
|
|
2730
|
+
if (degree === 0) {
|
|
2731
|
+
nextQueue.push(neighbor);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
queue = nextQueue;
|
|
2736
|
+
}
|
|
2737
|
+
const totalSorted = layers.reduce((sum, layer) => sum + layer.length, 0);
|
|
2738
|
+
if (totalSorted !== nodes.length) {
|
|
2739
|
+
throw new Error("[orchestrator] 检测到循环依赖");
|
|
2740
|
+
}
|
|
2741
|
+
return layers;
|
|
2742
|
+
}
|
|
2743
|
+
async function orchestrate(nodes, failureStrategy = "failFast") {
|
|
2744
|
+
if (nodes.length === 0) {
|
|
2745
|
+
return { results: /* @__PURE__ */ new Map(), errors: /* @__PURE__ */ new Map() };
|
|
2746
|
+
}
|
|
2747
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
2748
|
+
const layers = topologicalSort(nodes);
|
|
2749
|
+
const results = /* @__PURE__ */ new Map();
|
|
2750
|
+
const errors = /* @__PURE__ */ new Map();
|
|
2751
|
+
for (const layer of layers) {
|
|
2752
|
+
const tasks = layer.map(async (id) => {
|
|
2753
|
+
const node = nodeMap.get(id);
|
|
2754
|
+
try {
|
|
2755
|
+
const result = await node.execute();
|
|
2756
|
+
results.set(id, result);
|
|
2757
|
+
} catch (e) {
|
|
2758
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
2759
|
+
errors.set(id, error);
|
|
2760
|
+
if (failureStrategy === "failFast") {
|
|
2761
|
+
throw error;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
if (failureStrategy === "failFast") {
|
|
2766
|
+
await Promise.all(tasks);
|
|
2767
|
+
} else {
|
|
2768
|
+
await Promise.allSettled(tasks);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return { results, errors };
|
|
2772
|
+
}
|
|
2773
|
+
function createNode(id, execute, dependencies = []) {
|
|
2774
|
+
return { id, execute, dependencies };
|
|
2775
|
+
}
|
|
2776
|
+
function after(...deps) {
|
|
2777
|
+
return (id, execute) => {
|
|
2778
|
+
return { id, execute, dependencies: deps };
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
function useRequest(client) {
|
|
2782
|
+
const controllers = [];
|
|
2783
|
+
onUnmounted(() => {
|
|
2784
|
+
for (const controller of controllers) {
|
|
2785
|
+
controller.abort("组件已卸载");
|
|
2786
|
+
}
|
|
2787
|
+
controllers.length = 0;
|
|
2788
|
+
});
|
|
2789
|
+
const proxiedClient = {
|
|
2790
|
+
...client,
|
|
2791
|
+
registerAbortController: (controller) => {
|
|
2792
|
+
controllers.push(controller);
|
|
2793
|
+
client.registerAbortController(controller);
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
return proxiedClient;
|
|
2797
|
+
}
|
|
2798
|
+
function usePolling(fn, interval) {
|
|
2799
|
+
const data = ref();
|
|
2800
|
+
const isPolling = ref(false);
|
|
2801
|
+
const error = ref(null);
|
|
2802
|
+
let timer = null;
|
|
2803
|
+
async function tick() {
|
|
2804
|
+
try {
|
|
2805
|
+
const result = await fn();
|
|
2806
|
+
data.value = result;
|
|
2807
|
+
error.value = null;
|
|
2808
|
+
} catch (e) {
|
|
2809
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
2810
|
+
}
|
|
2811
|
+
if (isPolling.value) {
|
|
2812
|
+
timer = setTimeout(tick, interval);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
function start() {
|
|
2816
|
+
if (isPolling.value) return;
|
|
2817
|
+
isPolling.value = true;
|
|
2818
|
+
tick();
|
|
2819
|
+
}
|
|
2820
|
+
function stop() {
|
|
2821
|
+
isPolling.value = false;
|
|
2822
|
+
if (timer !== null) {
|
|
2823
|
+
clearTimeout(timer);
|
|
2824
|
+
timer = null;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
onUnmounted(stop);
|
|
2828
|
+
return { data, isPolling, error, start, stop };
|
|
2829
|
+
}
|
|
2830
|
+
function useBatchLoader(batchFn, options = {}) {
|
|
2831
|
+
const windowMs = options.windowMs ?? 16;
|
|
2832
|
+
const maxBatchSize = options.maxBatchSize ?? 100;
|
|
2833
|
+
let queue = [];
|
|
2834
|
+
let timer = null;
|
|
2835
|
+
async function executeBatch(batch) {
|
|
2836
|
+
try {
|
|
2837
|
+
const keys = batch.map((e) => e.key);
|
|
2838
|
+
const values = await batchFn(keys);
|
|
2839
|
+
if (values.length !== batch.length) {
|
|
2840
|
+
const error = new Error(`[useBatchLoader] 批量函数返回 ${values.length} 个结果,期望 ${batch.length} 个`);
|
|
2841
|
+
batch.forEach((e) => e.reject(error));
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
batch.forEach((entry, i) => entry.resolve(values[i]));
|
|
2845
|
+
} catch (e) {
|
|
2846
|
+
batch.forEach((entry) => entry.reject(e));
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function scheduleFlush() {
|
|
2850
|
+
if (timer !== null) return;
|
|
2851
|
+
timer = setTimeout(() => {
|
|
2852
|
+
timer = null;
|
|
2853
|
+
flush();
|
|
2854
|
+
}, windowMs);
|
|
2855
|
+
}
|
|
2856
|
+
function flush() {
|
|
2857
|
+
if (timer !== null) {
|
|
2858
|
+
clearTimeout(timer);
|
|
2859
|
+
timer = null;
|
|
2860
|
+
}
|
|
2861
|
+
while (queue.length > 0) {
|
|
2862
|
+
const batch = queue.splice(0, maxBatchSize);
|
|
2863
|
+
executeBatch(batch);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
function load(key) {
|
|
2867
|
+
return new Promise((resolve, reject) => {
|
|
2868
|
+
queue.push({ key, resolve, reject });
|
|
2869
|
+
if (queue.length >= maxBatchSize) {
|
|
2870
|
+
flush();
|
|
2871
|
+
} else {
|
|
2872
|
+
scheduleFlush();
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
onUnmounted(() => {
|
|
2877
|
+
if (timer !== null) {
|
|
2878
|
+
clearTimeout(timer);
|
|
2879
|
+
timer = null;
|
|
2880
|
+
}
|
|
2881
|
+
queue.forEach((e) => e.reject(new Error("组件已卸载")));
|
|
2882
|
+
queue = [];
|
|
2883
|
+
});
|
|
2884
|
+
return { load, flush };
|
|
2885
|
+
}
|
|
2886
|
+
function createUploader(uploaderOptions = {}) {
|
|
2887
|
+
const transport = new XhrTransport();
|
|
2888
|
+
function buildURL(url) {
|
|
2889
|
+
if (url.startsWith("http")) return url;
|
|
2890
|
+
return `${uploaderOptions.baseURL ?? ""}${url}`;
|
|
2891
|
+
}
|
|
2892
|
+
function buildHeaders(options) {
|
|
2893
|
+
var _a;
|
|
2894
|
+
const headers = { ...uploaderOptions.headers, ...options.headers };
|
|
2895
|
+
const token = (_a = uploaderOptions.getToken) == null ? void 0 : _a.call(uploaderOptions);
|
|
2896
|
+
if (token) {
|
|
2897
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
2898
|
+
}
|
|
2899
|
+
return headers;
|
|
2900
|
+
}
|
|
2901
|
+
function doUpload(formData, options) {
|
|
2902
|
+
const progress = ref(0);
|
|
2903
|
+
const abortController = new AbortController();
|
|
2904
|
+
const promise = transport.send({
|
|
2905
|
+
url: buildURL(options.url),
|
|
2906
|
+
method: "POST",
|
|
2907
|
+
headers: buildHeaders(options),
|
|
2908
|
+
body: formData,
|
|
2909
|
+
signal: abortController.signal,
|
|
2910
|
+
onUploadProgress: (event) => {
|
|
2911
|
+
var _a;
|
|
2912
|
+
const percent = event.total > 0 ? Math.round(event.loaded / event.total * 100) : 0;
|
|
2913
|
+
progress.value = percent;
|
|
2914
|
+
(_a = options.onProgress) == null ? void 0 : _a.call(options, percent);
|
|
2915
|
+
}
|
|
2916
|
+
}).then(async (response) => {
|
|
2917
|
+
if (response.status < 200 || response.status >= 300) {
|
|
2918
|
+
throw new NetworkRequestError({
|
|
2919
|
+
type: "httpError",
|
|
2920
|
+
status: response.status,
|
|
2921
|
+
statusText: response.statusText
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
const contentType = response.headers["content-type"] ?? "";
|
|
2925
|
+
if (contentType.includes("application/json")) {
|
|
2926
|
+
const json = await response.json();
|
|
2927
|
+
if (isWrappedResponse(json)) {
|
|
2928
|
+
if (!json.success) throw new BusinessError(json.message || "上传失败");
|
|
2929
|
+
return json.data;
|
|
2930
|
+
}
|
|
2931
|
+
return json;
|
|
2932
|
+
}
|
|
2933
|
+
return await response.text();
|
|
2934
|
+
});
|
|
2935
|
+
return {
|
|
2936
|
+
progress,
|
|
2937
|
+
abort: () => abortController.abort("上传已取消"),
|
|
2938
|
+
promise
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
return {
|
|
2942
|
+
upload(file, options) {
|
|
2943
|
+
const fieldName = options.fieldName ?? "file";
|
|
2944
|
+
const formData = new FormData();
|
|
2945
|
+
formData.append(fieldName, file);
|
|
2946
|
+
if (options.data) {
|
|
2947
|
+
for (const [key, value] of Object.entries(options.data)) {
|
|
2948
|
+
formData.append(key, value);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
return doUpload(formData, options);
|
|
2952
|
+
},
|
|
2953
|
+
uploadMultiple(files, options) {
|
|
2954
|
+
const fieldName = options.fieldName ?? "files";
|
|
2955
|
+
const formData = new FormData();
|
|
2956
|
+
files.forEach((file) => formData.append(fieldName, file));
|
|
2957
|
+
if (options.data) {
|
|
2958
|
+
for (const [key, value] of Object.entries(options.data)) {
|
|
2959
|
+
formData.append(key, value);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return doUpload(formData, options);
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
function isWrappedResponse(value) {
|
|
2967
|
+
return typeof value === "object" && value !== null && "success" in value && "data" in value && typeof value.success === "boolean";
|
|
2968
|
+
}
|
|
2969
|
+
function defineUserStore(options) {
|
|
2970
|
+
const { loginApi, onLoginSuccess, onLogoutSuccess } = options;
|
|
2971
|
+
const useStore = defineStore("kine-user", () => {
|
|
2972
|
+
const auth = useAuth();
|
|
2973
|
+
const user = computed(() => auth.user.value);
|
|
2974
|
+
const isLoggedIn = computed(() => auth.isAuthenticated.value);
|
|
2975
|
+
const token = computed(() => auth.token.value);
|
|
2976
|
+
const permissions = computed(() => auth.permissions.value);
|
|
2977
|
+
function can(resource, action, scope) {
|
|
2978
|
+
return auth.can(resource, action, scope);
|
|
2979
|
+
}
|
|
2980
|
+
async function login(account, password) {
|
|
2981
|
+
const result = await loginApi({ account, password });
|
|
2982
|
+
auth.loginWith(result);
|
|
2983
|
+
onLoginSuccess == null ? void 0 : onLoginSuccess();
|
|
2984
|
+
}
|
|
2985
|
+
function logout() {
|
|
2986
|
+
auth.logout();
|
|
2987
|
+
onLogoutSuccess == null ? void 0 : onLogoutSuccess();
|
|
2988
|
+
}
|
|
2989
|
+
async function initUser() {
|
|
2990
|
+
await auth.restore();
|
|
2991
|
+
}
|
|
2992
|
+
return { user, isLoggedIn, token, permissions, can, login, logout, initUser };
|
|
2993
|
+
});
|
|
2994
|
+
return useStore;
|
|
2995
|
+
}
|
|
2996
|
+
export {
|
|
2997
|
+
BusinessError,
|
|
2998
|
+
ControlGate,
|
|
2999
|
+
FetchTransport,
|
|
3000
|
+
KContent,
|
|
3001
|
+
KFileList,
|
|
3002
|
+
KHeader,
|
|
3003
|
+
KImageUpload,
|
|
3004
|
+
KLayout,
|
|
3005
|
+
KLoginPage,
|
|
3006
|
+
KNavMenu,
|
|
3007
|
+
KPageHeader,
|
|
3008
|
+
KSearchTable,
|
|
3009
|
+
KSider,
|
|
3010
|
+
KUpload,
|
|
3011
|
+
LAYOUT_COLLAPSED_KEY,
|
|
3012
|
+
LAYOUT_COLLAPSED_WIDTH_KEY,
|
|
3013
|
+
LAYOUT_SIDER_WIDTH_KEY,
|
|
3014
|
+
LAYOUT_TOGGLE_KEY,
|
|
3015
|
+
NetworkRequestError,
|
|
3016
|
+
REQUEST_CLIENT_KEY,
|
|
3017
|
+
RequestBuilder,
|
|
3018
|
+
XhrTransport,
|
|
3019
|
+
addTabFromRoute,
|
|
3020
|
+
after,
|
|
3021
|
+
createAuth,
|
|
3022
|
+
createAuthGuard,
|
|
3023
|
+
createCrudApp,
|
|
3024
|
+
createDefaultRequest,
|
|
3025
|
+
createNode,
|
|
3026
|
+
createPermissiveRequest,
|
|
3027
|
+
createQueryClient,
|
|
3028
|
+
createRequest,
|
|
3029
|
+
createRouterGuard,
|
|
3030
|
+
createStrictRequest,
|
|
3031
|
+
createTabStore,
|
|
3032
|
+
createUploader,
|
|
3033
|
+
createVCan,
|
|
3034
|
+
defineCrudRoutes,
|
|
3035
|
+
defineRepository,
|
|
3036
|
+
defineUserStore,
|
|
3037
|
+
orchestrate,
|
|
3038
|
+
setupCrud,
|
|
3039
|
+
useAuth,
|
|
3040
|
+
useBatchLoader,
|
|
3041
|
+
useMenuFromRoutes,
|
|
3042
|
+
usePolling,
|
|
3043
|
+
useRequest,
|
|
3044
|
+
useRequestClient,
|
|
3045
|
+
useTabStore
|
|
3046
|
+
};
|