befly-admin-ui 1.8.14

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.
Files changed (46) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -0
  3. package/jsconfig.json +14 -0
  4. package/package.json +51 -0
  5. package/styles/variables.scss +148 -0
  6. package/utils/arrayToTree.js +115 -0
  7. package/utils/cleanParams.js +29 -0
  8. package/utils/fieldClear.js +62 -0
  9. package/utils/genShortId.js +12 -0
  10. package/utils/hashPassword.js +9 -0
  11. package/utils/scanViewsDir.js +120 -0
  12. package/utils/withDefaultColumns.js +46 -0
  13. package/views/config/dict/components/edit.vue +120 -0
  14. package/views/config/dict/index.vue +188 -0
  15. package/views/config/dictType/components/edit.vue +110 -0
  16. package/views/config/dictType/index.vue +153 -0
  17. package/views/config/index.vue +3 -0
  18. package/views/config/system/components/edit.vue +184 -0
  19. package/views/config/system/index.vue +188 -0
  20. package/views/index/components/addonList.vue +148 -0
  21. package/views/index/components/environmentInfo.vue +116 -0
  22. package/views/index/components/operationLogs.vue +127 -0
  23. package/views/index/components/performanceMetrics.vue +153 -0
  24. package/views/index/components/quickActions.vue +30 -0
  25. package/views/index/components/serviceStatus.vue +197 -0
  26. package/views/index/components/systemNotifications.vue +144 -0
  27. package/views/index/components/systemOverview.vue +194 -0
  28. package/views/index/components/systemResources.vue +121 -0
  29. package/views/index/components/userInfo.vue +210 -0
  30. package/views/index/index.vue +67 -0
  31. package/views/jsconfig.json +15 -0
  32. package/views/log/email/index.vue +221 -0
  33. package/views/log/index.vue +3 -0
  34. package/views/log/login/index.vue +95 -0
  35. package/views/log/operate/index.vue +169 -0
  36. package/views/login_1/index.vue +400 -0
  37. package/views/people/admin/components/edit.vue +173 -0
  38. package/views/people/admin/index.vue +121 -0
  39. package/views/people/index.vue +3 -0
  40. package/views/permission/api/index.vue +146 -0
  41. package/views/permission/index.vue +3 -0
  42. package/views/permission/menu/index.vue +109 -0
  43. package/views/permission/role/components/api.vue +371 -0
  44. package/views/permission/role/components/edit.vue +143 -0
  45. package/views/permission/role/components/menu.vue +310 -0
  46. package/views/permission/role/index.vue +175 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * 将一维数组按 { id, pid } 组装为树形结构(纯函数 / 无副作用)。
3
+ *
4
+ * - 默认字段:id / pid / children / sort
5
+ * - pid 为空字符串或父节点不存在时,视为根节点
6
+ * - 内部会 clone 一份节点对象,并写入 children: []
7
+ * - 默认自带递归排序:按 sort 升序;sort 缺省/非法或 < 1 视为 999999;sort 相同按 id 自然序
8
+ */
9
+ export function arrayToTree(items, id = "id", pid = "pid", children = "children", sort = "sort") {
10
+ const idKey = typeof id === "string" && id.length > 0 ? id : "id";
11
+ const pidKey = typeof pid === "string" && pid.length > 0 ? pid : "pid";
12
+ const childrenKey = typeof children === "string" && children.length > 0 ? children : "children";
13
+ const sortKey = typeof sort === "string" && sort.length > 0 ? sort : "sort";
14
+ const map = new Map();
15
+ const flat = [];
16
+ const safeItems = Array.isArray(items) ? items : [];
17
+ const normalizeKey = (value) => {
18
+ if (typeof value === "string") {
19
+ return value;
20
+ }
21
+ if (typeof value === "number" && Number.isFinite(value)) {
22
+ return String(value);
23
+ }
24
+ return "";
25
+ };
26
+ for (const item of safeItems) {
27
+ const itemObj = typeof item === "object" && item !== null ? item : null;
28
+ const rawId = itemObj ? itemObj[idKey] : undefined;
29
+ const rawPid = itemObj ? itemObj[pidKey] : undefined;
30
+ const normalizedId = normalizeKey(rawId);
31
+ const normalizedPid = normalizeKey(rawPid);
32
+ const nextNode = Object.assign({}, item);
33
+ const nextNodeObj = nextNode;
34
+ nextNodeObj[idKey] = normalizedId;
35
+ nextNodeObj[pidKey] = normalizedPid;
36
+ nextNodeObj[childrenKey] = [];
37
+ flat.push(nextNode);
38
+ if (normalizedId.length > 0) {
39
+ map.set(normalizedId, nextNode);
40
+ }
41
+ }
42
+ const tree = [];
43
+ for (const node of flat) {
44
+ const nodeObj = node;
45
+ const selfId = normalizeKey(nodeObj[idKey]);
46
+ const parentId = normalizeKey(nodeObj[pidKey]);
47
+ if (parentId.length > 0 && parentId !== selfId) {
48
+ const parent = map.get(parentId);
49
+ if (parent) {
50
+ const parentObj = parent;
51
+ const childrenValue = parentObj[childrenKey];
52
+ if (Array.isArray(childrenValue)) {
53
+ childrenValue.push(node);
54
+ continue;
55
+ }
56
+ }
57
+ }
58
+ tree.push(node);
59
+ }
60
+ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
61
+ const getSortValue = (node) => {
62
+ const nodeObj = node;
63
+ const raw = nodeObj[sortKey];
64
+ if (typeof raw !== "number") {
65
+ return 999999;
66
+ }
67
+ if (!Number.isFinite(raw)) {
68
+ return 999999;
69
+ }
70
+ if (raw < 1) {
71
+ return 999999;
72
+ }
73
+ return raw;
74
+ };
75
+ const compareNode = (a, b) => {
76
+ const aSort = getSortValue(a);
77
+ const bSort = getSortValue(b);
78
+ if (aSort !== bSort) {
79
+ return aSort - bSort;
80
+ }
81
+ const aObj = a;
82
+ const bObj = b;
83
+ const aId = aObj[idKey];
84
+ const bId = bObj[idKey];
85
+ return collator.compare(typeof aId === "string" ? aId : "", typeof bId === "string" ? bId : "");
86
+ };
87
+ const sortTreeInPlace = (nodes, seen) => {
88
+ if (!Array.isArray(nodes)) {
89
+ return;
90
+ }
91
+ if (nodes.length > 1) {
92
+ nodes.sort(compareNode);
93
+ }
94
+ for (const node of nodes) {
95
+ if (typeof node !== "object" || node === null) {
96
+ continue;
97
+ }
98
+ if (seen.has(node)) {
99
+ continue;
100
+ }
101
+ seen.add(node);
102
+ const nodeObj = node;
103
+ const childNodes = nodeObj[childrenKey];
104
+ if (Array.isArray(childNodes) && childNodes.length > 0) {
105
+ sortTreeInPlace(childNodes, seen);
106
+ }
107
+ }
108
+ };
109
+ sortTreeInPlace(tree, new WeakSet());
110
+ return {
111
+ flat: flat,
112
+ tree: tree,
113
+ map: map
114
+ };
115
+ }
@@ -0,0 +1,29 @@
1
+ export function cleanParams(data, dropValues, dropKeyValue) {
2
+ const globalDropValues = dropValues ?? [];
3
+ const perKeyDropValues = dropKeyValue ?? {};
4
+ const globalDropSet = new Set(globalDropValues);
5
+ const out = {};
6
+ for (const key of Object.keys(data)) {
7
+ const value = data[key];
8
+ // 默认强制移除 null / undefined
9
+ if (value === null || value === undefined) {
10
+ continue;
11
+ }
12
+ // 如果该 key 配了规则:以 key 规则为准,不再应用全局 dropValues
13
+ if (Object.hasOwn(perKeyDropValues, key)) {
14
+ const keyDropValues = perKeyDropValues[key] ?? [];
15
+ const keyDropSet = new Set(keyDropValues);
16
+ if (keyDropSet.has(value)) {
17
+ continue;
18
+ }
19
+ out[key] = value;
20
+ continue;
21
+ }
22
+ // 未配置 key 规则:应用全局 dropValues
23
+ if (globalDropSet.has(value)) {
24
+ continue;
25
+ }
26
+ out[key] = value;
27
+ }
28
+ return out;
29
+ }
@@ -0,0 +1,62 @@
1
+ function isObject(val) {
2
+ return val !== null && typeof val === "object" && !Array.isArray(val);
3
+ }
4
+ function isArray(val) {
5
+ return Array.isArray(val);
6
+ }
7
+ export function fieldClear(data, options = {}) {
8
+ const pickKeys = options.pickKeys;
9
+ const omitKeys = options.omitKeys;
10
+ const keepValues = options.keepValues;
11
+ const excludeValues = options.excludeValues;
12
+ const keepMap = options.keepMap;
13
+ const filterObj = (obj) => {
14
+ const result = {};
15
+ let keys = Object.keys(obj);
16
+ if (pickKeys && pickKeys.length) {
17
+ keys = keys.filter((k) => pickKeys.includes(k));
18
+ }
19
+ if (omitKeys && omitKeys.length) {
20
+ keys = keys.filter((k) => !omitKeys.includes(k));
21
+ }
22
+ for (const key of keys) {
23
+ const value = obj[key];
24
+ // 1. keepMap 优先
25
+ if (keepMap && Object.hasOwn(keepMap, key)) {
26
+ if (Object.is(keepMap[key], value)) {
27
+ result[key] = value;
28
+ continue;
29
+ }
30
+ }
31
+ // 2. keepValues
32
+ if (keepValues && keepValues.length && !keepValues.includes(value)) {
33
+ continue;
34
+ }
35
+ // 3. excludeValues
36
+ if (excludeValues && excludeValues.length && excludeValues.includes(value)) {
37
+ continue;
38
+ }
39
+ result[key] = value;
40
+ }
41
+ return result;
42
+ };
43
+ if (isArray(data)) {
44
+ return data
45
+ .map((item) => {
46
+ if (isObject(item)) {
47
+ return filterObj(item);
48
+ }
49
+ return item;
50
+ })
51
+ .filter((item) => {
52
+ if (isObject(item)) {
53
+ return Object.keys(item).length > 0;
54
+ }
55
+ return true;
56
+ });
57
+ }
58
+ if (isObject(data)) {
59
+ return filterObj(data);
60
+ }
61
+ return data;
62
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 生成短 ID
3
+ * 由时间戳(base36)+ 随机字符组成,约 13 位
4
+ * - 前 8 位:时间戳(可排序)
5
+ * - 后 5 位:随机字符(防冲突)
6
+ * @returns 短 ID 字符串
7
+ * @example
8
+ * genShortId() // "lxyz1a2b3c4"
9
+ */
10
+ export function genShortId() {
11
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
12
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 前端密码 sha256(浏览器侧)
3
+ */
4
+ export async function hashPassword(password) {
5
+ const dataBuffer = new TextEncoder().encode(password);
6
+ const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
7
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
8
+ return hashArray.map((item) => item.toString(16).padStart(2, "0")).join("");
9
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 清理目录名中的数字后缀
3
+ * 如:login_1 → login, index_2 → index
4
+ */
5
+ export function cleanDirName(name) {
6
+ return name.replace(/_\d+$/, "");
7
+ }
8
+ /**
9
+ * 约束:统一 path 形态,避免隐藏菜单匹配、DB 同步出现重复
10
+ * - 必须以 / 开头
11
+ * - 折叠多个 /
12
+ * - 去掉尾随 /(根 / 除外)
13
+ */
14
+ export function normalizeMenuPath(path) {
15
+ let result = path;
16
+ if (!result) {
17
+ return "/";
18
+ }
19
+ if (!result.startsWith("/")) {
20
+ result = `/${result}`;
21
+ }
22
+ result = result.replace(/\/+?/g, "/");
23
+ if (result.length > 1) {
24
+ result = result.replace(/\/+$/, "");
25
+ }
26
+ return result;
27
+ }
28
+ /**
29
+ * 递归规范化并按 path 去重(同 path 的 children 合并)
30
+ *
31
+ * 说明:该函数是纯函数,不依赖任何运行时环境;会返回新数组,但会在内部对克隆对象做合并赋值。
32
+ */
33
+ export function normalizeMenuTree(menus) {
34
+ const map = new Map();
35
+ for (const menu of menus) {
36
+ const rawPath = menu.path;
37
+ const menuPath = rawPath ? normalizeMenuPath(rawPath) : "";
38
+ if (!menuPath) {
39
+ continue;
40
+ }
41
+ // 不使用 structuredClone:
42
+ // - 结构中可能出现函数/类实例等不可 clone 的值
43
+ // - 这里我们只需要“保留额外字段 + 递归 children 生成新数组”
44
+ // 用浅拷贝即可满足需求
45
+ const cloned = Object.assign({}, menu);
46
+ cloned.path = menuPath;
47
+ const rawChildren = menu.children;
48
+ if (rawChildren && rawChildren.length > 0) {
49
+ cloned.children = normalizeMenuTree(rawChildren);
50
+ }
51
+ const existing = map.get(menuPath);
52
+ if (existing) {
53
+ const clonedChildren = cloned.children;
54
+ if (clonedChildren && clonedChildren.length > 0) {
55
+ let existingChildren = existing.children;
56
+ if (!existingChildren) {
57
+ existingChildren = [];
58
+ existing.children = existingChildren;
59
+ }
60
+ for (const child of clonedChildren) {
61
+ existingChildren.push(child);
62
+ }
63
+ existing.children = normalizeMenuTree(existingChildren);
64
+ }
65
+ if (typeof cloned.sort === "number") {
66
+ existing.sort = cloned.sort;
67
+ }
68
+ if (typeof cloned.name === "string" && cloned.name) {
69
+ existing.name = cloned.name;
70
+ }
71
+ } else {
72
+ map.set(menuPath, cloned);
73
+ }
74
+ }
75
+ const result = Array.from(map.values());
76
+ result.sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999));
77
+ return result;
78
+ }
79
+ /**
80
+ * 只取第一个 <script ... setup ...> 块
81
+ */
82
+ export function extractScriptSetupBlock(vueContent) {
83
+ const openTag = /<script\b[^>]*\bsetup\b[^>]*>/i.exec(vueContent);
84
+ if (!openTag) {
85
+ return null;
86
+ }
87
+ const start = openTag.index + openTag[0].length;
88
+ const closeIndex = vueContent.indexOf("</script>", start);
89
+ if (closeIndex < 0) {
90
+ return null;
91
+ }
92
+ return vueContent.slice(start, closeIndex);
93
+ }
94
+ /**
95
+ * 从 <script setup> 中提取 definePage({ meta })
96
+ *
97
+ * 简化约束:
98
+ * - 每个页面只有一个 definePage
99
+ * - title 是纯字符串字面量
100
+ * - order 是数字字面量(可选)
101
+ * - 不考虑变量/表达式/多段 meta 组合
102
+ */
103
+ export function extractDefinePageMetaFromScriptSetup(scriptSetup) {
104
+ const titleMatch = scriptSetup.match(/definePage\s*\([\s\S]*?meta\s*:\s*\{[\s\S]*?title\s*:\s*(["'`])([^"'`]+)\1/);
105
+ if (!titleMatch) {
106
+ return null;
107
+ }
108
+ const title = titleMatch[2];
109
+ if (typeof title !== "string") {
110
+ return null;
111
+ }
112
+ const orderMatch = scriptSetup.match(/definePage\s*\([\s\S]*?meta\s*:\s*\{[\s\S]*?order\s*:\s*(\d+)/);
113
+ if (!orderMatch) {
114
+ return { title: title };
115
+ }
116
+ return {
117
+ title: title,
118
+ order: Number(orderMatch[1])
119
+ };
120
+ }
@@ -0,0 +1,46 @@
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, customConfig) {
11
+ const safeColumns = Array.isArray(columns) ? columns : [];
12
+ const specialColumnConfig = Object.assign(
13
+ {
14
+ operation: { width: 100, align: "center", fixed: "right" },
15
+ sort: { width: 80, align: "center" },
16
+ state: { width: 100, align: "center" },
17
+ result: { width: 80, align: "center" },
18
+ duration: { width: 100, align: "center" },
19
+ createTime: { width: 170, align: "center" },
20
+ updateTime: { width: 170, align: "center" },
21
+ operateTime: { width: 170, align: "center" },
22
+ createdAt: { width: 170, align: "center" },
23
+ updatedAt: { width: 170, align: "center" },
24
+ id: { width: 200, align: "center" }
25
+ },
26
+ customConfig || {}
27
+ );
28
+ return safeColumns.map((col) => {
29
+ const colObj = typeof col === "object" && col !== null ? col : {};
30
+ const colKey = typeof colObj["colKey"] === "string" ? colObj["colKey"] : undefined;
31
+ let specialConfig = colKey ? specialColumnConfig[colKey] : undefined;
32
+ if (!specialConfig && typeof colKey === "string" && (colKey.endsWith("At") || colKey.endsWith("At2"))) {
33
+ specialConfig = { align: "center" };
34
+ }
35
+ const base = {
36
+ width: 200,
37
+ ellipsis: true
38
+ };
39
+ const merged = Object.assign({}, base);
40
+ if (specialConfig) {
41
+ Object.assign(merged, specialConfig);
42
+ }
43
+ Object.assign(merged, colObj);
44
+ return merged;
45
+ });
46
+ }
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <PageDialog v-model="visible" :title="actionType === 'add' ? '添加字典项' : '编辑字典项'" @confirm="handleSubmit">
3
+ <TForm :data="$Data.formData" :rules="$Data.rules" label-width="100px" ref="formRef">
4
+ <TFormItem label="字典类型" name="typeCode">
5
+ <TSelect v-model="$Data.formData.typeCode" placeholder="请选择字典类型" filterable>
6
+ <TOption v-for="item in typeList" :key="item.code" :value="item.code" :label="item.name" />
7
+ </TSelect>
8
+ </TFormItem>
9
+ <TFormItem label="键值" name="key">
10
+ <TInput v-model="$Data.formData.key" placeholder="请输入键名(英文/数字/下划线)" />
11
+ </TFormItem>
12
+ <TFormItem label="标签" name="label">
13
+ <TInput v-model="$Data.formData.label" placeholder="请输入标签(显示名称)" />
14
+ </TFormItem>
15
+ <TFormItem label="排序" name="sort">
16
+ <TInputNumber v-model="$Data.formData.sort" :min="0" placeholder="请输入排序值" />
17
+ </TFormItem>
18
+ <TFormItem label="备注" name="remark">
19
+ <TTextarea v-model="$Data.formData.remark" placeholder="请输入备注信息" :autosize="{ minRows: 3, maxRows: 6 }" />
20
+ </TFormItem>
21
+ </TForm>
22
+ </PageDialog>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { computed, reactive, ref } from "vue";
27
+
28
+ import { Form as TForm, FormItem as TFormItem, Input as TInput, Select as TSelect, Option as TOption, Textarea as TTextarea, InputNumber as TInputNumber, MessagePlugin } from "tdesign-vue-next";
29
+ import PageDialog from "@/components/pageDialog.vue";
30
+ import { $Http } from "@/plugins/http";
31
+
32
+ const props = defineProps({
33
+ modelValue: Boolean,
34
+ actionType: String,
35
+ rowData: Object,
36
+ typeList: Array
37
+ });
38
+
39
+ const $Emit = defineEmits<{
40
+ (e: "update:modelValue", value: boolean): void;
41
+ (e: "success"): void;
42
+ }>();
43
+
44
+ const visible = computed({
45
+ get: () => props.modelValue,
46
+ set: (val) => $Emit("update:modelValue", val)
47
+ });
48
+
49
+ type TDesignFormInstance = {
50
+ validate: () => Promise<unknown>;
51
+ };
52
+
53
+ const formRef = ref<TDesignFormInstance | null>(null);
54
+
55
+ const $Data = reactive({
56
+ formData: {
57
+ typeCode: "",
58
+ key: "",
59
+ label: "",
60
+ sort: 0,
61
+ remark: ""
62
+ },
63
+ rules: {
64
+ typeCode: [{ required: true, message: "请选择字典类型" }],
65
+ key: [{ required: true, message: "请输入键值" }],
66
+ label: [{ required: true, message: "请输入标签" }]
67
+ }
68
+ });
69
+
70
+ async function handleSubmit(): Promise<void> {
71
+ try {
72
+ const form = formRef.value;
73
+ if (!form) {
74
+ MessagePlugin.warning("表单未就绪");
75
+ return;
76
+ }
77
+
78
+ const valid = await form.validate();
79
+ if (valid !== true) return;
80
+ const apiUrl = props.actionType === "add" ? "/core/dict/ins" : "/core/dict/upd";
81
+ const params: Record<string, unknown> = {
82
+ typeCode: $Data.formData.typeCode,
83
+ key: $Data.formData.key,
84
+ label: $Data.formData.label,
85
+ sort: $Data.formData.sort,
86
+ remark: $Data.formData.remark
87
+ };
88
+ if (props.actionType === "upd" && props.rowData) {
89
+ const row = props.rowData as Record<string, unknown>;
90
+ params["id"] = row["id"];
91
+ }
92
+
93
+ const res = await $Http.post(apiUrl, params);
94
+ if (res.code === 0) {
95
+ MessagePlugin.success(props.actionType === "add" ? "添加成功" : "更新成功");
96
+ visible.value = false;
97
+ $Emit("success");
98
+ } else {
99
+ MessagePlugin.error(res.msg || "操作失败");
100
+ }
101
+ } catch (_error) {
102
+ MessagePlugin.error("操作失败");
103
+ }
104
+ }
105
+
106
+ // 该组件由父组件 v-if 控制挂载/卸载,因此无需 watch:创建时初始化一次即可
107
+ if (props.actionType === "upd" && props.rowData) {
108
+ $Data.formData.typeCode = props.rowData.typeCode || "";
109
+ $Data.formData.key = props.rowData.key || "";
110
+ $Data.formData.label = props.rowData.label || "";
111
+ $Data.formData.sort = props.rowData.sort || 0;
112
+ $Data.formData.remark = props.rowData.remark || "";
113
+ } else {
114
+ $Data.formData.typeCode = "";
115
+ $Data.formData.key = "";
116
+ $Data.formData.label = "";
117
+ $Data.formData.sort = 0;
118
+ $Data.formData.remark = "";
119
+ }
120
+ </script>