@vue-spark/app-helpers 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/permission/index.d.ts +100 -0
- package/dist/permission/index.js +118 -0
- package/dist/sso/index.d.ts +103 -0
- package/dist/sso/index.js +89 -0
- package/dist/tabs/index.d.ts +126 -0
- package/dist/tabs/index.js +126 -0
- package/dist/utils-BwZlLmaw.js +12 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2025 leihaohao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @vue-spark/app-helpers
|
|
2
|
+
|
|
3
|
+
轻量级 Vue3 应用开发辅助工具包。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @vue-spark/app-helpers
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使用方式
|
|
12
|
+
|
|
13
|
+
请参考 [./src](./src) 目录下的各工具的**README.md**文件。
|
|
14
|
+
|
|
15
|
+
## License
|
|
16
|
+
|
|
17
|
+
[MIT](./LICENSE) License © 2025 [leihaohao](https://github.com/l246804)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AllowedComponentProps, App, ComponentCustomProps, Directive, InjectionKey, ShallowReactive, VNodeProps } from "vue";
|
|
2
|
+
|
|
3
|
+
//#region src/permission/core.d.ts
|
|
4
|
+
declare const PERMISSION_CODE_ALL = "*";
|
|
5
|
+
type PermissionCodeAll = typeof PERMISSION_CODE_ALL;
|
|
6
|
+
type PermissionCode = PermissionCodeAll | (string & {});
|
|
7
|
+
type PermissionOperator = 'or' | 'and';
|
|
8
|
+
interface Permission {
|
|
9
|
+
get codes(): ShallowReactive<readonly string[]>;
|
|
10
|
+
add: (codes: PermissionCode | PermissionCode[]) => void;
|
|
11
|
+
set: (codes: PermissionCode | PermissionCode[]) => void;
|
|
12
|
+
clear: () => void;
|
|
13
|
+
/**
|
|
14
|
+
* check permission
|
|
15
|
+
* @param codes permission codes
|
|
16
|
+
* @param op check permission operator, default is 'or'
|
|
17
|
+
* @returns whether the user has permission
|
|
18
|
+
*/
|
|
19
|
+
check: (codes: PermissionCode | PermissionCode[], op?: PermissionOperator) => boolean;
|
|
20
|
+
install: (app: App) => void;
|
|
21
|
+
}
|
|
22
|
+
interface PermissionOptions {
|
|
23
|
+
/**
|
|
24
|
+
* initial permission codes
|
|
25
|
+
*/
|
|
26
|
+
initialCodes?: PermissionCode | PermissionCode[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create permission instance
|
|
30
|
+
* @param options permission options
|
|
31
|
+
*/
|
|
32
|
+
declare function createPermission(options?: PermissionOptions): Permission;
|
|
33
|
+
declare module 'vue' {
|
|
34
|
+
interface ComponentCustomProperties {
|
|
35
|
+
$permission: Permission;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/permission/component.d.ts
|
|
40
|
+
interface PermissionGuardProps {
|
|
41
|
+
codes: PermissionCode | PermissionCode[];
|
|
42
|
+
/**
|
|
43
|
+
* check permission operator
|
|
44
|
+
* @default 'or'
|
|
45
|
+
*/
|
|
46
|
+
op?: PermissionOperator;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A component to guard permission
|
|
50
|
+
* @example
|
|
51
|
+
* ```html
|
|
52
|
+
* <script setup lang="ts">
|
|
53
|
+
* import { usePermission, PermissionGuard } from '@vue-spark/app-helpers/permission'
|
|
54
|
+
*
|
|
55
|
+
* const permission = usePermission()
|
|
56
|
+
* permission.set(['list:add', 'list:edit', 'list:delete'])
|
|
57
|
+
* </script>
|
|
58
|
+
*
|
|
59
|
+
* <template>
|
|
60
|
+
* <!-- only show when user has permission -->
|
|
61
|
+
* <PermissionGuard :codes="['list:delete']" op="or">
|
|
62
|
+
* <button>Delete</button>
|
|
63
|
+
* </PermissionGuard>
|
|
64
|
+
* </template>
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare const PermissionGuard: {
|
|
68
|
+
new (): {
|
|
69
|
+
$props: AllowedComponentProps & ComponentCustomProps & VNodeProps & PermissionGuardProps;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/permission/directive.d.ts
|
|
74
|
+
/**
|
|
75
|
+
* A permission directive
|
|
76
|
+
* @example
|
|
77
|
+
* ```html
|
|
78
|
+
* <script setup lang="ts">
|
|
79
|
+
* import { vPermission, usePermission } from '@vue-spark/app-helpers/permission'
|
|
80
|
+
*
|
|
81
|
+
* const permission = usePermission()
|
|
82
|
+
* permission.set(['list:add', 'list:edit', 'list:delete'])
|
|
83
|
+
* </script>
|
|
84
|
+
*
|
|
85
|
+
* <template>
|
|
86
|
+
* <!-- only show when user has permission -->
|
|
87
|
+
* <button v-permission:or="['list:delete']">Delete</button>
|
|
88
|
+
* </template>
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare const vPermission: Directive<Element, PermissionCode | PermissionCode[], string, PermissionOperator>;
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/permission/injection.d.ts
|
|
94
|
+
declare const permissionKey: InjectionKey<Permission>;
|
|
95
|
+
/**
|
|
96
|
+
* Inject permission instance
|
|
97
|
+
*/
|
|
98
|
+
declare function usePermission(): Permission;
|
|
99
|
+
//#endregion
|
|
100
|
+
export { PERMISSION_CODE_ALL, Permission, PermissionCode, PermissionCodeAll, PermissionGuard, PermissionGuardProps, PermissionOperator, PermissionOptions, createPermission, permissionKey, usePermission, vPermission };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ensureArray } from "../utils-BwZlLmaw.js";
|
|
2
|
+
import { defineComponent, inject, shallowRef, triggerRef } from "vue";
|
|
3
|
+
|
|
4
|
+
//#region src/permission/injection.ts
|
|
5
|
+
const permissionKey = Symbol("permission key");
|
|
6
|
+
/**
|
|
7
|
+
* Inject permission instance
|
|
8
|
+
*/
|
|
9
|
+
function usePermission() {
|
|
10
|
+
return inject(permissionKey);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/permission/component.ts
|
|
15
|
+
const PermissionGuardImpl = /* @__PURE__ */ defineComponent({
|
|
16
|
+
name: "PermissionGuard",
|
|
17
|
+
props: {
|
|
18
|
+
codes: {
|
|
19
|
+
type: [String, Array],
|
|
20
|
+
required: true
|
|
21
|
+
},
|
|
22
|
+
op: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: "or"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
setup(props, { slots }) {
|
|
28
|
+
const permission = usePermission();
|
|
29
|
+
return () => {
|
|
30
|
+
return slots.default && permission.check(props.codes, props.op) ? slots.default() : null;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* A component to guard permission
|
|
36
|
+
* @example
|
|
37
|
+
* ```html
|
|
38
|
+
* <script setup lang="ts">
|
|
39
|
+
* import { usePermission, PermissionGuard } from '@vue-spark/app-helpers/permission'
|
|
40
|
+
*
|
|
41
|
+
* const permission = usePermission()
|
|
42
|
+
* permission.set(['list:add', 'list:edit', 'list:delete'])
|
|
43
|
+
* </script>
|
|
44
|
+
*
|
|
45
|
+
* <template>
|
|
46
|
+
* <!-- only show when user has permission -->
|
|
47
|
+
* <PermissionGuard :codes="['list:delete']" op="or">
|
|
48
|
+
* <button>Delete</button>
|
|
49
|
+
* </PermissionGuard>
|
|
50
|
+
* </template>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
const PermissionGuard = PermissionGuardImpl;
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/permission/core.ts
|
|
57
|
+
const PERMISSION_CODE_ALL = "*";
|
|
58
|
+
/**
|
|
59
|
+
* Create permission instance
|
|
60
|
+
* @param options permission options
|
|
61
|
+
*/
|
|
62
|
+
function createPermission(options = {}) {
|
|
63
|
+
const codeSet = shallowRef(new Set(ensureArray(options.initialCodes ?? [])));
|
|
64
|
+
const permission = {
|
|
65
|
+
get codes() {
|
|
66
|
+
return [...codeSet.value];
|
|
67
|
+
},
|
|
68
|
+
add(codes) {
|
|
69
|
+
ensureArray(codes).forEach((code) => codeSet.value.add(code));
|
|
70
|
+
triggerRef(codeSet);
|
|
71
|
+
},
|
|
72
|
+
set(codes) {
|
|
73
|
+
codeSet.value = new Set(ensureArray(codes));
|
|
74
|
+
},
|
|
75
|
+
clear() {
|
|
76
|
+
codeSet.value = new Set();
|
|
77
|
+
},
|
|
78
|
+
check(codes, op = "or") {
|
|
79
|
+
if (codeSet.value.has(PERMISSION_CODE_ALL)) return true;
|
|
80
|
+
if (codeSet.value.size === 0) return false;
|
|
81
|
+
return ensureArray(codes)[op === "and" ? "every" : "some"]((code) => codeSet.value.has(code));
|
|
82
|
+
},
|
|
83
|
+
install(app) {
|
|
84
|
+
app.config.globalProperties.$permission = permission;
|
|
85
|
+
app.provide(permissionKey, permission);
|
|
86
|
+
app.onUnmount(() => permission.clear());
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return permission;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/permission/directive.ts
|
|
94
|
+
/**
|
|
95
|
+
* A permission directive
|
|
96
|
+
* @example
|
|
97
|
+
* ```html
|
|
98
|
+
* <script setup lang="ts">
|
|
99
|
+
* import { vPermission, usePermission } from '@vue-spark/app-helpers/permission'
|
|
100
|
+
*
|
|
101
|
+
* const permission = usePermission()
|
|
102
|
+
* permission.set(['list:add', 'list:edit', 'list:delete'])
|
|
103
|
+
* </script>
|
|
104
|
+
*
|
|
105
|
+
* <template>
|
|
106
|
+
* <!-- only show when user has permission -->
|
|
107
|
+
* <button v-permission:or="['list:delete']">Delete</button>
|
|
108
|
+
* </template>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
const vPermission = { mounted(el, binding) {
|
|
112
|
+
const { value, arg, instance } = binding;
|
|
113
|
+
const permission = instance && instance.$permission;
|
|
114
|
+
if (!permission || !permission.check(value, arg)) el.remove();
|
|
115
|
+
} };
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { PERMISSION_CODE_ALL, PermissionGuard, createPermission, permissionKey, usePermission, vPermission };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//#region src/sso/parseUrl/param.d.ts
|
|
2
|
+
type ParamName = string;
|
|
3
|
+
interface Param {
|
|
4
|
+
/**
|
|
5
|
+
* 参数名称
|
|
6
|
+
*/
|
|
7
|
+
name: ParamName;
|
|
8
|
+
/**
|
|
9
|
+
* 该参数是否必填
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
required?: boolean;
|
|
13
|
+
}
|
|
14
|
+
type ParamRaw = ParamName | Param;
|
|
15
|
+
declare function normalizeParams(params: ParamRaw[]): Param[];
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/sso/parseUrl/paramGroup.d.ts
|
|
18
|
+
type ParamGroupKey = PropertyKey;
|
|
19
|
+
interface ParamGroup {
|
|
20
|
+
/**
|
|
21
|
+
* 参数组标识
|
|
22
|
+
*/
|
|
23
|
+
key: ParamGroupKey;
|
|
24
|
+
/**
|
|
25
|
+
* 参数组的参数配置列表
|
|
26
|
+
*/
|
|
27
|
+
params: Param[];
|
|
28
|
+
}
|
|
29
|
+
type ParamGroupsRaw = Record<ParamGroupKey, ParamRaw[]>;
|
|
30
|
+
declare function normalizeParamGroups(groups: ParamGroupsRaw): ParamGroup[];
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/sso/parseUrl/index.d.ts
|
|
33
|
+
interface ParseUrlOptions {
|
|
34
|
+
/**
|
|
35
|
+
* 被解析的 URL
|
|
36
|
+
* @default window.location.href
|
|
37
|
+
*/
|
|
38
|
+
url?: string | URL;
|
|
39
|
+
/**
|
|
40
|
+
* 需要解析的参数组,数组时为默认组
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* // 使用默认分组
|
|
44
|
+
* ['access_token', 'domain']
|
|
45
|
+
*
|
|
46
|
+
* // 自定义分组
|
|
47
|
+
* {
|
|
48
|
+
* success: ['access_token', { name: 'domain', required: false }],
|
|
49
|
+
* error: ['error']
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
paramGroups: ParamRaw[] | ParamGroupsRaw;
|
|
54
|
+
}
|
|
55
|
+
interface ParseUrlResult {
|
|
56
|
+
/**
|
|
57
|
+
* 验证成功的参数组
|
|
58
|
+
*/
|
|
59
|
+
group: ParamGroup;
|
|
60
|
+
/**
|
|
61
|
+
* 根据参数组提取的数据
|
|
62
|
+
*/
|
|
63
|
+
data: Record<string, string | null>;
|
|
64
|
+
/**
|
|
65
|
+
* 被解析的 URL
|
|
66
|
+
*/
|
|
67
|
+
rawUrl: string | URL;
|
|
68
|
+
/**
|
|
69
|
+
* 清理参数后的 URL
|
|
70
|
+
*/
|
|
71
|
+
cleanUrl: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 根据参数组解析 URL 并返回结果
|
|
75
|
+
* @param options 配置项
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const parsed = parseUrl({ paramGroups: ['access_token'] })
|
|
80
|
+
* if (parsed) {
|
|
81
|
+
* localStorage.setItem('access_token', parsed.data.access_token)
|
|
82
|
+
* history.replaceState(null, '', parsed.cleanUrl)
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare function parseUrl(options: ParseUrlOptions): ParseUrlResult | null;
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/sso/removeUrlSearchParams.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* 移除 URL 中指定的参数
|
|
91
|
+
* @param url URL
|
|
92
|
+
* @param params 需要移除的参数名列表,设为 `true` 表示移除所有参数
|
|
93
|
+
*/
|
|
94
|
+
declare function removeUrlSearchParams(url: string | URL, params: true | string[]): string;
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/sso/resolveUrlSearchParams.d.ts
|
|
97
|
+
/**
|
|
98
|
+
* 根据 URL 获取 {@link URLSearchParams} 实例
|
|
99
|
+
* @param url URL
|
|
100
|
+
*/
|
|
101
|
+
declare function resolveUrlSearchParams(url: string | URL): URLSearchParams;
|
|
102
|
+
//#endregion
|
|
103
|
+
export { Param, ParamGroup, ParamGroupKey, ParamGroupsRaw, ParamName, ParamRaw, ParseUrlOptions, ParseUrlResult, normalizeParamGroups, normalizeParams, parseUrl, removeUrlSearchParams, resolveUrlSearchParams };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { assign, isArray, isObject } from "../utils-BwZlLmaw.js";
|
|
2
|
+
|
|
3
|
+
//#region src/sso/resolveUrlSearchParams.ts
|
|
4
|
+
/**
|
|
5
|
+
* 根据 URL 获取 {@link URLSearchParams} 实例
|
|
6
|
+
* @param url URL
|
|
7
|
+
*/
|
|
8
|
+
function resolveUrlSearchParams(url) {
|
|
9
|
+
const urlString = url.toString();
|
|
10
|
+
return new URLSearchParams(urlString.includes("?") ? urlString.slice(urlString.lastIndexOf("?")) : "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/sso/removeUrlSearchParams.ts
|
|
15
|
+
/**
|
|
16
|
+
* 移除 URL 中指定的参数
|
|
17
|
+
* @param url URL
|
|
18
|
+
* @param params 需要移除的参数名列表,设为 `true` 表示移除所有参数
|
|
19
|
+
*/
|
|
20
|
+
function removeUrlSearchParams(url, params) {
|
|
21
|
+
const searchParams = resolveUrlSearchParams(url);
|
|
22
|
+
url = url.toString();
|
|
23
|
+
if (url.includes("?")) url = url.slice(0, url.lastIndexOf("?"));
|
|
24
|
+
if (searchParams.size && isArray(params)) {
|
|
25
|
+
params.forEach((param) => searchParams.delete(param));
|
|
26
|
+
if (searchParams.size) url += `?${searchParams}`;
|
|
27
|
+
}
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/sso/parseUrl/param.ts
|
|
33
|
+
function normalizeParams(params) {
|
|
34
|
+
return params.map((p) => assign({ required: true }, !isObject(p) ? { name: p } : p));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/sso/parseUrl/paramGroup.ts
|
|
39
|
+
function normalizeParamGroups(groups) {
|
|
40
|
+
return Reflect.ownKeys(groups).map((key) => ({
|
|
41
|
+
key,
|
|
42
|
+
params: normalizeParams(groups[key])
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/sso/parseUrl/index.ts
|
|
48
|
+
const defaultGroupKey = Symbol("default group");
|
|
49
|
+
/**
|
|
50
|
+
* 根据参数组解析 URL 并返回结果
|
|
51
|
+
* @param options 配置项
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const parsed = parseUrl({ paramGroups: ['access_token'] })
|
|
56
|
+
* if (parsed) {
|
|
57
|
+
* localStorage.setItem('access_token', parsed.data.access_token)
|
|
58
|
+
* history.replaceState(null, '', parsed.cleanUrl)
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function parseUrl(options) {
|
|
63
|
+
const { url = window.location.href, paramGroups: paramGroupsRaw } = options;
|
|
64
|
+
const searchParams = resolveUrlSearchParams(url);
|
|
65
|
+
if (!searchParams.size) return null;
|
|
66
|
+
const paramGroups = normalizeParamGroups(isArray(paramGroupsRaw) ? { [defaultGroupKey]: paramGroupsRaw } : paramGroupsRaw);
|
|
67
|
+
for (const group of paramGroups) if (validateGroup(searchParams, group)) return {
|
|
68
|
+
group,
|
|
69
|
+
data: pickGroupData(searchParams, group),
|
|
70
|
+
rawUrl: url,
|
|
71
|
+
cleanUrl: removeUrlSearchParams(url, group.params.map((p) => p.name))
|
|
72
|
+
};
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function pickGroupData(searchParams, group) {
|
|
76
|
+
return group.params.reduce((acc, param) => {
|
|
77
|
+
acc[param.name] = searchParams.get(param.name);
|
|
78
|
+
return acc;
|
|
79
|
+
}, {});
|
|
80
|
+
}
|
|
81
|
+
function validateGroup(searchParams, group) {
|
|
82
|
+
return group.params.length > 0 && group.params.every((param) => {
|
|
83
|
+
if (!param.required) return true;
|
|
84
|
+
return searchParams.has(param.name);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { normalizeParamGroups, normalizeParams, parseUrl, removeUrlSearchParams, resolveUrlSearchParams };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//#region src/tabs/index.d.ts
|
|
2
|
+
type TabType = string;
|
|
3
|
+
type TabsSideType = 'left' | 'right';
|
|
4
|
+
interface TabsHelperOptions<TabData extends {}> {
|
|
5
|
+
/**
|
|
6
|
+
* 判断标签是否可移除,返回假值时,标签将不可移除
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* createTabsHelper({
|
|
10
|
+
* // 固定标签不可以移除
|
|
11
|
+
* isRemovable: ({ tabData }) => !tabData.isFixed
|
|
12
|
+
* })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
isRemovable?: (ctx: {
|
|
16
|
+
tab: TabType;
|
|
17
|
+
tabData: TabData;
|
|
18
|
+
}) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 移除标签前触发,返回假值时阻止移除,返回 {@link Promise} 时若被 `reject` 也可以阻止移除,
|
|
21
|
+
* 但是 `resolve` 时仍需返回真值
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* createTabsHelper({
|
|
25
|
+
* beforeRemove: ({ tabData }) => {
|
|
26
|
+
* return Modal.confirm(`确定要移除标签【${tabData.title}】吗?`).then(() => true)
|
|
27
|
+
* }
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
beforeRemove?: (ctx: {
|
|
32
|
+
tab: TabType;
|
|
33
|
+
tabData: TabData;
|
|
34
|
+
}) => boolean | Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
interface TabsHelper<TabData extends {}> {
|
|
37
|
+
/**
|
|
38
|
+
* 配置项
|
|
39
|
+
*/
|
|
40
|
+
options: TabsHelperOptions<TabData>;
|
|
41
|
+
/**
|
|
42
|
+
* 当前激活的标签
|
|
43
|
+
*/
|
|
44
|
+
get activeTab(): TabType | undefined;
|
|
45
|
+
set activeTab(tab: TabType);
|
|
46
|
+
/**
|
|
47
|
+
* 获取标签数据
|
|
48
|
+
* @param tab 标签
|
|
49
|
+
*/
|
|
50
|
+
getTabData: (tab: TabType) => TabData | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* 设置标签数据,若标签不存在则跳过
|
|
53
|
+
* @param tab 标签
|
|
54
|
+
* @param tabData 标签数据
|
|
55
|
+
*/
|
|
56
|
+
setTabData: (tab: TabType, tabData: TabData) => void;
|
|
57
|
+
/**
|
|
58
|
+
* 获取所有标签和标签数据
|
|
59
|
+
*/
|
|
60
|
+
getTabs: () => [tab: TabType, tabData: TabData][];
|
|
61
|
+
/**
|
|
62
|
+
* 设置所有标签和标签数据
|
|
63
|
+
* @param tabs 标签和标签数据
|
|
64
|
+
*/
|
|
65
|
+
setTabs: (tabs: [tab: TabType, tabData: TabData][]) => void;
|
|
66
|
+
/**
|
|
67
|
+
* 获取指定标签的索引
|
|
68
|
+
* @param tab 指定标签
|
|
69
|
+
*/
|
|
70
|
+
indexOf: (tab: TabType) => number;
|
|
71
|
+
/**
|
|
72
|
+
* 获取指定标签的指定方向侧所有标签和标签数据
|
|
73
|
+
* @param tab 指定标签
|
|
74
|
+
* @param side 指定方向侧
|
|
75
|
+
*/
|
|
76
|
+
getSideTabs: (tab: TabType, side: TabsSideType) => [tab: TabType, tabData: TabData][];
|
|
77
|
+
/**
|
|
78
|
+
* 添加标签和标签数据,遇到重复的标签仅更新标签数据
|
|
79
|
+
* @param tab 标签
|
|
80
|
+
* @param tabData 标签数据
|
|
81
|
+
*/
|
|
82
|
+
addTab: (tab: TabType, tabData: TabData) => void;
|
|
83
|
+
/**
|
|
84
|
+
* 判断是否存在指定标签
|
|
85
|
+
* @param tab 标签
|
|
86
|
+
*/
|
|
87
|
+
hasTab: (tab: TabType) => boolean;
|
|
88
|
+
/**
|
|
89
|
+
* 判断指定标签是否可以移除,若列表长度小于等于 `1` 则永久返回 `false`
|
|
90
|
+
* @param tab 指定标签,默认为当前激活的标签
|
|
91
|
+
*/
|
|
92
|
+
canRemoveTab: (tab?: TabType) => boolean;
|
|
93
|
+
/**
|
|
94
|
+
* 判断除过指定标签的其他标签是否有可移除的标签
|
|
95
|
+
* @param tab 指定标签,默认为当前激活的标签
|
|
96
|
+
*/
|
|
97
|
+
canRemoveOtherTabs: (tab?: TabType) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* 判断指定标签的指定方向侧是否有可移除的标签
|
|
100
|
+
* @param tab 指定标签,默认为当前激活的标签
|
|
101
|
+
*/
|
|
102
|
+
canRemoveSideTabs: (side: TabsSideType, tab?: TabType) => boolean;
|
|
103
|
+
/**
|
|
104
|
+
* 尝试移除指定标签列表,只有返回真值才可以真正移除
|
|
105
|
+
* @param tabs 标签列表
|
|
106
|
+
*/
|
|
107
|
+
tryRemoveTabs: (tabs: (TabType | [tab: TabType, tabData: TabData])[]) => Promise<boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* 移除指定标签,若移除的是当前激活标签则移除前会自动激活下一个标签
|
|
110
|
+
* @param tab 指定标签,默认为当前激活的标签
|
|
111
|
+
*/
|
|
112
|
+
removeTab: (tab?: TabType) => Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* 移除除过指定标签的其他可移除的标签,并将指定标签设置为激活标签
|
|
115
|
+
* @param tab 指定标签,默认为当前激活的标签
|
|
116
|
+
*/
|
|
117
|
+
removeOtherTabs: (tab?: TabType) => Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* 移除指定标签的指定方向侧的所有可移除的标签,若当前激活的标签存在被移除的标签内,则将指定标签设置为激活标签
|
|
120
|
+
* @param tab 指定标签,默认为当前激活的标签
|
|
121
|
+
*/
|
|
122
|
+
removeSideTabs: (side: TabsSideType, tab?: TabType) => Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
declare function createTabsHelper<TabData extends {}>(userOptions?: TabsHelperOptions<TabData>): TabsHelper<TabData>;
|
|
125
|
+
//#endregion
|
|
126
|
+
export { TabsHelper, TabsHelperOptions, TabsSideType, createTabsHelper };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { assign, isArray } from "../utils-BwZlLmaw.js";
|
|
2
|
+
import { shallowRef, triggerRef } from "vue";
|
|
3
|
+
|
|
4
|
+
//#region src/tabs/index.ts
|
|
5
|
+
const defaultOptions = {
|
|
6
|
+
isRemovable: () => true,
|
|
7
|
+
beforeRemove: () => true
|
|
8
|
+
};
|
|
9
|
+
function createTabsHelper(userOptions = {}) {
|
|
10
|
+
const options = assign({}, defaultOptions, userOptions);
|
|
11
|
+
const activeTab = shallowRef();
|
|
12
|
+
const tabMap = shallowRef(new Map());
|
|
13
|
+
const getRealTab = (targetTab) => {
|
|
14
|
+
return isArray(targetTab) ? targetTab[0] : targetTab;
|
|
15
|
+
};
|
|
16
|
+
const canRemoveTab = (targetTab) => {
|
|
17
|
+
if (tabMap.value.size <= 1) return false;
|
|
18
|
+
targetTab = getRealTab(targetTab);
|
|
19
|
+
const tabData = tabMap.value.get(targetTab);
|
|
20
|
+
return !tabData || options.isRemovable({
|
|
21
|
+
tab: targetTab,
|
|
22
|
+
tabData
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const setTab = (targetTab, tabData) => {
|
|
26
|
+
tabMap.value.set(targetTab, assign({}, tabData));
|
|
27
|
+
triggerRef(tabMap);
|
|
28
|
+
};
|
|
29
|
+
const removeTabs = (targetTabs) => {
|
|
30
|
+
targetTabs.forEach((tab) => tabMap.value.delete(getRealTab(tab)));
|
|
31
|
+
triggerRef(tabMap);
|
|
32
|
+
};
|
|
33
|
+
const setActiveTab = (targetTab) => {
|
|
34
|
+
activeTab.value = getRealTab(targetTab);
|
|
35
|
+
};
|
|
36
|
+
const helper = {
|
|
37
|
+
options,
|
|
38
|
+
get activeTab() {
|
|
39
|
+
return activeTab.value;
|
|
40
|
+
},
|
|
41
|
+
set activeTab(targetTab) {
|
|
42
|
+
setActiveTab(targetTab);
|
|
43
|
+
},
|
|
44
|
+
getTabData(targetTab) {
|
|
45
|
+
return tabMap.value.get(targetTab);
|
|
46
|
+
},
|
|
47
|
+
setTabData(targetTab, tabData) {
|
|
48
|
+
helper.hasTab(targetTab) && setTab(targetTab, tabData);
|
|
49
|
+
},
|
|
50
|
+
getTabs() {
|
|
51
|
+
return [...tabMap.value];
|
|
52
|
+
},
|
|
53
|
+
indexOf(targetTab) {
|
|
54
|
+
return [...tabMap.value.keys()].indexOf(targetTab);
|
|
55
|
+
},
|
|
56
|
+
getSideTabs(targetTab, side) {
|
|
57
|
+
const tabs = helper.getTabs();
|
|
58
|
+
const index = helper.indexOf(targetTab);
|
|
59
|
+
return side === "left" ? tabs.slice(0, index) : tabs.slice(index + 1);
|
|
60
|
+
},
|
|
61
|
+
setTabs(tabs) {
|
|
62
|
+
tabMap.value = new Map(tabs);
|
|
63
|
+
},
|
|
64
|
+
addTab(targetTab, tabData) {
|
|
65
|
+
setTab(targetTab, tabData);
|
|
66
|
+
setActiveTab(targetTab);
|
|
67
|
+
},
|
|
68
|
+
hasTab(targetTab) {
|
|
69
|
+
return tabMap.value.has(targetTab);
|
|
70
|
+
},
|
|
71
|
+
canRemoveTab(targetTab = helper.activeTab) {
|
|
72
|
+
return !!targetTab && canRemoveTab(targetTab);
|
|
73
|
+
},
|
|
74
|
+
canRemoveOtherTabs(targetTab = helper.activeTab) {
|
|
75
|
+
return !!targetTab && helper.getTabs().some(([tab]) => tab !== targetTab && canRemoveTab(tab));
|
|
76
|
+
},
|
|
77
|
+
canRemoveSideTabs(side, targetTab = helper.activeTab) {
|
|
78
|
+
return !!targetTab && helper.getSideTabs(targetTab, side).some(canRemoveTab);
|
|
79
|
+
},
|
|
80
|
+
async tryRemoveTabs(targetTabs) {
|
|
81
|
+
let valid = tabMap.value.size > 0 && targetTabs.length > 0;
|
|
82
|
+
for (const targetTab of targetTabs) {
|
|
83
|
+
const realTab = getRealTab(targetTab);
|
|
84
|
+
try {
|
|
85
|
+
const allowRemove = await options.beforeRemove({
|
|
86
|
+
tab: realTab,
|
|
87
|
+
tabData: helper.getTabData(realTab)
|
|
88
|
+
});
|
|
89
|
+
valid = valid && !!allowRemove;
|
|
90
|
+
} catch {
|
|
91
|
+
valid = false;
|
|
92
|
+
}
|
|
93
|
+
if (!valid) break;
|
|
94
|
+
}
|
|
95
|
+
return valid;
|
|
96
|
+
},
|
|
97
|
+
async removeTab(targetTab = helper.activeTab) {
|
|
98
|
+
if (tabMap.value.size <= 1 || !targetTab || !canRemoveTab(targetTab) || !await helper.tryRemoveTabs([targetTab])) return;
|
|
99
|
+
const tabs = helper.getTabs();
|
|
100
|
+
const index = helper.indexOf(targetTab);
|
|
101
|
+
const isActive = targetTab === helper.activeTab;
|
|
102
|
+
const nextTab = isActive ? tabs[index - 1] || tabs[index + 1] : null;
|
|
103
|
+
nextTab && setActiveTab(nextTab);
|
|
104
|
+
removeTabs([targetTab]);
|
|
105
|
+
},
|
|
106
|
+
async removeOtherTabs(targetTab = helper.activeTab) {
|
|
107
|
+
if (!targetTab) return;
|
|
108
|
+
const otherTabs = helper.getTabs().filter(([tab]) => tab !== targetTab && canRemoveTab(tab));
|
|
109
|
+
if (!await helper.tryRemoveTabs(otherTabs)) return;
|
|
110
|
+
targetTab !== helper.activeTab && setActiveTab(targetTab);
|
|
111
|
+
removeTabs(otherTabs);
|
|
112
|
+
},
|
|
113
|
+
async removeSideTabs(side, targetTab = helper.activeTab) {
|
|
114
|
+
if (!targetTab) return;
|
|
115
|
+
const sideTabs = helper.getSideTabs(targetTab, side).filter(canRemoveTab);
|
|
116
|
+
if (!await helper.tryRemoveTabs(sideTabs)) return;
|
|
117
|
+
const activeInSide = targetTab !== helper.activeTab && sideTabs.some(([tab]) => tab === helper.activeTab);
|
|
118
|
+
activeInSide && setActiveTab(targetTab);
|
|
119
|
+
removeTabs(sideTabs);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return helper;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
export { createTabsHelper };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/utils/index.ts
|
|
2
|
+
const isArray = Array.isArray;
|
|
3
|
+
function ensureArray(val) {
|
|
4
|
+
return isArray(val) ? val : [val];
|
|
5
|
+
}
|
|
6
|
+
const assign = Object.assign;
|
|
7
|
+
function isObject(val) {
|
|
8
|
+
return val !== null && typeof val === "object";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { assign, ensureArray, isArray, isObject };
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vue-spark/app-helpers",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0-beta.1",
|
|
5
|
+
"description": "Lightweight Helpers for Vue 3 Application Development.",
|
|
6
|
+
"author": "leihaohao <https://github.com/l246804>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/vue-spark/app-helpers#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/vue-spark/app-helpers.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vue-spark/app-helpers/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"vue",
|
|
18
|
+
"app",
|
|
19
|
+
"helpers",
|
|
20
|
+
"permission",
|
|
21
|
+
"sso",
|
|
22
|
+
"tabs"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
"./*": "./dist/*/index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"vue": "^3.5.0",
|
|
37
|
+
"vue-router": "^4.5.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"vue-router": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@antfu/eslint-config": "^4.14.1",
|
|
46
|
+
"@tsconfig/node22": "^22.0.2",
|
|
47
|
+
"@types/node": "^22.15.17",
|
|
48
|
+
"@vue/tsconfig": "^0.7.0",
|
|
49
|
+
"bumpp": "^10.1.0",
|
|
50
|
+
"eslint": "^9.26.0",
|
|
51
|
+
"eslint-plugin-format": "^1.0.1",
|
|
52
|
+
"happy-dom": "^17.4.7",
|
|
53
|
+
"lint-staged": "^16.1.2",
|
|
54
|
+
"prettier": "^3.5.3",
|
|
55
|
+
"simple-git-hooks": "^2.13.0",
|
|
56
|
+
"tsdown": "^0.11.9",
|
|
57
|
+
"tsx": "^4.19.4",
|
|
58
|
+
"typescript": "^5.8.3",
|
|
59
|
+
"vitest": "^3.1.3",
|
|
60
|
+
"vue": "^3.5.16",
|
|
61
|
+
"vue-router": "^4.5.1"
|
|
62
|
+
},
|
|
63
|
+
"simple-git-hooks": {
|
|
64
|
+
"pre-commit": "npx lint-staged"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"dev": "tsdown --watch",
|
|
68
|
+
"build": "tsdown",
|
|
69
|
+
"test": "vitest",
|
|
70
|
+
"typecheck": "tsc --noEmit",
|
|
71
|
+
"format": "prettier . --write --cache --cache-location ./node_modules/.cache/.prettier-cache",
|
|
72
|
+
"lint": "eslint . --cache --cache-location ./node_modules/.cache/.eslint-cache",
|
|
73
|
+
"lint:fix": "pnpm run lint --fix",
|
|
74
|
+
"release": "bumpp && pnpm publish"
|
|
75
|
+
}
|
|
76
|
+
}
|