befly-shared 1.0.0 → 1.0.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/package.json CHANGED
@@ -1,23 +1,25 @@
1
1
  {
2
2
  "name": "befly-shared",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
- "description": "Befly 纯函数与纯常量共享包(不包含 Node/浏览器特定实现)",
6
- "license": "Apache-2.0",
7
- "author": "chensuiyi <bimostyle@qq.com>",
8
- "files": [
9
- "package.json",
10
- "utils"
11
- ],
5
+ "description": "Befly Shared - 通用工具函数库",
12
6
  "type": "module",
13
7
  "exports": {
14
- "./utils/*": "./utils/*.ts"
8
+ "./*": {
9
+ "import": "./utils/*.js",
10
+ "default": "./utils/*.js"
11
+ }
15
12
  },
16
- "scripts": {
17
- "test": "bun test"
13
+ "files": [
14
+ "utils/",
15
+ "package.json"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org"
18
20
  },
19
21
  "engines": {
20
22
  "bun": ">=1.3.0"
21
23
  },
22
- "gitHead": "faa8189c7d23cf45885c03d1425cba8f5bf45df9"
24
+ "gitHead": "cb5f3fe922985a84d5abd02a27e50cade25ad135"
23
25
  }
@@ -0,0 +1,90 @@
1
+ export function createHttp(options) {
2
+ const config = options && typeof options === "object" ? options : {};
3
+ const apiPath = typeof config.apiPath === "string" ? config.apiPath : "";
4
+ const getToken = typeof config.getToken === "function" ? config.getToken : () => "";
5
+
6
+ function shouldDrop(key, value, dropList, dropKeyMap) {
7
+ for (const dropValue of dropList) {
8
+ if (Object.is(value, dropValue)) {
9
+ return true;
10
+ }
11
+ }
12
+ if (Array.isArray(dropKeyMap[key])) {
13
+ for (const dropValue of dropKeyMap[key]) {
14
+ if (Object.is(value, dropValue)) {
15
+ return true;
16
+ }
17
+ }
18
+ }
19
+ return false;
20
+ }
21
+
22
+ return async function requestPost(url, data, dropValues, dropKeyValue) {
23
+ try {
24
+ const fullUrl = `${apiPath}${url}`;
25
+ const isForm = data instanceof FormData;
26
+ const dropList = Array.isArray(dropValues) ? dropValues : [null, undefined];
27
+ const dropKeyMap = dropKeyValue && typeof dropKeyValue === "object" ? dropKeyValue : {};
28
+ let payloadData = data ?? {};
29
+ if (isForm) {
30
+ const nextForm = new FormData();
31
+ for (const entry of payloadData.entries()) {
32
+ const key = entry[0];
33
+ const value = entry[1];
34
+ if (!shouldDrop(key, value, dropList, dropKeyMap)) {
35
+ nextForm.append(key, value);
36
+ }
37
+ }
38
+ payloadData = nextForm;
39
+ } else if (payloadData && typeof payloadData === "object") {
40
+ const nextData = {};
41
+ for (const key of Object.keys(payloadData)) {
42
+ const value = payloadData[key];
43
+ if (!shouldDrop(key, value, dropList, dropKeyMap)) {
44
+ nextData[key] = value;
45
+ }
46
+ }
47
+ payloadData = nextData;
48
+ }
49
+ const headers = new Headers();
50
+ if (!isForm) {
51
+ headers.set("Content-Type", "application/json");
52
+ }
53
+ const tokenValue = getToken();
54
+ if (tokenValue) {
55
+ headers.set("Authorization", "Bearer " + tokenValue);
56
+ }
57
+ const init = {
58
+ method: "POST",
59
+ headers: headers,
60
+ body: isForm ? payloadData : JSON.stringify(payloadData ?? {})
61
+ };
62
+ const res = await fetch(fullUrl, init);
63
+ let payload;
64
+ try {
65
+ payload = await res.json();
66
+ } catch {
67
+ if (res.ok) {
68
+ return Promise.reject({ msg: "响应解析失败" });
69
+ }
70
+ payload = {
71
+ code: res.status,
72
+ msg: `请求失败:HTTP ${res.status}`
73
+ };
74
+ }
75
+ if (payload.code !== 0) {
76
+ return Promise.reject({
77
+ msg: payload.msg ?? "请求失败",
78
+ code: payload.code,
79
+ detail: payload.detail
80
+ });
81
+ }
82
+ return Promise.resolve(payload);
83
+ } catch (error) {
84
+ if (error && typeof error === "object") {
85
+ return Promise.reject(error);
86
+ }
87
+ return Promise.reject({ msg: "网络连接失败" });
88
+ }
89
+ };
90
+ }
@@ -0,0 +1,125 @@
1
+ # createHttp
2
+
3
+ ## 功能说明
4
+
5
+ `createHttp(options)` 用于创建一个通用 POST 请求函数,支持:
6
+
7
+ - JSON 请求体自动序列化
8
+ - `FormData` 自动直传
9
+ - 默认值/按字段值过滤(`dropValues`、`dropKeyValue`)
10
+ - 自动附带 Bearer Token
11
+ - 统一响应与错误结构处理
12
+
13
+ 创建后返回的请求函数签名为:
14
+
15
+ - `requestPost(url, data, dropValues, dropKeyValue)`
16
+
17
+ ---
18
+
19
+ ## 入参
20
+
21
+ ### createHttp(options)
22
+
23
+ - `options.apiPath`:`string`
24
+ - 接口前缀,例如:`http://localhost:3000/api`
25
+ - `options.getToken`:`() => string`
26
+ - 获取 token 的函数
27
+ - 返回空字符串时不会添加 `Authorization`
28
+
29
+ ### requestPost(url, data, dropValues, dropKeyValue)
30
+
31
+ - `url`:`string`
32
+ - 业务接口路径,例如:`/core/login`
33
+ - `data`:`object | FormData`
34
+ - 普通对象会按 JSON 发送
35
+ - `FormData` 会按表单发送
36
+ - `dropValues`:`Array<any>`(可选)
37
+ - 全局过滤值列表
38
+ - 默认:`[null, undefined]`
39
+ - `dropKeyValue`:`Record<string, Array<any>>`(可选)
40
+ - 按字段过滤值
41
+ - 例如:`{ page: [0], keyword: [""] }`
42
+
43
+ ---
44
+
45
+ ## 返回值
46
+
47
+ ### 成功
48
+
49
+ 返回后端 JSON 对象(要求后端 `code === 0`)。
50
+
51
+ ### 失败
52
+
53
+ Promise reject 对象结构:
54
+
55
+ - `msg`: 错误消息
56
+ - `code`: 后端业务码(可选)
57
+ - `detail`: 后端明细(可选)
58
+
59
+ 特殊情况:
60
+
61
+ - 响应可读但 JSON 解析失败:`{ msg: "响应解析失败" }`
62
+ - 非 2xx 且无法按业务格式解析:`{ code: HTTP状态码, msg: "请求失败:HTTP xxx" }`
63
+ - 网络异常:`{ msg: "网络连接失败" }`
64
+
65
+ ---
66
+
67
+ ## 使用示例
68
+
69
+ ### 1)创建实例
70
+
71
+ ```javascript
72
+ import { createHttp } from "befly-shared/createHttp";
73
+
74
+ const $Http = createHttp({
75
+ apiPath: "http://localhost:3000/api",
76
+ getToken: () => localStorage.getItem("befly-admin-token") ?? ""
77
+ });
78
+ ```
79
+
80
+ ### 2)JSON 请求
81
+
82
+ ```javascript
83
+ const res = await $Http("/core/user/list", {
84
+ page: 1,
85
+ limit: 30,
86
+ keyword: ""
87
+ });
88
+ ```
89
+
90
+ ### 3)FormData 请求
91
+
92
+ ```javascript
93
+ const form = new FormData();
94
+ form.append("file", file);
95
+ form.append("remark", "头像");
96
+
97
+ const res = await $Http("/core/upload/file", form);
98
+ ```
99
+
100
+ ### 4)过滤默认值
101
+
102
+ ```javascript
103
+ const res = await $Http("/core/user/list", { page: 1, limit: 30, keyword: "", state: null }, [null, undefined, ""], { page: [0] });
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 行为细节
109
+
110
+ - 当 `data instanceof FormData` 时:
111
+ - 不设置 `Content-Type`(由浏览器自动带 boundary)
112
+ - `body` 直接传 `FormData`
113
+ - 当 `data` 是普通对象时:
114
+ - 设置 `Content-Type: application/json`
115
+ - `body` 为 `JSON.stringify(data)`
116
+ - 有 token 时自动设置:
117
+ - `Authorization: Bearer <token>`
118
+
119
+ ---
120
+
121
+ ## 注意事项
122
+
123
+ - 后端响应格式建议统一为 `{ code, msg, data, detail }`
124
+ - 仅当 `code === 0` 视为成功
125
+ - 该实现固定为 `POST` 请求
@@ -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
- }
@@ -1,127 +0,0 @@
1
- export type BuildTreeByParentPathOptions = {
2
- pathKey?: string;
3
- parentPathKey?: string;
4
- childrenKey?: string;
5
- normalize?: (value: unknown) => string;
6
- };
7
-
8
- export type BuildTreeByParentPathResult<T extends Record<string, any>> = {
9
- flat: Array<T>;
10
- tree: Array<T>;
11
- map: Map<string, T>;
12
- };
13
-
14
- /**
15
- * 将一维数组按 { path, parentPath } 组装为树形结构(纯函数 / 无副作用)。
16
- *
17
- * - 默认字段:path / parentPath / children
18
- * - parentPath 为空字符串或父节点不存在时,视为根节点
19
- * - 内部会 clone 一份节点对象,并写入 children: []
20
- * - 默认自带递归排序:按 sort 升序;sort 缺省/非法或 < 1 视为 999999;sort 相同按 path 自然序
21
- */
22
- export function buildTreeByParentPath<T extends Record<string, any>>(items: T[], options?: BuildTreeByParentPathOptions): BuildTreeByParentPathResult<T> {
23
- const pathKey = typeof options?.pathKey === "string" && options.pathKey.length > 0 ? options.pathKey : "path";
24
- const parentPathKey = typeof options?.parentPathKey === "string" && options.parentPathKey.length > 0 ? options.parentPathKey : "parentPath";
25
- const childrenKey = typeof options?.childrenKey === "string" && options.childrenKey.length > 0 ? options.childrenKey : "children";
26
-
27
- const normalize =
28
- typeof options?.normalize === "function"
29
- ? options.normalize
30
- : (value: unknown) => {
31
- return typeof value === "string" ? value : "";
32
- };
33
-
34
- const map = new Map<string, T>();
35
- const flat: T[] = [];
36
-
37
- const safeItems = Array.isArray(items) ? items : [];
38
-
39
- for (const item of safeItems) {
40
- const rawPath = item ? item[pathKey] : undefined;
41
- const rawParentPath = item ? item[parentPathKey] : undefined;
42
-
43
- const normalizedPath = normalize(rawPath);
44
- const normalizedParentPath = normalize(rawParentPath);
45
-
46
- const nextNode = Object.assign({}, item) as T;
47
- (nextNode as any)[pathKey] = normalizedPath;
48
- (nextNode as any)[parentPathKey] = normalizedParentPath;
49
- (nextNode as any)[childrenKey] = [];
50
-
51
- flat.push(nextNode);
52
-
53
- if (typeof normalizedPath === "string" && normalizedPath.length > 0) {
54
- map.set(normalizedPath, nextNode);
55
- }
56
- }
57
-
58
- const tree: T[] = [];
59
- for (const node of flat) {
60
- const parentPath = (node as any)[parentPathKey];
61
-
62
- if (typeof parentPath === "string" && parentPath.length > 0) {
63
- const parent = map.get(parentPath);
64
- if (parent && Array.isArray((parent as any)[childrenKey])) {
65
- (parent as any)[childrenKey].push(node);
66
- continue;
67
- }
68
- }
69
-
70
- tree.push(node);
71
- }
72
-
73
- const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
74
-
75
- const getSortValue = (node: T): number => {
76
- const raw = node ? (node as any).sort : undefined;
77
- if (typeof raw !== "number") {
78
- return 999999;
79
- }
80
-
81
- if (!Number.isFinite(raw)) {
82
- return 999999;
83
- }
84
-
85
- if (raw < 1) {
86
- return 999999;
87
- }
88
-
89
- return raw;
90
- };
91
-
92
- const compareNode = (a: T, b: T): number => {
93
- const aSort = getSortValue(a);
94
- const bSort = getSortValue(b);
95
-
96
- if (aSort !== bSort) {
97
- return aSort - bSort;
98
- }
99
-
100
- const aPath = a ? (a as any)[pathKey] : "";
101
- const bPath = b ? (b as any)[pathKey] : "";
102
- return collator.compare(typeof aPath === "string" ? aPath : "", typeof bPath === "string" ? bPath : "");
103
- };
104
-
105
- const sortTreeInPlace = (nodes: Array<T>): void => {
106
- if (!Array.isArray(nodes) || nodes.length <= 1) {
107
- return;
108
- }
109
-
110
- nodes.sort(compareNode);
111
-
112
- for (const node of nodes) {
113
- const children = node ? (node as any)[childrenKey] : undefined;
114
- if (Array.isArray(children) && children.length > 0) {
115
- sortTreeInPlace(children);
116
- }
117
- }
118
- };
119
-
120
- sortTreeInPlace(tree);
121
-
122
- return {
123
- flat: flat,
124
- tree: tree,
125
- map: map
126
- };
127
- }
@@ -1,148 +0,0 @@
1
- export type ViewDirMeta = {
2
- title: string;
3
- order?: number;
4
- };
5
-
6
- type MenuNodeLike<T> = {
7
- name?: string;
8
- path?: string;
9
- sort?: number;
10
- children?: T[];
11
- };
12
-
13
- /**
14
- * 清理目录名中的数字后缀
15
- * 如:login_1 → login, index_2 → index
16
- */
17
- export function cleanDirName(name: string): string {
18
- return name.replace(/_\d+$/, "");
19
- }
20
-
21
- /**
22
- * 约束:统一 path 形态,避免隐藏菜单匹配、DB 同步出现重复
23
- * - 必须以 / 开头
24
- * - 折叠多个 /
25
- * - 去掉尾随 /(根 / 除外)
26
- */
27
- export function normalizeMenuPath(path: string): string {
28
- let result = path;
29
-
30
- if (!result) {
31
- return "/";
32
- }
33
-
34
- if (!result.startsWith("/")) {
35
- result = `/${result}`;
36
- }
37
-
38
- result = result.replace(/\/+/g, "/");
39
-
40
- if (result.length > 1) {
41
- result = result.replace(/\/+$/, "");
42
- }
43
-
44
- return result;
45
- }
46
-
47
- /**
48
- * 递归规范化并按 path 去重(同 path 的 children 合并)
49
- *
50
- * 说明:该函数是纯函数,不依赖任何运行时环境;会返回新数组,但会在内部对克隆对象做合并赋值。
51
- */
52
- export function normalizeMenuTree<T extends MenuNodeLike<T>>(menus: T[]): T[] {
53
- const map = new Map<string, T>();
54
-
55
- for (const menu of menus) {
56
- const rawPath = menu.path;
57
- const menuPath = rawPath ? normalizeMenuPath(rawPath) : "";
58
-
59
- if (!menuPath) {
60
- continue;
61
- }
62
-
63
- // 不使用 structuredClone:
64
- // - 结构中可能出现函数/类实例等不可 clone 的值
65
- // - 这里我们只需要“保留额外字段 + 递归 children 生成新数组”
66
- // 用浅拷贝即可满足需求
67
- const cloned = Object.assign({}, menu) as T;
68
- (cloned as any).path = menuPath;
69
-
70
- const rawChildren = menu.children;
71
- if (rawChildren && rawChildren.length > 0) {
72
- (cloned as any).children = normalizeMenuTree(rawChildren);
73
- }
74
-
75
- const existing = map.get(menuPath);
76
- if (existing) {
77
- const clonedChildren = (cloned as any).children as T[] | undefined;
78
- if (clonedChildren && clonedChildren.length > 0) {
79
- let existingChildren = (existing as any).children as T[] | undefined;
80
- if (!existingChildren) {
81
- existingChildren = [];
82
- (existing as any).children = existingChildren;
83
- }
84
-
85
- for (const child of clonedChildren) {
86
- existingChildren.push(child);
87
- }
88
-
89
- (existing as any).children = normalizeMenuTree(existingChildren);
90
- }
91
-
92
- if (typeof cloned.sort === "number") {
93
- (existing as any).sort = cloned.sort;
94
- }
95
-
96
- if (typeof cloned.name === "string" && cloned.name) {
97
- (existing as any).name = cloned.name;
98
- }
99
- } else {
100
- map.set(menuPath, cloned);
101
- }
102
- }
103
-
104
- const result = Array.from(map.values());
105
- result.sort((a, b) => ((a as any).sort ?? 999999) - ((b as any).sort ?? 999999));
106
- return result;
107
- }
108
-
109
- /**
110
- * 只取第一个 <script ... setup ...> 块
111
- */
112
- export function extractScriptSetupBlock(vueContent: string): string | null {
113
- const openTag = /<script\b[^>]*\bsetup\b[^>]*>/i.exec(vueContent);
114
- if (!openTag) {
115
- return null;
116
- }
117
-
118
- const start = openTag.index + openTag[0].length;
119
- const closeIndex = vueContent.indexOf("</script>", start);
120
- if (closeIndex < 0) {
121
- return null;
122
- }
123
-
124
- return vueContent.slice(start, closeIndex);
125
- }
126
-
127
- /**
128
- * 从 <script setup> 中提取 definePage({ meta })
129
- *
130
- * 简化约束:
131
- * - 每个页面只有一个 definePage
132
- * - title 是纯字符串字面量
133
- * - order 是数字字面量(可选)
134
- * - 不考虑变量/表达式/多段 meta 组合
135
- */
136
- export function extractDefinePageMetaFromScriptSetup(scriptSetup: string): ViewDirMeta | null {
137
- const titleMatch = scriptSetup.match(/definePage\s*\([\s\S]*?meta\s*:\s*\{[\s\S]*?title\s*:\s*(["'`])([^"'`]+)\1/);
138
- if (!titleMatch) {
139
- return null;
140
- }
141
-
142
- const orderMatch = scriptSetup.match(/definePage\s*\([\s\S]*?meta\s*:\s*\{[\s\S]*?order\s*:\s*(\d+)/);
143
-
144
- return {
145
- title: titleMatch[2],
146
- order: orderMatch ? Number(orderMatch[1]) : undefined
147
- };
148
- }