@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 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
+ }