befly-admin 3.10.4 → 3.11.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/index.html +1 -1
- package/package.json +5 -4
- package/src/layouts/default.vue +2 -2
- package/src/main.ts +17 -0
- package/src/plugins/{config.js → config.ts} +12 -10
- package/src/plugins/{global.js → global.ts} +5 -4
- package/src/plugins/http.ts +180 -0
- package/src/plugins/{router.js → router.ts} +7 -6
- package/src/plugins/storage.ts +158 -0
- package/src/types/auto-imports.d.ts +18 -15
- package/src/types/befly-vite.d.ts +9 -0
- package/src/types/shims-vue.d.ts +6 -0
- package/src/types/vite-env.d.ts +1 -0
- package/src/main.js +0 -47
- package/src/plugins/http.js +0 -86
- package/src/plugins/storage.js +0 -137
- package/src/utils/arrayToTree.ts +0 -135
package/index.html
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly-admin",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.11.1",
|
|
4
|
+
"gitHead": "540d9551c589cb33d711c593e561f0fffc0aac0c",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly Admin - 基于 Vue3 + TDesign Vue Next 的后台管理系统",
|
|
7
7
|
"files": [
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
"preview": "vite preview"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@befly-addon/admin": "^1.
|
|
32
|
-
"@iconify-json/lucide": "^1.2.
|
|
31
|
+
"@befly-addon/admin": "^1.7.1",
|
|
32
|
+
"@iconify-json/lucide": "^1.2.84",
|
|
33
33
|
"axios": "^1.13.2",
|
|
34
|
+
"befly-shared": "1.4.2",
|
|
34
35
|
"befly-vite": "^1.4.4",
|
|
35
36
|
"pinia": "^3.0.4",
|
|
36
37
|
"tdesign-vue-next": "^1.18.0",
|
package/src/layouts/default.vue
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
|
|
77
77
|
<script setup lang="ts">
|
|
78
78
|
import { DialogPlugin } from "tdesign-vue-next";
|
|
79
|
-
import { arrayToTree } from "
|
|
79
|
+
import { arrayToTree } from "befly-shared/utils/arrayToTree";
|
|
80
80
|
|
|
81
81
|
const router = useRouter();
|
|
82
82
|
const route = useRoute();
|
|
@@ -135,7 +135,7 @@ const $Method = {
|
|
|
135
135
|
// 获取用户菜单权限
|
|
136
136
|
async fetchUserMenus() {
|
|
137
137
|
try {
|
|
138
|
-
const { data } = await $Http("/addon/admin/menu/all");
|
|
138
|
+
const { data } = await $Http.post("/addon/admin/menu/all");
|
|
139
139
|
const lists = Array.isArray(data?.lists) ? data.lists : [];
|
|
140
140
|
|
|
141
141
|
const bizMenus = [];
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// 引入 TDesign 样式
|
|
2
|
+
import "tdesign-vue-next/es/style/index.css";
|
|
3
|
+
// 引入 addonAdmin 的 CSS 变量
|
|
4
|
+
import "@befly-addon/admin/styles/variables.scss";
|
|
5
|
+
// 引入全局基础样式(reset、通用类、滚动条等)
|
|
6
|
+
import "./styles/global.scss";
|
|
7
|
+
import App from "./App.vue";
|
|
8
|
+
|
|
9
|
+
const app = createApp(App);
|
|
10
|
+
|
|
11
|
+
// 安装基础插件
|
|
12
|
+
app.use(createPinia());
|
|
13
|
+
|
|
14
|
+
// 使用路由
|
|
15
|
+
app.use($Router);
|
|
16
|
+
|
|
17
|
+
app.mount("#app");
|
|
@@ -3,27 +3,29 @@
|
|
|
3
3
|
* 存放框架内置的配置变量,不建议修改
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export type BeflyAdminConfig = {
|
|
7
|
+
appTitle: string;
|
|
8
|
+
apiBaseUrl: string;
|
|
9
|
+
uploadUrl: string;
|
|
10
|
+
storageNamespace: string;
|
|
11
|
+
loginPath: string;
|
|
12
|
+
homePath: string;
|
|
13
|
+
isDev: boolean;
|
|
14
|
+
isProd: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
6
17
|
/**
|
|
7
18
|
* 内置配置对象
|
|
8
19
|
*/
|
|
9
|
-
export const $Config = {
|
|
10
|
-
/** 应用标题 */
|
|
20
|
+
export const $Config: BeflyAdminConfig = {
|
|
11
21
|
appTitle: import.meta.env.VITE_APP_TITLE || "野蜂飞舞",
|
|
12
|
-
/** API 基础地址 */
|
|
13
22
|
apiBaseUrl: import.meta.env.VITE_API_BASE_URL || "",
|
|
14
23
|
|
|
15
|
-
/** 上传地址(优先使用 VITE_UPLOAD_URL;否则基于 VITE_API_BASE_URL 拼接 /upload) */
|
|
16
24
|
uploadUrl: import.meta.env.VITE_UPLOAD_URL || ((import.meta.env.VITE_API_BASE_URL || "").length > 0 ? `${import.meta.env.VITE_API_BASE_URL}/upload` : "/upload"),
|
|
17
|
-
/** 存储命名空间 */
|
|
18
25
|
storageNamespace: import.meta.env.VITE_STORAGE_NAMESPACE || "befly_admin",
|
|
19
26
|
|
|
20
|
-
/** 登录页路径(可通过 VITE_LOGIN_PATH 覆盖) */
|
|
21
27
|
loginPath: import.meta.env.VITE_LOGIN_PATH || "/addon/admin/login",
|
|
22
|
-
|
|
23
|
-
/** 首页路径(可通过 VITE_HOME_PATH 覆盖) */
|
|
24
28
|
homePath: import.meta.env.VITE_HOME_PATH || "/addon/admin",
|
|
25
|
-
/** 是否开发环境 */
|
|
26
29
|
isDev: import.meta.env.DEV,
|
|
27
|
-
/** 是否生产环境 */
|
|
28
30
|
isProd: import.meta.env.PROD
|
|
29
31
|
};
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
* 全局状态管理
|
|
3
3
|
* 集中管理所有全局数据,避免分散到多个 store 文件
|
|
4
4
|
*/
|
|
5
|
+
|
|
5
6
|
export const useGlobal = defineStore("global", () => {
|
|
6
7
|
// ==================== 全局数据 ====================
|
|
7
|
-
const data =
|
|
8
|
+
const data = reactive<Record<string, unknown>>({});
|
|
8
9
|
|
|
9
10
|
// ==================== 全局方法 ====================
|
|
10
|
-
const method = {};
|
|
11
|
+
const method: Record<string, unknown> = {};
|
|
11
12
|
|
|
12
13
|
// ==================== 返回 ====================
|
|
13
14
|
return {
|
|
14
|
-
data,
|
|
15
|
-
method
|
|
15
|
+
data: data,
|
|
16
|
+
method: method
|
|
16
17
|
};
|
|
17
18
|
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import axios, { AxiosHeaders, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from "axios";
|
|
2
|
+
import { cleanParams } from "befly-shared/utils/cleanParams";
|
|
3
|
+
|
|
4
|
+
import { $Storage } from "./storage";
|
|
5
|
+
|
|
6
|
+
export type HttpApiResponse<TData> = {
|
|
7
|
+
code: number;
|
|
8
|
+
msg?: string;
|
|
9
|
+
data?: TData;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type HttpCleanParamsOptions = {
|
|
13
|
+
dropValues?: readonly unknown[];
|
|
14
|
+
dropKeyValue?: Record<string, readonly unknown[]>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type HttpClientOptions = AxiosRequestConfig & HttpCleanParamsOptions;
|
|
18
|
+
|
|
19
|
+
export type HttpGetData = Record<string, unknown>;
|
|
20
|
+
export type HttpPostData = Record<string, unknown> | FormData;
|
|
21
|
+
|
|
22
|
+
function toAxiosRequestConfig(options: HttpClientOptions | undefined): AxiosRequestConfig | undefined {
|
|
23
|
+
if (!options) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const out = Object.assign({}, options) as Record<string, unknown>;
|
|
28
|
+
delete out["dropValues"];
|
|
29
|
+
delete out["dropKeyValue"];
|
|
30
|
+
|
|
31
|
+
if (Object.keys(out).length === 0) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return out as AxiosRequestConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
if (typeof value !== "object" || value === null) return false;
|
|
40
|
+
if (Array.isArray(value)) return false;
|
|
41
|
+
if (value instanceof FormData) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function maybeCleanRequestData(data: Record<string, unknown>, cleanOptions: HttpCleanParamsOptions | undefined): Record<string, unknown> {
|
|
46
|
+
if (!isPlainRecord(data)) {
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dropValues = cleanOptions?.dropValues;
|
|
51
|
+
const dropKeyValue = cleanOptions?.dropKeyValue;
|
|
52
|
+
return cleanParams(data, dropValues ?? [], dropKeyValue);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class HttpError extends Error {
|
|
56
|
+
public code: number;
|
|
57
|
+
public data?: unknown;
|
|
58
|
+
public rawError?: unknown;
|
|
59
|
+
|
|
60
|
+
public constructor(code: number, msg: string, data?: unknown, rawError?: unknown) {
|
|
61
|
+
super(msg);
|
|
62
|
+
this.name = "HttpError";
|
|
63
|
+
this.code = code;
|
|
64
|
+
this.data = data;
|
|
65
|
+
this.rawError = rawError;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isNormalizedHttpError(value: unknown): value is HttpError {
|
|
70
|
+
return value instanceof HttpError;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function showNetworkErrorToast(): Promise<void> {
|
|
74
|
+
// 在测试/非浏览器环境下,tdesign-vue-next 可能不可用;仅在需要展示提示时再加载。
|
|
75
|
+
try {
|
|
76
|
+
const mod = await import("tdesign-vue-next");
|
|
77
|
+
mod.MessagePlugin.error("网络连接失败");
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function unwrapApiResponse<TData>(promise: Promise<AxiosResponse<HttpApiResponse<TData>>>): Promise<HttpApiResponse<TData>> {
|
|
84
|
+
try {
|
|
85
|
+
const response = await promise;
|
|
86
|
+
const res = response.data;
|
|
87
|
+
|
|
88
|
+
if (res.code !== 0) {
|
|
89
|
+
throw new HttpError(res.code, res.msg || "请求失败", res.data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return res;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// 业务错误:不显示提示,由业务层处理
|
|
95
|
+
if (isNormalizedHttpError(error)) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await showNetworkErrorToast();
|
|
100
|
+
throw new HttpError(-1, "网络连接失败", undefined, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 创建 axios 实例
|
|
105
|
+
const request = axios.create({
|
|
106
|
+
baseURL: (import.meta as unknown as { env?: Record<string, string> }).env?.VITE_API_BASE_URL || "",
|
|
107
|
+
timeout: 10000,
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/json"
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 请求拦截器
|
|
114
|
+
request.interceptors.request.use(
|
|
115
|
+
(config: InternalAxiosRequestConfig) => {
|
|
116
|
+
const token = $Storage.local.get("token");
|
|
117
|
+
if (token) {
|
|
118
|
+
const headers = new AxiosHeaders(config.headers);
|
|
119
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
120
|
+
config.headers = headers;
|
|
121
|
+
}
|
|
122
|
+
return config;
|
|
123
|
+
},
|
|
124
|
+
(error: unknown) => {
|
|
125
|
+
return Promise.reject(error);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
async function httpGet<TData>(url: string, data?: HttpGetData, options?: HttpClientOptions): Promise<HttpApiResponse<TData>> {
|
|
130
|
+
const axiosConfig = toAxiosRequestConfig(options);
|
|
131
|
+
const inputData = data ?? {};
|
|
132
|
+
const cleanedData = maybeCleanRequestData(inputData, options);
|
|
133
|
+
|
|
134
|
+
// 规则:GET 必须传 params;为空也传空对象
|
|
135
|
+
const finalConfig = Object.assign({}, axiosConfig);
|
|
136
|
+
(finalConfig as { params?: unknown }).params = cleanedData;
|
|
137
|
+
|
|
138
|
+
return unwrapApiResponse<TData>(request.get<HttpApiResponse<TData>>(url, finalConfig));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function httpPost<TData>(url: string, data?: HttpPostData, options?: HttpClientOptions): Promise<HttpApiResponse<TData>> {
|
|
142
|
+
const axiosConfig = toAxiosRequestConfig(options);
|
|
143
|
+
if (data === undefined) {
|
|
144
|
+
// 规则:POST 必须传 body;为空也传空对象
|
|
145
|
+
return unwrapApiResponse<TData>(request.post<HttpApiResponse<TData>>(url, {}, axiosConfig));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (data instanceof FormData) {
|
|
149
|
+
return unwrapApiResponse<TData>(request.post<HttpApiResponse<TData>>(url, data, axiosConfig));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const cleanedData = maybeCleanRequestData(data, options);
|
|
153
|
+
if (Object.keys(cleanedData).length === 0) {
|
|
154
|
+
// 规则:POST 必须传 body;清洗为空则传空对象
|
|
155
|
+
return unwrapApiResponse<TData>(request.post<HttpApiResponse<TData>>(url, {}, axiosConfig));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return unwrapApiResponse<TData>(request.post<HttpApiResponse<TData>>(url, cleanedData, axiosConfig));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 统一的 HTTP 请求对象(仅支持 GET 和 POST)
|
|
163
|
+
* - 调用方式:$Http.get(url, data?, options?) / $Http.post(url, data?, options?)
|
|
164
|
+
* - 重要行为:
|
|
165
|
+
* - 未传 data / 清洗为空时:仍会发送空对象(GET: params={}, POST: body={})
|
|
166
|
+
* - 原因:部分后端接口会基于“参数结构存在”触发默认逻辑/签名校验/中间件约束;
|
|
167
|
+
* 因此这里不做“省略空对象”的优化。
|
|
168
|
+
* - 传入 plain object 时:默认强制移除 null / undefined
|
|
169
|
+
* - 可选参数清洗(第三参,且可与 axios config 混用):
|
|
170
|
+
* - dropValues:全局丢弃值列表(仅对未配置 dropKeyValue 的 key 生效)
|
|
171
|
+
* - dropKeyValue:按 key 精确配置丢弃值列表(覆盖全局 dropValues)
|
|
172
|
+
*
|
|
173
|
+
* 例子:保留 page=0,但丢弃 keyword="",并且其它字段应用全局 dropValues
|
|
174
|
+
* - dropValues: [0, ""]
|
|
175
|
+
* - dropKeyValue: { page: [], keyword: [""] }
|
|
176
|
+
*/
|
|
177
|
+
export const $Http = {
|
|
178
|
+
get: httpGet,
|
|
179
|
+
post: httpPost
|
|
180
|
+
};
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { $Storage } from "@/plugins/storage";
|
|
2
1
|
import { Layouts } from "befly-vite";
|
|
3
2
|
import { createRouter, createWebHashHistory } from "vue-router";
|
|
4
3
|
import { routes } from "vue-router/auto-routes";
|
|
5
4
|
|
|
5
|
+
import { $Storage } from "./storage";
|
|
6
|
+
|
|
6
7
|
// 应用自定义布局系统(同时可选注入根路径重定向)
|
|
7
|
-
const finalRoutes = Layouts(routes, $Config.homePath, (layoutName) => {
|
|
8
|
+
const finalRoutes = Layouts(routes, $Config.homePath, (layoutName: string) => {
|
|
8
9
|
if (layoutName === "default") {
|
|
9
|
-
return () => import("
|
|
10
|
+
return () => import("../layouts/default.vue");
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
return () => import(
|
|
13
|
+
return () => import(`../layouts/${layoutName}.vue`);
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 创建并导出路由实例
|
|
17
|
-
* 可直接在 main.
|
|
18
|
+
* 可直接在 main.ts 中使用 app.use($Router)
|
|
18
19
|
*/
|
|
19
20
|
export const $Router = createRouter({
|
|
20
21
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
|
@@ -32,7 +33,7 @@ $Router.beforeEach((to, _from, next) => {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// 公开路由放行
|
|
35
|
-
if (to.meta?.public === true) {
|
|
36
|
+
if (to.meta?.["public"] === true) {
|
|
36
37
|
return next();
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage 封装
|
|
3
|
+
* 支持命名空间隔离,统一管理 localStorage 和 sessionStorage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 获取命名空间
|
|
7
|
+
const NAMESPACE = (import.meta as unknown as { env?: Record<string, string> }).env?.VITE_STORAGE_NAMESPACE || "befly";
|
|
8
|
+
|
|
9
|
+
class MemoryStorage implements Storage {
|
|
10
|
+
private map: Map<string, string>;
|
|
11
|
+
|
|
12
|
+
public constructor() {
|
|
13
|
+
this.map = new Map<string, string>();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public get length(): number {
|
|
17
|
+
return this.map.size;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public clear(): void {
|
|
21
|
+
for (const key of this.map.keys()) {
|
|
22
|
+
delete (this as unknown as Record<string, unknown>)[key];
|
|
23
|
+
}
|
|
24
|
+
this.map.clear();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public getItem(key: string): string | null {
|
|
28
|
+
return this.map.get(key) ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public key(index: number): string | null {
|
|
32
|
+
const keys = Array.from(this.map.keys());
|
|
33
|
+
return keys[index] ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public removeItem(key: string): void {
|
|
37
|
+
this.map.delete(key);
|
|
38
|
+
delete (this as unknown as Record<string, unknown>)[key];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public setItem(key: string, value: string): void {
|
|
42
|
+
this.map.set(key, value);
|
|
43
|
+
(this as unknown as Record<string, unknown>)[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getBrowserStorage(kind: "localStorage" | "sessionStorage"): Storage {
|
|
48
|
+
const win = globalThis as unknown as { window?: { localStorage?: Storage; sessionStorage?: Storage } };
|
|
49
|
+
const storage = win.window?.[kind];
|
|
50
|
+
if (storage) {
|
|
51
|
+
return storage;
|
|
52
|
+
}
|
|
53
|
+
return new MemoryStorage();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type StorageOps = {
|
|
57
|
+
set: (key: string, value: unknown) => void;
|
|
58
|
+
get: {
|
|
59
|
+
(key: string): string | null;
|
|
60
|
+
(key: string, defaultValue: string | null): string | null;
|
|
61
|
+
<T>(key: string, defaultValue: T): T;
|
|
62
|
+
};
|
|
63
|
+
remove: (key: string) => void;
|
|
64
|
+
clear: () => void;
|
|
65
|
+
has: (key: string) => boolean;
|
|
66
|
+
keys: () => string[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 存储操作类
|
|
71
|
+
*/
|
|
72
|
+
class StorageManager {
|
|
73
|
+
private localStorage: Storage;
|
|
74
|
+
private sessionStorage: Storage;
|
|
75
|
+
private namespace: string;
|
|
76
|
+
|
|
77
|
+
public constructor(namespace: string = NAMESPACE) {
|
|
78
|
+
this.localStorage = getBrowserStorage("localStorage");
|
|
79
|
+
this.sessionStorage = getBrowserStorage("sessionStorage");
|
|
80
|
+
this.namespace = namespace;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private getKey(key: string): string {
|
|
84
|
+
return `${this.namespace}:${key}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private createStorageOps(storage: Storage): StorageOps {
|
|
88
|
+
return {
|
|
89
|
+
set: (key: string, value: unknown) => {
|
|
90
|
+
try {
|
|
91
|
+
const fullKey = this.getKey(key);
|
|
92
|
+
const serializedValue = JSON.stringify(value);
|
|
93
|
+
storage.setItem(fullKey, serializedValue);
|
|
94
|
+
} catch {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
get: ((key: string, defaultValue?: unknown) => {
|
|
100
|
+
try {
|
|
101
|
+
const fullKey = this.getKey(key);
|
|
102
|
+
const value = storage.getItem(fullKey);
|
|
103
|
+
if (value === null) {
|
|
104
|
+
return defaultValue ?? null;
|
|
105
|
+
}
|
|
106
|
+
return JSON.parse(value) as unknown;
|
|
107
|
+
} catch {
|
|
108
|
+
return defaultValue ?? null;
|
|
109
|
+
}
|
|
110
|
+
}) as StorageOps["get"],
|
|
111
|
+
|
|
112
|
+
remove: (key: string) => {
|
|
113
|
+
try {
|
|
114
|
+
const fullKey = this.getKey(key);
|
|
115
|
+
storage.removeItem(fullKey);
|
|
116
|
+
} catch {
|
|
117
|
+
// ignore
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
clear: () => {
|
|
122
|
+
try {
|
|
123
|
+
const keys = Object.keys(storage);
|
|
124
|
+
const prefix = `${this.namespace}:`;
|
|
125
|
+
keys.forEach((key) => {
|
|
126
|
+
if (key.startsWith(prefix)) {
|
|
127
|
+
storage.removeItem(key);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
has: (key: string) => {
|
|
136
|
+
const fullKey = this.getKey(key);
|
|
137
|
+
return storage.getItem(fullKey) !== null;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
keys: () => {
|
|
141
|
+
const keys = Object.keys(storage);
|
|
142
|
+
const prefix = `${this.namespace}:`;
|
|
143
|
+
return keys.filter((key) => key.startsWith(prefix)).map((key) => key.substring(prefix.length));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public get local(): StorageOps {
|
|
149
|
+
return this.createStorageOps(this.localStorage);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public get session(): StorageOps {
|
|
153
|
+
return this.createStorageOps(this.sessionStorage);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 导出单例
|
|
158
|
+
export const $Storage = new StorageManager();
|
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
// biome-ignore lint: disable
|
|
7
7
|
export {}
|
|
8
8
|
declare global {
|
|
9
|
-
const $Config: typeof import('../plugins/config
|
|
10
|
-
const $Http: typeof import('../plugins/http
|
|
11
|
-
const $Router: typeof import('../plugins/router
|
|
12
|
-
const $Storage: typeof import('../plugins/storage
|
|
9
|
+
const $Config: typeof import('../plugins/config').$Config
|
|
10
|
+
const $Http: typeof import('../plugins/http').$Http
|
|
11
|
+
const $Router: typeof import('../plugins/router').$Router
|
|
12
|
+
const $Storage: typeof import('../plugins/storage').$Storage
|
|
13
13
|
const DialogPlugin: typeof import("tdesign-vue-next").DialogPlugin
|
|
14
14
|
const EffectScope: typeof import('vue').EffectScope
|
|
15
15
|
const MessagePlugin: typeof import('tdesign-vue-next').MessagePlugin
|
|
16
16
|
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
|
17
|
-
const arrayToTree: typeof import('
|
|
17
|
+
const arrayToTree: typeof import('befly-shared/utils/arrayToTree').arrayToTree
|
|
18
|
+
const cleanParams: typeof import('befly-shared/utils/cleanParams').cleanParams
|
|
18
19
|
const computed: typeof import('vue').computed
|
|
19
20
|
const createApp: typeof import('vue').createApp
|
|
20
21
|
const createPinia: typeof import('pinia').createPinia
|
|
@@ -62,7 +63,7 @@ declare global {
|
|
|
62
63
|
const readonly: typeof import('vue').readonly
|
|
63
64
|
const ref: typeof import('vue').ref
|
|
64
65
|
const resolveComponent: typeof import('vue').resolveComponent
|
|
65
|
-
const router: typeof import('../plugins/router
|
|
66
|
+
const router: typeof import('../plugins/router').router
|
|
66
67
|
const setActivePinia: typeof import('pinia').setActivePinia
|
|
67
68
|
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
|
|
68
69
|
const shallowReactive: typeof import('vue').shallowReactive
|
|
@@ -78,7 +79,7 @@ declare global {
|
|
|
78
79
|
const useAttrs: typeof import('vue').useAttrs
|
|
79
80
|
const useCssModule: typeof import('vue').useCssModule
|
|
80
81
|
const useCssVars: typeof import('vue').useCssVars
|
|
81
|
-
const useGlobal: typeof import('../plugins/global
|
|
82
|
+
const useGlobal: typeof import('../plugins/global').useGlobal
|
|
82
83
|
const useId: typeof import('vue').useId
|
|
83
84
|
const useModel: typeof import('vue').useModel
|
|
84
85
|
const useRoute: typeof import('vue-router').useRoute
|
|
@@ -96,8 +97,11 @@ declare global {
|
|
|
96
97
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
|
97
98
|
import('vue')
|
|
98
99
|
// @ts-ignore
|
|
99
|
-
export type {
|
|
100
|
-
import('../
|
|
100
|
+
export type { BeflyAdminConfig } from '../plugins/config'
|
|
101
|
+
import('../plugins/config')
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
export type { HttpApiResponse, HttpCleanParamsOptions, HttpClientOptions, HttpGetData, HttpPostData } from '../plugins/http'
|
|
104
|
+
import('../plugins/http')
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
// for vue template auto import
|
|
@@ -105,14 +109,13 @@ import { UnwrapRef } from 'vue'
|
|
|
105
109
|
declare module 'vue' {
|
|
106
110
|
interface GlobalComponents {}
|
|
107
111
|
interface ComponentCustomProperties {
|
|
108
|
-
readonly $Config: UnwrapRef<typeof import('../plugins/config
|
|
109
|
-
readonly $Http: UnwrapRef<typeof import('../plugins/http
|
|
110
|
-
readonly $Router: UnwrapRef<typeof import('../plugins/router
|
|
111
|
-
readonly $Storage: UnwrapRef<typeof import('../plugins/storage
|
|
112
|
+
readonly $Config: UnwrapRef<typeof import('../plugins/config')['$Config']>
|
|
113
|
+
readonly $Http: UnwrapRef<typeof import('../plugins/http')['$Http']>
|
|
114
|
+
readonly $Router: UnwrapRef<typeof import('../plugins/router')['$Router']>
|
|
115
|
+
readonly $Storage: UnwrapRef<typeof import('../plugins/storage')['$Storage']>
|
|
112
116
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
|
113
117
|
readonly MessagePlugin: UnwrapRef<typeof import('tdesign-vue-next')['MessagePlugin']>
|
|
114
118
|
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
|
115
|
-
readonly arrayToTree: UnwrapRef<typeof import('../utils/arrayToTree')['arrayToTree']>
|
|
116
119
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
|
117
120
|
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
|
118
121
|
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
|
@@ -175,7 +178,7 @@ declare module 'vue' {
|
|
|
175
178
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
|
176
179
|
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
|
177
180
|
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
|
178
|
-
readonly useGlobal: UnwrapRef<typeof import('../plugins/global
|
|
181
|
+
readonly useGlobal: UnwrapRef<typeof import('../plugins/global')['useGlobal']>
|
|
179
182
|
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
|
180
183
|
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
|
181
184
|
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/src/main.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// 引入 TDesign 组件
|
|
2
|
-
import { Table as TTable } from "tdesign-vue-next";
|
|
3
|
-
// 引入 TDesign 样式
|
|
4
|
-
import "tdesign-vue-next/es/style/index.css";
|
|
5
|
-
// 引入 addonAdmin 的 CSS 变量
|
|
6
|
-
import "@befly-addon/admin/styles/variables.scss";
|
|
7
|
-
// 引入全局基础样式(reset、通用类、滚动条等)
|
|
8
|
-
import "@/styles/global.scss";
|
|
9
|
-
import App from "./App.vue";
|
|
10
|
-
|
|
11
|
-
const app = createApp(App);
|
|
12
|
-
|
|
13
|
-
// 安装基础插件
|
|
14
|
-
app.use(createPinia());
|
|
15
|
-
|
|
16
|
-
// 使用路由
|
|
17
|
-
app.use($Router);
|
|
18
|
-
|
|
19
|
-
// 全局配置 TTable 默认属性
|
|
20
|
-
app.component("TTable", {
|
|
21
|
-
...TTable,
|
|
22
|
-
props: {
|
|
23
|
-
...TTable.props,
|
|
24
|
-
bordered: {
|
|
25
|
-
type: [Boolean, Object],
|
|
26
|
-
default: () => ({ cell: "horizontal" })
|
|
27
|
-
},
|
|
28
|
-
size: {
|
|
29
|
-
type: String,
|
|
30
|
-
default: "small"
|
|
31
|
-
},
|
|
32
|
-
height: {
|
|
33
|
-
type: [String, Number],
|
|
34
|
-
default: "100%"
|
|
35
|
-
},
|
|
36
|
-
selectOnRowClick: {
|
|
37
|
-
type: Boolean,
|
|
38
|
-
default: true
|
|
39
|
-
},
|
|
40
|
-
activeRowType: {
|
|
41
|
-
type: String,
|
|
42
|
-
default: "single"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
app.mount("#app");
|
package/src/plugins/http.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import axios from "axios";
|
|
2
|
-
import { MessagePlugin } from "tdesign-vue-next";
|
|
3
|
-
|
|
4
|
-
import { $Storage } from "./storage";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @typedef {Object} ApiResponse
|
|
8
|
-
* @property {0 | 1} code
|
|
9
|
-
* @property {string} msg
|
|
10
|
-
* @property {any} data
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// 创建 axios 实例
|
|
14
|
-
const request = axios.create({
|
|
15
|
-
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
16
|
-
timeout: 10000,
|
|
17
|
-
headers: {
|
|
18
|
-
"Content-Type": "application/json"
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// 请求拦截器
|
|
23
|
-
request.interceptors.request.use(
|
|
24
|
-
(config) => {
|
|
25
|
-
// 添加 token
|
|
26
|
-
const token = $Storage.local.get("token");
|
|
27
|
-
if (token) {
|
|
28
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
29
|
-
}
|
|
30
|
-
return config;
|
|
31
|
-
},
|
|
32
|
-
(error) => {
|
|
33
|
-
return Promise.reject(error);
|
|
34
|
-
}
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
// 响应拦截器
|
|
38
|
-
request.interceptors.response.use(
|
|
39
|
-
(response) => {
|
|
40
|
-
const res = response.data;
|
|
41
|
-
|
|
42
|
-
// 如果code不是0,说明业务失败(不显示提示,由业务层处理)
|
|
43
|
-
if (res.code !== 0) {
|
|
44
|
-
return Promise.reject({
|
|
45
|
-
code: res.code,
|
|
46
|
-
msg: res.msg || "请求失败",
|
|
47
|
-
data: res.data
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 成功时返回 data
|
|
52
|
-
return res;
|
|
53
|
-
},
|
|
54
|
-
(error) => {
|
|
55
|
-
MessagePlugin.error("网络连接失败");
|
|
56
|
-
return Promise.reject({
|
|
57
|
-
code: -1,
|
|
58
|
-
msg: "网络连接失败",
|
|
59
|
-
error: error
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 统一的 HTTP 请求方法(仅支持 GET 和 POST)
|
|
66
|
-
* @template T
|
|
67
|
-
* @param {string} url - 请求路径
|
|
68
|
-
* @param {any} [data={}] - 请求数据,默认为空对象
|
|
69
|
-
* @param {'get' | 'post'} [method='post'] - 请求方法,默认为 'post',可选 'get' | 'post'
|
|
70
|
-
* @param {import('axios').AxiosRequestConfig} [config] - axios 请求配置
|
|
71
|
-
* @returns {Promise<any>} 成功返回 data,失败抛出 {code, msg, data} 对象
|
|
72
|
-
*/
|
|
73
|
-
export function $Http(url, data = {}, method = "post", config) {
|
|
74
|
-
const methodLower = method.toLowerCase();
|
|
75
|
-
|
|
76
|
-
// GET 请求将 data 作为 params
|
|
77
|
-
if (methodLower === "get") {
|
|
78
|
-
return request.get(url, {
|
|
79
|
-
...config,
|
|
80
|
-
params: data
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// POST 请求将 data 作为 body
|
|
85
|
-
return request.post(url, data, config);
|
|
86
|
-
}
|
package/src/plugins/storage.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storage 封装
|
|
3
|
-
* 支持命名空间隔离,统一管理 localStorage 和 sessionStorage
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// 获取命名空间
|
|
7
|
-
const NAMESPACE = import.meta.env.VITE_STORAGE_NAMESPACE || "befly";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 存储操作类
|
|
11
|
-
*/
|
|
12
|
-
class StorageManager {
|
|
13
|
-
/**
|
|
14
|
-
* @param {string} namespace
|
|
15
|
-
*/
|
|
16
|
-
constructor(namespace = NAMESPACE) {
|
|
17
|
-
this.localStorage = window.localStorage;
|
|
18
|
-
this.sessionStorage = window.sessionStorage;
|
|
19
|
-
this.namespace = namespace;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 生成带命名空间的key
|
|
24
|
-
* @param {string} key
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
getKey(key) {
|
|
28
|
-
return `${this.namespace}:${key}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 创建存储操作方法
|
|
33
|
-
* @param {Storage} storage
|
|
34
|
-
*/
|
|
35
|
-
createStorageOps(storage) {
|
|
36
|
-
return {
|
|
37
|
-
/**
|
|
38
|
-
* 设置存储
|
|
39
|
-
* @param {string} key - 键名
|
|
40
|
-
* @param {any} value - 值(自动序列化)
|
|
41
|
-
* @returns {void}
|
|
42
|
-
*/
|
|
43
|
-
set: (key, value) => {
|
|
44
|
-
try {
|
|
45
|
-
const fullKey = this.getKey(key);
|
|
46
|
-
const serializedValue = JSON.stringify(value);
|
|
47
|
-
storage.setItem(fullKey, serializedValue);
|
|
48
|
-
} catch {}
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 获取存储
|
|
53
|
-
* @template T
|
|
54
|
-
* @param {string} key - 键名
|
|
55
|
-
* @param {T | null} [defaultValue=null] - 默认值
|
|
56
|
-
* @returns {T | null} 解析后的值
|
|
57
|
-
*/
|
|
58
|
-
get: (key, defaultValue = null) => {
|
|
59
|
-
try {
|
|
60
|
-
const fullKey = this.getKey(key);
|
|
61
|
-
const value = storage.getItem(fullKey);
|
|
62
|
-
if (value === null) {
|
|
63
|
-
return defaultValue;
|
|
64
|
-
}
|
|
65
|
-
return JSON.parse(value);
|
|
66
|
-
} catch {
|
|
67
|
-
return defaultValue;
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 删除存储
|
|
73
|
-
* @param {string} key - 键名
|
|
74
|
-
* @returns {void}
|
|
75
|
-
*/
|
|
76
|
-
remove: (key) => {
|
|
77
|
-
try {
|
|
78
|
-
const fullKey = this.getKey(key);
|
|
79
|
-
storage.removeItem(fullKey);
|
|
80
|
-
} catch {}
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 清空当前命名空间下的所有存储
|
|
85
|
-
* @returns {void}
|
|
86
|
-
*/
|
|
87
|
-
clear: () => {
|
|
88
|
-
try {
|
|
89
|
-
const keys = Object.keys(storage);
|
|
90
|
-
const prefix = `${this.namespace}:`;
|
|
91
|
-
keys.forEach((key) => {
|
|
92
|
-
if (key.startsWith(prefix)) {
|
|
93
|
-
storage.removeItem(key);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
} catch {}
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 判断是否存在某个键
|
|
101
|
-
* @param {string} key - 键名
|
|
102
|
-
* @returns {boolean}
|
|
103
|
-
*/
|
|
104
|
-
has: (key) => {
|
|
105
|
-
const fullKey = this.getKey(key);
|
|
106
|
-
return storage.getItem(fullKey) !== null;
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 获取所有键名
|
|
111
|
-
* @returns {string[]} 去除命名空间前缀的键名列表
|
|
112
|
-
*/
|
|
113
|
-
keys: () => {
|
|
114
|
-
const keys = Object.keys(storage);
|
|
115
|
-
const prefix = `${this.namespace}:`;
|
|
116
|
-
return keys.filter((key) => key.startsWith(prefix)).map((key) => key.substring(prefix.length));
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* localStorage 操作方法
|
|
123
|
-
*/
|
|
124
|
-
get local() {
|
|
125
|
-
return this.createStorageOps(this.localStorage);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* sessionStorage 操作方法
|
|
130
|
-
*/
|
|
131
|
-
get session() {
|
|
132
|
-
return this.createStorageOps(this.sessionStorage);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 导出单例
|
|
137
|
-
export const $Storage = new StorageManager();
|
package/src/utils/arrayToTree.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
export type ArrayToTreeResult<T extends Record<string, any>> = {
|
|
2
|
-
flat: Array<T>;
|
|
3
|
-
tree: Array<T>;
|
|
4
|
-
map: Map<string, T>;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 将一维数组按 { id, pid } 组装为树形结构(纯函数 / 无副作用)。
|
|
9
|
-
*
|
|
10
|
-
* - 默认字段:id / pid / children / sort
|
|
11
|
-
* - pid 为空字符串或父节点不存在时,视为根节点
|
|
12
|
-
* - 内部会 clone 一份节点对象,并写入 children: []
|
|
13
|
-
* - 默认自带递归排序:按 sort 升序;sort 缺省/非法或 < 1 视为 999999;sort 相同按 id 自然序
|
|
14
|
-
*/
|
|
15
|
-
export function arrayToTree<T extends Record<string, any>>(items: T[], id: string = "id", pid: string = "pid", children: string = "children", sort: string = "sort"): ArrayToTreeResult<T> {
|
|
16
|
-
const idKey = typeof id === "string" && id.length > 0 ? id : "id";
|
|
17
|
-
const pidKey = typeof pid === "string" && pid.length > 0 ? pid : "pid";
|
|
18
|
-
const childrenKey = typeof children === "string" && children.length > 0 ? children : "children";
|
|
19
|
-
const sortKey = typeof sort === "string" && sort.length > 0 ? sort : "sort";
|
|
20
|
-
|
|
21
|
-
const map = new Map<string, T>();
|
|
22
|
-
const flat: T[] = [];
|
|
23
|
-
|
|
24
|
-
const safeItems = Array.isArray(items) ? items : [];
|
|
25
|
-
|
|
26
|
-
const normalizeKey = (value: unknown): string => {
|
|
27
|
-
if (typeof value === "string") {
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
31
|
-
return String(value);
|
|
32
|
-
}
|
|
33
|
-
return "";
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
for (const item of safeItems) {
|
|
37
|
-
const rawId = item ? (item as any)[idKey] : undefined;
|
|
38
|
-
const rawPid = item ? (item as any)[pidKey] : undefined;
|
|
39
|
-
|
|
40
|
-
const normalizedId = normalizeKey(rawId);
|
|
41
|
-
const normalizedPid = normalizeKey(rawPid);
|
|
42
|
-
|
|
43
|
-
const nextNode = Object.assign({}, item) as T;
|
|
44
|
-
(nextNode as any)[idKey] = normalizedId;
|
|
45
|
-
(nextNode as any)[pidKey] = normalizedPid;
|
|
46
|
-
(nextNode as any)[childrenKey] = [];
|
|
47
|
-
|
|
48
|
-
flat.push(nextNode);
|
|
49
|
-
|
|
50
|
-
if (normalizedId.length > 0) {
|
|
51
|
-
map.set(normalizedId, nextNode);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const tree: T[] = [];
|
|
56
|
-
|
|
57
|
-
for (const node of flat) {
|
|
58
|
-
const selfId = normalizeKey(node ? (node as any)[idKey] : undefined);
|
|
59
|
-
const parentId = normalizeKey(node ? (node as any)[pidKey] : undefined);
|
|
60
|
-
|
|
61
|
-
if (parentId.length > 0 && parentId !== selfId) {
|
|
62
|
-
const parent = map.get(parentId);
|
|
63
|
-
if (parent && Array.isArray((parent as any)[childrenKey])) {
|
|
64
|
-
(parent as any)[childrenKey].push(node);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
tree.push(node);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
|
73
|
-
|
|
74
|
-
const getSortValue = (node: T): number => {
|
|
75
|
-
const raw = node ? (node as any)[sortKey] : undefined;
|
|
76
|
-
if (typeof raw !== "number") {
|
|
77
|
-
return 999999;
|
|
78
|
-
}
|
|
79
|
-
if (!Number.isFinite(raw)) {
|
|
80
|
-
return 999999;
|
|
81
|
-
}
|
|
82
|
-
if (raw < 1) {
|
|
83
|
-
return 999999;
|
|
84
|
-
}
|
|
85
|
-
return raw;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const compareNode = (a: T, b: T): number => {
|
|
89
|
-
const aSort = getSortValue(a);
|
|
90
|
-
const bSort = getSortValue(b);
|
|
91
|
-
|
|
92
|
-
if (aSort !== bSort) {
|
|
93
|
-
return aSort - bSort;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const aId = a ? (a as any)[idKey] : "";
|
|
97
|
-
const bId = b ? (b as any)[idKey] : "";
|
|
98
|
-
|
|
99
|
-
return collator.compare(typeof aId === "string" ? aId : "", typeof bId === "string" ? bId : "");
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const sortTreeInPlace = (nodes: Array<T>, seen: WeakSet<object>): void => {
|
|
103
|
-
if (!Array.isArray(nodes)) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (nodes.length > 1) {
|
|
108
|
-
nodes.sort(compareNode);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
for (const node of nodes) {
|
|
112
|
-
if (typeof node !== "object" || node === null) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (seen.has(node)) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
seen.add(node);
|
|
120
|
-
|
|
121
|
-
const childNodes = (node as any)[childrenKey];
|
|
122
|
-
if (Array.isArray(childNodes) && childNodes.length > 0) {
|
|
123
|
-
sortTreeInPlace(childNodes, seen);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
sortTreeInPlace(tree, new WeakSet<object>());
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
flat: flat,
|
|
132
|
-
tree: tree,
|
|
133
|
-
map: map
|
|
134
|
-
};
|
|
135
|
-
}
|