@befly-addon/admin 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adminViews/config/dict/index.vue +1 -1
- package/adminViews/config/dictType/index.vue +1 -1
- package/adminViews/config/system/index.vue +1 -1
- package/adminViews/log/email/index.vue +1 -1
- package/adminViews/log/login/index.vue +1 -1
- package/adminViews/log/operate/index.vue +1 -1
- package/adminViews/login_1/index.vue +1 -1
- package/adminViews/people/admin/components/edit.vue +2 -2
- package/adminViews/people/admin/index.vue +1 -1
- package/adminViews/permission/api/index.vue +2 -2
- package/adminViews/permission/menu/index.vue +2 -2
- package/adminViews/permission/role/components/edit.vue +1 -1
- package/adminViews/permission/role/components/menu.vue +1 -1
- package/adminViews/permission/role/index.vue +1 -1
- package/apis/admin/cacheRefresh.ts +5 -5
- package/apis/api/all.ts +1 -1
- package/apis/api/list.ts +1 -1
- package/apis/auth/login.ts +1 -1
- package/apis/dashboard/serviceStatus.ts +2 -2
- package/apis/dashboard/systemResources.ts +1 -1
- package/apis/email/send.ts +1 -1
- package/apis/menu/all.ts +1 -1
- package/apis/menu/list.ts +1 -1
- package/apis/role/del.ts +1 -1
- package/apis/role/save.ts +1 -1
- package/apis/sysConfig/del.ts +1 -1
- package/apis/sysConfig/ins.ts +1 -1
- package/apis/sysConfig/upd.ts +1 -1
- package/package.json +4 -3
- package/utils/arrayToTree.ts +135 -0
- package/utils/fieldClear.ts +85 -0
- package/utils/hashPassword.ts +19 -0
- package/utils/normalizePathnameListInput.ts +55 -0
- package/utils/withDefaultColumns.ts +44 -0
|
@@ -83,7 +83,7 @@ import ILucideChevronDown from "~icons/lucide/chevron-down";
|
|
|
83
83
|
import EditDialog from "./components/edit.vue";
|
|
84
84
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
85
85
|
import { $Http } from "@/plugins/http";
|
|
86
|
-
import { withDefaultColumns } from "
|
|
86
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
87
87
|
|
|
88
88
|
definePage({
|
|
89
89
|
meta: {
|
|
@@ -80,7 +80,7 @@ import ILucideChevronDown from "~icons/lucide/chevron-down";
|
|
|
80
80
|
import EditDialog from "./components/edit.vue";
|
|
81
81
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
82
82
|
import { $Http } from "@/plugins/http";
|
|
83
|
-
import { withDefaultColumns } from "
|
|
83
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
84
84
|
|
|
85
85
|
definePage({
|
|
86
86
|
meta: {
|
|
@@ -91,7 +91,7 @@ import ILucideChevronDown from "~icons/lucide/chevron-down";
|
|
|
91
91
|
import EditDialog from "./components/edit.vue";
|
|
92
92
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
93
93
|
import { $Http } from "@/plugins/http";
|
|
94
|
-
import { withDefaultColumns } from "
|
|
94
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
95
95
|
|
|
96
96
|
definePage({
|
|
97
97
|
meta: {
|
|
@@ -90,7 +90,7 @@ import ILucideSend from "~icons/lucide/send";
|
|
|
90
90
|
import ILucideCheckCircle from "~icons/lucide/check-circle";
|
|
91
91
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
92
92
|
import { $Http } from "@/plugins/http";
|
|
93
|
-
import { withDefaultColumns } from "
|
|
93
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
94
94
|
|
|
95
95
|
definePage({
|
|
96
96
|
meta: {
|
|
@@ -54,7 +54,7 @@ import { Button as TButton, Table as TTable, Tag as TTag, Pagination as TPaginat
|
|
|
54
54
|
import ILucideRotateCw from "~icons/lucide/rotate-cw";
|
|
55
55
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
56
56
|
import { $Http } from "@/plugins/http";
|
|
57
|
-
import { withDefaultColumns } from "
|
|
57
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
58
58
|
|
|
59
59
|
definePage({
|
|
60
60
|
meta: {
|
|
@@ -74,7 +74,7 @@ import { Button as TButton, Table as TTable, Tag as TTag, Pagination as TPaginat
|
|
|
74
74
|
import ILucideRotateCw from "~icons/lucide/rotate-cw";
|
|
75
75
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
76
76
|
import { $Http } from "@/plugins/http";
|
|
77
|
-
import { withDefaultColumns } from "
|
|
77
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
78
78
|
|
|
79
79
|
definePage({
|
|
80
80
|
meta: {
|
|
@@ -70,7 +70,7 @@ import ILucideUser from "~icons/lucide/user";
|
|
|
70
70
|
import ILucideLock from "~icons/lucide/lock";
|
|
71
71
|
import { $Http } from "@/plugins/http";
|
|
72
72
|
import { $Storage } from "@/plugins/storage";
|
|
73
|
-
import { hashPassword } from "
|
|
73
|
+
import { hashPassword } from "../../../utils/hashPassword";
|
|
74
74
|
|
|
75
75
|
definePage({
|
|
76
76
|
meta: {
|
|
@@ -47,8 +47,8 @@ import {
|
|
|
47
47
|
MessagePlugin
|
|
48
48
|
} from "tdesign-vue-next";
|
|
49
49
|
import { $Http } from "@/plugins/http";
|
|
50
|
-
import { fieldClear } from "
|
|
51
|
-
import { hashPassword } from "
|
|
50
|
+
import { fieldClear } from "../../../../utils/fieldClear";
|
|
51
|
+
import { hashPassword } from "../../../../utils/hashPassword";
|
|
52
52
|
|
|
53
53
|
const $Prop = defineProps({
|
|
54
54
|
modelValue: {
|
|
@@ -79,7 +79,7 @@ import ILucideChevronDown from "~icons/lucide/chevron-down";
|
|
|
79
79
|
import EditDialog from "./components/edit.vue";
|
|
80
80
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
81
81
|
import { $Http } from "@/plugins/http";
|
|
82
|
-
import { withDefaultColumns } from "
|
|
82
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
83
83
|
|
|
84
84
|
definePage({
|
|
85
85
|
meta: {
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
import { Button as TButton, Table as TTable, Tag as TTag, Input as TInput, MessagePlugin } from "tdesign-vue-next";
|
|
55
55
|
import ILucideRotateCw from "~icons/lucide/rotate-cw";
|
|
56
56
|
import ILucideSearch from "~icons/lucide/search";
|
|
57
|
-
import { $Http } from "@/plugins/http";
|
|
58
|
-
import { withDefaultColumns } from "@/utils/withDefaultColumns";
|
|
59
57
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
58
|
+
import { $Http } from "@/plugins/http";
|
|
59
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
60
60
|
|
|
61
61
|
definePage({
|
|
62
62
|
meta: {
|
|
@@ -44,8 +44,8 @@ import { Button as TButton, Table as TTable, Tag as TTag, MessagePlugin } from "
|
|
|
44
44
|
import ILucideRotateCw from "~icons/lucide/rotate-cw";
|
|
45
45
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
46
46
|
import { $Http } from "@/plugins/http";
|
|
47
|
-
import { arrayToTree } from "
|
|
48
|
-
import { withDefaultColumns } from "
|
|
47
|
+
import { arrayToTree } from "../../../utils/arrayToTree";
|
|
48
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
49
49
|
|
|
50
50
|
definePage({
|
|
51
51
|
meta: {
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
MessagePlugin
|
|
44
44
|
} from "tdesign-vue-next";
|
|
45
45
|
import { $Http } from "@/plugins/http";
|
|
46
|
-
import { fieldClear } from "
|
|
46
|
+
import { fieldClear } from "../../../../utils/fieldClear";
|
|
47
47
|
|
|
48
48
|
const $Prop = defineProps({
|
|
49
49
|
modelValue: {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
import { Dialog as TDialog, CheckboxGroup as TCheckboxGroup, Checkbox as TCheckbox, Button as TButton, Input as TInput, MessagePlugin } from "tdesign-vue-next";
|
|
45
45
|
import ILucideSearch from "~icons/lucide/search";
|
|
46
46
|
import { $Http } from "@/plugins/http";
|
|
47
|
-
import { arrayToTree } from "
|
|
47
|
+
import { arrayToTree } from "../../../../utils/arrayToTree";
|
|
48
48
|
|
|
49
49
|
const $Prop = defineProps({
|
|
50
50
|
modelValue: {
|
|
@@ -102,7 +102,7 @@ import MenuDialog from "./components/menu.vue";
|
|
|
102
102
|
import ApiDialog from "./components/api.vue";
|
|
103
103
|
import DetailPanel from "@/components/DetailPanel.vue";
|
|
104
104
|
import { $Http } from "@/plugins/http";
|
|
105
|
-
import { withDefaultColumns } from "
|
|
105
|
+
import { withDefaultColumns } from "../../../utils/withDefaultColumns";
|
|
106
106
|
|
|
107
107
|
definePage({
|
|
108
108
|
meta: {
|
|
@@ -32,7 +32,7 @@ export default {
|
|
|
32
32
|
await befly.redis.setObject("apis:all", apis.data.lists);
|
|
33
33
|
results.apis = { success: true, count: apis.data.lists.length };
|
|
34
34
|
} catch (error: any) {
|
|
35
|
-
befly.logger.error({ err: error
|
|
35
|
+
befly.logger.error({ err: error, msg: "刷新接口缓存失败" });
|
|
36
36
|
results.apis = { success: false, error: error.message };
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -54,7 +54,7 @@ export default {
|
|
|
54
54
|
childCount: childCount
|
|
55
55
|
};
|
|
56
56
|
} catch (error: any) {
|
|
57
|
-
befly.logger.error({ err: error
|
|
57
|
+
befly.logger.error({ err: error, msg: "刷新菜单缓存失败" });
|
|
58
58
|
results.menus = { success: false, error: error.message };
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -74,7 +74,7 @@ export default {
|
|
|
74
74
|
|
|
75
75
|
results.roles = { success: true, count: count };
|
|
76
76
|
} catch (error: any) {
|
|
77
|
-
befly.logger.error({ err: error
|
|
77
|
+
befly.logger.error({ err: error, msg: "刷新角色缓存失败" });
|
|
78
78
|
results.roles = { success: false, error: error.message };
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -83,7 +83,7 @@ export default {
|
|
|
83
83
|
await befly.cache.rebuildRoleApiPermissions();
|
|
84
84
|
results.roleApiPermissions = { success: true };
|
|
85
85
|
} catch (error: any) {
|
|
86
|
-
befly.logger.error({ err: error
|
|
86
|
+
befly.logger.error({ err: error, msg: "重建角色接口权限缓存失败" });
|
|
87
87
|
results.roleApiPermissions = { success: false, error: error.message };
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -96,7 +96,7 @@ export default {
|
|
|
96
96
|
return befly.tool.No("部分缓存刷新失败", results);
|
|
97
97
|
}
|
|
98
98
|
} catch (error: any) {
|
|
99
|
-
befly.logger.error({ err: error
|
|
99
|
+
befly.logger.error({ err: error, msg: "刷新全部缓存失败" });
|
|
100
100
|
return befly.tool.No("刷新全部缓存失败", { error: error.message });
|
|
101
101
|
}
|
|
102
102
|
}
|
package/apis/api/all.ts
CHANGED
package/apis/api/list.ts
CHANGED
package/apis/auth/login.ts
CHANGED
|
@@ -82,7 +82,7 @@ export default {
|
|
|
82
82
|
return befly.tool.No("账号或密码错误");
|
|
83
83
|
}
|
|
84
84
|
} catch (error: any) {
|
|
85
|
-
befly.logger.error({ err: error
|
|
85
|
+
befly.logger.error({ err: error, msg: "密码验证失败" });
|
|
86
86
|
logData.failReason = "密码格式错误";
|
|
87
87
|
await befly.db.insData({ table: "addon_admin_login_log", data: logData });
|
|
88
88
|
return befly.tool.No("密码格式错误,请重新设置密码");
|
|
@@ -14,7 +14,7 @@ export default {
|
|
|
14
14
|
responseTime: `${responseTime}ms`
|
|
15
15
|
});
|
|
16
16
|
} catch (error) {
|
|
17
|
-
befly.logger.error({ err: error
|
|
17
|
+
befly.logger.error({ err: error, msg: "数据库状态检测失败" });
|
|
18
18
|
services.push({
|
|
19
19
|
name: "数据库",
|
|
20
20
|
status: "stopped",
|
|
@@ -34,7 +34,7 @@ export default {
|
|
|
34
34
|
responseTime: `${responseTime}ms`
|
|
35
35
|
});
|
|
36
36
|
} catch (error) {
|
|
37
|
-
befly.logger.error({ err: error
|
|
37
|
+
befly.logger.error({ err: error, msg: "Redis状态检测失败" });
|
|
38
38
|
services.push({
|
|
39
39
|
name: "Redis",
|
|
40
40
|
status: "stopped",
|
package/apis/email/send.ts
CHANGED
package/apis/menu/all.ts
CHANGED
|
@@ -51,7 +51,7 @@ export default {
|
|
|
51
51
|
// 6. 返回一维数组(由前端构建树形结构)
|
|
52
52
|
return befly.tool.Yes("获取菜单成功", { lists: authorizedMenus });
|
|
53
53
|
} catch (error: any) {
|
|
54
|
-
befly.logger.error({ err: error
|
|
54
|
+
befly.logger.error({ err: error, msg: "获取用户菜单失败" });
|
|
55
55
|
return befly.tool.No("获取菜单失败");
|
|
56
56
|
}
|
|
57
57
|
}
|
package/apis/menu/list.ts
CHANGED
package/apis/role/del.ts
CHANGED
package/apis/role/save.ts
CHANGED
package/apis/sysConfig/del.ts
CHANGED
package/apis/sysConfig/ins.ts
CHANGED
package/apis/sysConfig/upd.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@befly-addon/admin",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"gitHead": "340c00e4dd05b62d89d25c01922c27cb35bd99af",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 管理后台功能组件",
|
|
7
7
|
"keywords": [
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"libs/",
|
|
30
30
|
"plugins/",
|
|
31
31
|
"styles/",
|
|
32
|
-
"tables/"
|
|
32
|
+
"tables/",
|
|
33
|
+
"utils/"
|
|
33
34
|
],
|
|
34
35
|
"type": "module",
|
|
35
36
|
"main": "package.json",
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export interface FieldClearOptions {
|
|
2
|
+
pickKeys?: string[];
|
|
3
|
+
omitKeys?: string[];
|
|
4
|
+
keepValues?: any[];
|
|
5
|
+
excludeValues?: any[];
|
|
6
|
+
keepMap?: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type FieldClearResult<T> = T extends Array<infer U> ? Array<FieldClearResult<U>> : T extends object ? { [K in keyof T]?: T[K] } : T;
|
|
10
|
+
|
|
11
|
+
function isObject(val: unknown): val is Record<string, any> {
|
|
12
|
+
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isArray(val: unknown): val is any[] {
|
|
16
|
+
return Array.isArray(val);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function fieldClear<T = any>(data: T | T[], options: FieldClearOptions = {}): FieldClearResult<T> {
|
|
20
|
+
const pickKeys = options.pickKeys;
|
|
21
|
+
const omitKeys = options.omitKeys;
|
|
22
|
+
const keepValues = options.keepValues;
|
|
23
|
+
const excludeValues = options.excludeValues;
|
|
24
|
+
const keepMap = options.keepMap;
|
|
25
|
+
|
|
26
|
+
const filterObj = (obj: Record<string, any>) => {
|
|
27
|
+
const result: Record<string, any> = {};
|
|
28
|
+
|
|
29
|
+
let keys = Object.keys(obj);
|
|
30
|
+
if (pickKeys && pickKeys.length) {
|
|
31
|
+
keys = keys.filter((k) => pickKeys.includes(k));
|
|
32
|
+
}
|
|
33
|
+
if (omitKeys && omitKeys.length) {
|
|
34
|
+
keys = keys.filter((k) => !omitKeys.includes(k));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = obj[key];
|
|
39
|
+
|
|
40
|
+
// 1. keepMap 优先
|
|
41
|
+
if (keepMap && Object.prototype.hasOwnProperty.call(keepMap, key)) {
|
|
42
|
+
if (Object.is(keepMap[key], value)) {
|
|
43
|
+
result[key] = value;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. keepValues
|
|
49
|
+
if (keepValues && keepValues.length && !keepValues.includes(value)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. excludeValues
|
|
54
|
+
if (excludeValues && excludeValues.length && excludeValues.includes(value)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
result[key] = value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (isArray(data)) {
|
|
65
|
+
return (data as any[])
|
|
66
|
+
.map((item) => {
|
|
67
|
+
if (isObject(item)) {
|
|
68
|
+
return filterObj(item);
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
71
|
+
})
|
|
72
|
+
.filter((item) => {
|
|
73
|
+
if (isObject(item)) {
|
|
74
|
+
return Object.keys(item).length > 0;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}) as FieldClearResult<T>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isObject(data)) {
|
|
81
|
+
return filterObj(data as Record<string, any>) as FieldClearResult<T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return data as FieldClearResult<T>;
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 使用 SHA-256 对密码进行哈希。
|
|
3
|
+
* @param password - 原始密码
|
|
4
|
+
* @param salt - 盐值,默认为 befly
|
|
5
|
+
* @returns 哈希后的密码(十六进制字符串)
|
|
6
|
+
*/
|
|
7
|
+
export async function hashPassword(password: string, salt: string = "befly"): Promise<string> {
|
|
8
|
+
const data = password + salt;
|
|
9
|
+
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
const dataBuffer = encoder.encode(data);
|
|
12
|
+
|
|
13
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
|
|
14
|
+
|
|
15
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
16
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
17
|
+
|
|
18
|
+
return hashHex;
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const HTTP_METHOD_PREFIX_RE = /^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/i;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 将未知输入规范化为 pathname 字符串数组。
|
|
5
|
+
*
|
|
6
|
+
* 规则(契约):
|
|
7
|
+
* - value 为“假值”(null/undefined/""/0/false/NaN)时返回空数组 []。
|
|
8
|
+
* - value 必须是 string[],否则抛错:`${fieldLabel} 必须是字符串数组`。
|
|
9
|
+
* - 数组元素必须满足:
|
|
10
|
+
* - 类型为 string
|
|
11
|
+
* - 不允许为空字符串
|
|
12
|
+
* - 不允许包含任何空白字符(空格/制表符/换行等)
|
|
13
|
+
* - 必须以 "/" 开头(pathname)
|
|
14
|
+
* - forbidMethodPrefix=true 时,禁止 "GET/POST/..." 等 method 前缀,并给出更明确的错误提示。
|
|
15
|
+
*
|
|
16
|
+
* 注意:该函数不会做任何隐式修复/转换(例如 trim/split/JSON.parse/去重/排序)。
|
|
17
|
+
*/
|
|
18
|
+
export function normalizePathnameListInput(value: unknown, fieldLabel: string, forbidMethodPrefix: boolean): string[] {
|
|
19
|
+
if (!value) return [];
|
|
20
|
+
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
throw new Error(`${fieldLabel} 必须是字符串数组`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const out: string[] = [];
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
28
|
+
const item = value[i];
|
|
29
|
+
const itemLabel = `${fieldLabel}[${i}]`;
|
|
30
|
+
|
|
31
|
+
if (typeof item !== "string") {
|
|
32
|
+
throw new Error(`${itemLabel} 必须是字符串`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (item.length === 0) {
|
|
36
|
+
throw new Error(`${itemLabel} 不允许为空字符串`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (forbidMethodPrefix && HTTP_METHOD_PREFIX_RE.test(item)) {
|
|
40
|
+
throw new Error(`${itemLabel} 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (/\s/.test(item)) {
|
|
44
|
+
throw new Error(`${itemLabel} 不允许包含空白字符(空格/制表符/换行等)`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!item.startsWith("/")) {
|
|
48
|
+
throw new Error(`${itemLabel} 必须是 pathname(以 / 开头)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
out.push(item);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 为表格列添加默认配置(纯函数)。
|
|
3
|
+
*
|
|
4
|
+
* 默认行为:
|
|
5
|
+
* - base 默认:{ width: 200, ellipsis: true }
|
|
6
|
+
* - 特殊列:operation/state/id
|
|
7
|
+
* - colKey 以 At/At2 结尾时:默认 { align: "center" }
|
|
8
|
+
* - customConfig 可覆盖/扩展默认 specialColumnConfig
|
|
9
|
+
*/
|
|
10
|
+
export function withDefaultColumns(columns: any[], customConfig?: Record<string, any>): any[] {
|
|
11
|
+
const safeColumns = Array.isArray(columns) ? columns : [];
|
|
12
|
+
|
|
13
|
+
const specialColumnConfig: Record<string, any> = Object.assign(
|
|
14
|
+
{
|
|
15
|
+
operation: { width: 100, align: "center", fixed: "right" },
|
|
16
|
+
state: { width: 100, align: "center" },
|
|
17
|
+
id: { width: 200, align: "center" }
|
|
18
|
+
},
|
|
19
|
+
customConfig || {}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return safeColumns.map((col) => {
|
|
23
|
+
const colKey = col && (col as any).colKey;
|
|
24
|
+
|
|
25
|
+
let specialConfig = colKey ? specialColumnConfig[colKey] : undefined;
|
|
26
|
+
|
|
27
|
+
if (!specialConfig && typeof colKey === "string" && (colKey.endsWith("At") || colKey.endsWith("At2"))) {
|
|
28
|
+
specialConfig = { align: "center" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const base = {
|
|
32
|
+
width: 200,
|
|
33
|
+
ellipsis: true
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const merged = Object.assign({}, base);
|
|
37
|
+
if (specialConfig) {
|
|
38
|
+
Object.assign(merged, specialConfig);
|
|
39
|
+
}
|
|
40
|
+
Object.assign(merged, col);
|
|
41
|
+
|
|
42
|
+
return merged;
|
|
43
|
+
});
|
|
44
|
+
}
|