hl-core 0.0.7-beta.1 → 0.0.7-beta.11

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 (42) hide show
  1. package/.prettierrc +2 -1
  2. package/api/index.ts +545 -0
  3. package/api/interceptors.ts +38 -0
  4. package/components/Button/Btn.vue +57 -0
  5. package/components/Button/BtnIcon.vue +47 -0
  6. package/components/Button/SortArrow.vue +21 -0
  7. package/components/Complex/Content.vue +5 -0
  8. package/components/Complex/ContentBlock.vue +5 -0
  9. package/components/Complex/Page.vue +43 -0
  10. package/components/Input/FormInput.vue +136 -0
  11. package/components/Input/RoundedInput.vue +136 -0
  12. package/components/Layout/Dialog.vue +84 -0
  13. package/components/Layout/Drawer.vue +42 -0
  14. package/components/Layout/Header.vue +48 -0
  15. package/components/Layout/Loader.vue +35 -0
  16. package/components/Layout/SettingsPanel.vue +33 -0
  17. package/components/Menu/MenuNav.vue +125 -0
  18. package/components/Menu/MenuNavItem.vue +27 -0
  19. package/components/Panel/PanelItem.vue +5 -0
  20. package/components/Transitions/FadeTransition.vue +5 -0
  21. package/composables/axios.ts +11 -0
  22. package/composables/classes.ts +1081 -0
  23. package/composables/constants.ts +61 -0
  24. package/composables/index.ts +133 -2
  25. package/composables/models.ts +43 -0
  26. package/composables/styles.ts +34 -8
  27. package/layouts/clear.vue +3 -0
  28. package/layouts/default.vue +37 -0
  29. package/layouts/full.vue +6 -0
  30. package/nuxt.config.ts +23 -4
  31. package/package.json +15 -4
  32. package/pages/500.vue +48 -0
  33. package/plugins/helperFunctionsPlugins.ts +11 -2
  34. package/plugins/storePlugin.ts +0 -2
  35. package/plugins/vuetifyPlugin.ts +10 -0
  36. package/store/data.store.js +2632 -6
  37. package/store/form.store.js +8 -0
  38. package/store/messages.ts +118 -27
  39. package/store/rules.js +7 -26
  40. package/tailwind.config.js +10 -0
  41. package/components/Button/GreenBtn.vue +0 -33
  42. package/store/app.store.js +0 -12
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <aside class="w-full lg:flex lg:w-3/4">
3
+ <slot></slot>
4
+ </aside>
5
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="w-full p-4" :class="[$libStyles.blueBgLight, $libStyles.rounded]">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <base-content class="flex-col" :class="[$libStyles.whiteBg]">
3
+ <base-header
4
+ class="justify-center lg:pl-14"
5
+ :has-back="hasBack"
6
+ :back-icon="backIcon"
7
+ :has-more="hasMore"
8
+ :more-icon="moreIcon"
9
+ :title="title"
10
+ @onBack="$emit('onBack')"
11
+ @onMore="$emit('onMore')"
12
+ ></base-header>
13
+ <slot></slot>
14
+ </base-content>
15
+ </template>
16
+
17
+ <script lang="ts">
18
+ export default defineComponent({
19
+ props: {
20
+ hasBack: {
21
+ type: Boolean,
22
+ default: false,
23
+ },
24
+ backIcon: {
25
+ type: String,
26
+ default: 'mdi-arrow-left',
27
+ },
28
+ hasMore: {
29
+ type: Boolean,
30
+ default: false,
31
+ },
32
+ moreIcon: {
33
+ type: String,
34
+ default: 'mdi-cog-outline',
35
+ },
36
+ title: {
37
+ type: String,
38
+ default: '',
39
+ },
40
+ },
41
+ emits: ['onBack', 'onMore'],
42
+ });
43
+ </script>
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <v-text-field
3
+ class="rounded-input"
4
+ v-model="fieldModel"
5
+ v-maska="maska"
6
+ :rules="rules"
7
+ :loading="loading"
8
+ :placeholder="placeholder"
9
+ :type="type"
10
+ :variant="variant"
11
+ :clear-icon="clearIcon"
12
+ :color="color"
13
+ :hint="hint"
14
+ :clearable="clearable"
15
+ :disabled="disabled"
16
+ :prepend-inner-icon="prependIcon ? prependIcon : ''"
17
+ :append-icon="appendIcon ? appendIcon : ''"
18
+ :bg-color="bgColor ? bgColor : ''"
19
+ @keyup.enter.prevent="submitted"
20
+ >
21
+ <template v-if="loading" #loader>
22
+ <v-progress-linear :active="loading" :color="color" absolute height="1" indeterminate></v-progress-linear>
23
+ </template>
24
+ </v-text-field>
25
+ </template>
26
+
27
+ <script lang="ts">
28
+ import { InputTypes } from '@/composables/models';
29
+
30
+ export default defineComponent({
31
+ name: 'BaseRoundedInput',
32
+ props: {
33
+ modelValue: {
34
+ type: String,
35
+ default: '',
36
+ },
37
+ loading: {
38
+ type: Boolean,
39
+ default: false,
40
+ },
41
+ clearable: {
42
+ type: Boolean,
43
+ default: true,
44
+ },
45
+ disabled: {
46
+ type: Boolean,
47
+ default: false,
48
+ },
49
+ placeholder: {
50
+ type: String,
51
+ default: 'Поле',
52
+ },
53
+ maska: {
54
+ type: String,
55
+ default: '',
56
+ },
57
+ hint: {
58
+ type: String,
59
+ default: '',
60
+ },
61
+ rules: {
62
+ type: Array<any>,
63
+ default: [],
64
+ },
65
+ type: {
66
+ type: String as PropType<InputTypes>,
67
+ default: 'text',
68
+ },
69
+ variant: {
70
+ type: String as PropType<'solo' | 'filled' | 'outlined' | 'plain' | 'underlined'>,
71
+ default: 'solo',
72
+ },
73
+ color: {
74
+ type: String,
75
+ default: '#009c73',
76
+ },
77
+ clearIcon: {
78
+ type: String,
79
+ default: 'mdi-close',
80
+ },
81
+ prependIcon: {
82
+ type: String,
83
+ },
84
+ appendIcon: {
85
+ type: String,
86
+ },
87
+ bgColor: {
88
+ type: String,
89
+ },
90
+ },
91
+ emits: ['update:modelValue', 'submitted'],
92
+
93
+ setup(props, { emit }) {
94
+ const fieldModel = ref(props.modelValue || '');
95
+
96
+ const updateValue = (event: any) => {
97
+ emit('update:modelValue', fieldModel.value);
98
+ };
99
+
100
+ const submitted = (event: any) => {
101
+ emit('submitted', event);
102
+ };
103
+
104
+ watch(
105
+ fieldModel,
106
+ () => {
107
+ updateValue(fieldModel.value);
108
+ },
109
+ { immediate: true },
110
+ );
111
+
112
+ return {
113
+ fieldModel,
114
+ submitted,
115
+ };
116
+ },
117
+ });
118
+ </script>
119
+
120
+ <style>
121
+ .rounded-input input:focus {
122
+ border: none !important;
123
+ outline: none !important;
124
+ }
125
+
126
+ /* .rounded-input .v-field {
127
+ border-radius: 8px;
128
+ border: 1px solid #dadada;
129
+ box-shadow: none;
130
+ font-size: 14px;
131
+ } */
132
+
133
+ .rounded-input .v-field--error {
134
+ border-color: #ff5449;
135
+ }
136
+ </style>
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <v-text-field
3
+ class="rounded-input"
4
+ v-model="fieldModel"
5
+ v-maska="maska"
6
+ :rules="rules"
7
+ :loading="loading"
8
+ :placeholder="placeholder"
9
+ :type="type"
10
+ :variant="variant"
11
+ :clear-icon="clearIcon"
12
+ :color="color"
13
+ :hint="hint"
14
+ :clearable="clearable"
15
+ :disabled="disabled"
16
+ :prepend-inner-icon="prependIcon ? prependIcon : ''"
17
+ :append-icon="appendIcon ? appendIcon : ''"
18
+ :bg-color="bgColor ? bgColor : ''"
19
+ @keyup.enter.prevent="submitted"
20
+ >
21
+ <template v-if="loading" #loader>
22
+ <v-progress-linear :active="loading" :color="color" absolute height="1" indeterminate></v-progress-linear>
23
+ </template>
24
+ </v-text-field>
25
+ </template>
26
+
27
+ <script lang="ts">
28
+ import { InputTypes } from '@/composables/models';
29
+
30
+ export default defineComponent({
31
+ name: 'BaseRoundedInput',
32
+ props: {
33
+ modelValue: {
34
+ type: String,
35
+ default: '',
36
+ },
37
+ loading: {
38
+ type: Boolean,
39
+ default: false,
40
+ },
41
+ clearable: {
42
+ type: Boolean,
43
+ default: true,
44
+ },
45
+ disabled: {
46
+ type: Boolean,
47
+ default: false,
48
+ },
49
+ placeholder: {
50
+ type: String,
51
+ default: 'Поле',
52
+ },
53
+ maska: {
54
+ type: String,
55
+ default: '',
56
+ },
57
+ hint: {
58
+ type: String,
59
+ default: '',
60
+ },
61
+ rules: {
62
+ type: Array<any>,
63
+ default: [],
64
+ },
65
+ type: {
66
+ type: String as PropType<InputTypes>,
67
+ default: 'text',
68
+ },
69
+ variant: {
70
+ type: String as PropType<'solo' | 'filled' | 'outlined' | 'plain' | 'underlined'>,
71
+ default: 'solo',
72
+ },
73
+ color: {
74
+ type: String,
75
+ default: '#009c73',
76
+ },
77
+ clearIcon: {
78
+ type: String,
79
+ default: 'mdi-close',
80
+ },
81
+ prependIcon: {
82
+ type: String,
83
+ },
84
+ appendIcon: {
85
+ type: String,
86
+ },
87
+ bgColor: {
88
+ type: String,
89
+ },
90
+ },
91
+ emits: ['update:modelValue', 'submitted'],
92
+
93
+ setup(props, { emit }) {
94
+ const fieldModel = ref(props.modelValue || '');
95
+
96
+ const updateValue = (event: any) => {
97
+ emit('update:modelValue', fieldModel.value);
98
+ };
99
+
100
+ const submitted = (event: any) => {
101
+ emit('submitted', event);
102
+ };
103
+
104
+ watch(
105
+ fieldModel,
106
+ () => {
107
+ updateValue(fieldModel.value);
108
+ },
109
+ { immediate: true },
110
+ );
111
+
112
+ return {
113
+ fieldModel,
114
+ submitted,
115
+ };
116
+ },
117
+ });
118
+ </script>
119
+
120
+ <style>
121
+ .rounded-input input:focus {
122
+ border: none !important;
123
+ outline: none !important;
124
+ }
125
+
126
+ .rounded-input .v-field {
127
+ border-radius: 8px;
128
+ border: 1px solid #dadada;
129
+ box-shadow: none;
130
+ font-size: 14px;
131
+ }
132
+
133
+ .rounded-input .v-field--error {
134
+ border-color: #ff5449;
135
+ }
136
+ </style>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <v-dialog v-model="fieldModel">
3
+ <v-card class="self-center w-full sm:w-3/4 md:w-2/3 lg:w-2/4 xl:w-[600px] rounded-lg !p-2">
4
+ <v-card-title>
5
+ <slot v-if="!title" name="title"></slot>
6
+ {{ title }}
7
+ </v-card-title>
8
+ <v-card-subtitle>
9
+ <slot v-if="!subtitle" name="subtitle"></slot>
10
+ {{ subtitle }}
11
+ </v-card-subtitle>
12
+ <v-card-text>
13
+ <slot v-if="text" name="content"></slot>
14
+ {{ text }}
15
+ </v-card-text>
16
+ <v-card-actions class="gap-[16px]">
17
+ <base-btn v-if="actions === 'default'" class="!w-fit px-6" size="sm" :text="$t('confirm.yes')" :btn="$libStyles.blueBtn" @click="$emit('yes')"></base-btn>
18
+ <base-btn v-if="actions === 'default'" class="!w-fit px-6" size="sm" :text="$t('confirm.no')" :btn="$libStyles.blueBtn" @click="$emit('no')" />
19
+ <slot v-if="actions !== 'default'" name="actions"></slot>
20
+ </v-card-actions>
21
+ </v-card>
22
+ </v-dialog>
23
+ </template>
24
+ <script lang="ts">
25
+ export default defineComponent({
26
+ name: 'BaseDialog',
27
+ props: {
28
+ modelValue: {
29
+ type: Boolean,
30
+ default: false,
31
+ },
32
+ title: {
33
+ type: String,
34
+ default: '',
35
+ },
36
+ subtitle: {
37
+ type: String,
38
+ default: '',
39
+ },
40
+ text: {
41
+ type: String,
42
+ default: '',
43
+ },
44
+ actions: {
45
+ type: String,
46
+ default: 'default',
47
+ },
48
+ },
49
+ emits: ['update:modelValue', 'submitted', 'yes', 'no'],
50
+ setup(props, { emit }) {
51
+ const fieldModel = ref(props.modelValue);
52
+
53
+ const updateValue = (event: boolean) => {
54
+ fieldModel.value = event;
55
+ };
56
+
57
+ const submitted = (event: any) => {
58
+ emit('submitted', event);
59
+ };
60
+
61
+ watch(
62
+ fieldModel,
63
+ () => {
64
+ emit('update:modelValue', fieldModel.value);
65
+ },
66
+ { immediate: true },
67
+ );
68
+
69
+ watch(
70
+ () => props.modelValue,
71
+ () => {
72
+ fieldModel.value = props.modelValue;
73
+ },
74
+ { immediate: true },
75
+ );
76
+
77
+ return {
78
+ fieldModel,
79
+ submitted,
80
+ updateValue,
81
+ };
82
+ },
83
+ });
84
+ </script>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <v-navigation-drawer
3
+ v-model="$dataStore[whichPanel].open"
4
+ :temporary="$dataStore[whichPanel].overlay"
5
+ location="right"
6
+ class="sm:!w-[400px] lg:!relative !right-0"
7
+ :class="[$dataStore[whichPanel].overlay ? 'lg:!hidden' : '', $dataStore[whichPanel].open ? '!w-full lg:!w-[420px]' : 'lg:!w-[0px]']"
8
+ >
9
+ <base-header :title="panelTitle" :has-back="true" back-icon="mdi-close" class="justify-center" @onBack="closePanel"></base-header>
10
+ <div class="flex flex-col" id="panel-actions">
11
+ <slot></slot>
12
+ </div>
13
+ </v-navigation-drawer>
14
+ </template>
15
+
16
+ <script lang="ts">
17
+ export default defineComponent({
18
+ name: 'BasePanel',
19
+ props: {
20
+ panelTitle: {
21
+ type: String,
22
+ default: '',
23
+ },
24
+ whichPanel: {
25
+ type: String as PropType<'settings' | 'panel'>,
26
+ default: 'panel',
27
+ },
28
+ },
29
+ setup(props) {
30
+ const dataStore = useDataStore();
31
+
32
+ const closePanel = () => {
33
+ dataStore[props.whichPanel].open = false;
34
+ dataStore.panelAction = null;
35
+ };
36
+
37
+ return {
38
+ closePanel,
39
+ };
40
+ },
41
+ });
42
+ </script>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <header class="relative w-full h-[70px] text-center font-medium align-middle flex items-center border-b-[1px]" :class="[$libStyles.blueBgLight, $libStyles.textSimple]">
3
+ <i v-if="hasBack" @click="$emit('onBack')" class="absolute left-5 mdi text-xl cursor-pointer" :class="[backIcon]"></i>
4
+ {{ title }}
5
+ <i v-if="hasMore" @click="$emit('onMore')" class="mdi absolute right-5 text-xl cursor-pointer" :class="[moreIcon, hideMoreOnLg ? 'lg:!hidden' : '']"> </i>
6
+ </header>
7
+ </template>
8
+
9
+ <script lang="ts">
10
+ export default defineComponent({
11
+ props: {
12
+ hasBack: {
13
+ type: Boolean,
14
+ default: false,
15
+ },
16
+ hasMore: {
17
+ type: Boolean,
18
+ default: false,
19
+ },
20
+ hideMoreOnLg: {
21
+ type: Boolean,
22
+ default: false,
23
+ },
24
+ backIcon: {
25
+ type: String,
26
+ default: 'mdi-arrow-left',
27
+ },
28
+ moreIcon: {
29
+ type: String,
30
+ default: 'mdi-cog-outline',
31
+ },
32
+ title: {
33
+ type: String,
34
+ default: '',
35
+ },
36
+ },
37
+ emits: ['onBack', 'onMore'],
38
+ setup() {
39
+ const dataStore = useDataStore();
40
+
41
+ const onClickOutside = () => {
42
+ dataStore.settings.open = false;
43
+ };
44
+
45
+ return { onClickOutside };
46
+ },
47
+ });
48
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <v-progress-circular :size="size" :width="width" :indeterminate="indeterminate" :color="color" :bg-color="bgColor"></v-progress-circular>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ export default defineComponent({
7
+ name: 'BaseLoader',
8
+ props: {
9
+ size: {
10
+ type: Number,
11
+ default: 40,
12
+ },
13
+ width: {
14
+ type: Number,
15
+ default: 4,
16
+ },
17
+ indeterminate: {
18
+ type: Boolean,
19
+ default: true,
20
+ },
21
+ overlay: {
22
+ type: Boolean,
23
+ default: true,
24
+ },
25
+ color: {
26
+ type: String,
27
+ default: '#FAB31C',
28
+ },
29
+ bgColor: {
30
+ type: String,
31
+ default: '#009C73',
32
+ },
33
+ },
34
+ });
35
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <base-drawer :panel-title="$dataStore.menu.title" which-panel="settings">
3
+ <base-panel-item>
4
+ <v-btn size="x-small" icon="mdi-minus" color="#A0B3D8" class="text-white" variant="flat"></v-btn>
5
+ Шрифт
6
+ <v-btn size="x-small" icon="mdi-plus" color="#A0B3D8" class="text-white" variant="flat"></v-btn>
7
+ </base-panel-item>
8
+ <base-panel-item>
9
+ <v-text-field v-model="$dataStore.user.fullName" :readonly="true" hide-details variant="plain" :label="$dataStore.user.roles?.join(', ')"></v-text-field>
10
+ <i class="mdi mdi-account-outline text-2xl text-[#A0B3D8]"></i
11
+ ></base-panel-item>
12
+ <base-panel-item v-for="panelItem of dataStore.settings.items" :key="panelItem.title!" class="cursor-pointer" @click="panelItem.action ? panelItem.action() : null">
13
+ {{ panelItem.title }}
14
+ <i v-if="panelItem.icon" class="mdi text-xl text-[#A0B3D8]" :class="[panelItem.icon]"></i>
15
+ </base-panel-item>
16
+ <base-panel-item @click="dialog = true" class="cursor-pointer">
17
+ Выход
18
+ <i class="mdi mdi-logout text-xl text-[#A0B3D8]"></i>
19
+ </base-panel-item>
20
+
21
+ <base-dialog v-model="dialog" :title="$t('dialog.exit')" :subtitle="$t('dialog.dataWillClear')" actions="default" @yes="logoutUser" @no="dialog = false"> </base-dialog
22
+ ></base-drawer>
23
+ </template>
24
+
25
+ <script lang="ts" setup>
26
+ const dialog = ref(false);
27
+ const dataStore = useDataStore();
28
+
29
+ const logoutUser = async () => {
30
+ dialog.value = false;
31
+ await dataStore.logoutUser();
32
+ };
33
+ </script>
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <aside class="w-full lg:w-1/4 bg-white flex flex-col border-r-2 relative">
3
+ <base-header
4
+ class="justify-center"
5
+ :title="title"
6
+ :has-back="hasBack"
7
+ :back-icon="backIcon"
8
+ :has-more="hasMore"
9
+ :hide-more-on-lg="hideMoreOnLg"
10
+ :more-icon="moreIcon"
11
+ @onBack="$emit('onBack')"
12
+ @onMore="$emit('onMore')"
13
+ ></base-header>
14
+ <slot key="slot-content" name="content"></slot>
15
+ <section key="main" class="px-2 pt-[14px] flex flex-col gap-[10px]">
16
+ <slot name="start"></slot>
17
+ <base-fade-transition>
18
+ <div v-if="$dataStore.menuItems && $dataStore.menuItems.length" class="flex flex-col gap-[10px]">
19
+ <div v-for="(item, index) of $dataStore.menuItems" :key="index">
20
+ <base-menu-nav-item
21
+ :menu-item="item"
22
+ :selected="!!selected.title && !!item.title && selected.title === item.title"
23
+ @click.left="pickItem(item)"
24
+ @click.middle="openTab(item)"
25
+ >
26
+ </base-menu-nav-item>
27
+ <hr v-if="item.hasLine" class="mt-[10px]" />
28
+ </div>
29
+ </div>
30
+ </base-fade-transition>
31
+ <slot name="end"></slot>
32
+ <slot name="actions"></slot>
33
+ <base-fade-transition>
34
+ <div
35
+ v-if="$dataStore.buttons && $dataStore.buttons.length"
36
+ class="flex flex-col gap-[10px] justify-self-end absolute bottom-5 lg:bottom-[30%] w-full pr-4"
37
+ >
38
+ <div v-for="(item, index) of $dataStore.buttons" :key="index">
39
+ <transition
40
+ enter-active-class="animate__animated animate__fadeIn animate__faster"
41
+ leave-active-class="animate__animated animate__fadeOut animate__faster"
42
+ >
43
+ <base-btn
44
+ v-if="
45
+ 'show' in item && typeof item.show === 'boolean'
46
+ ? item.show
47
+ : true
48
+ "
49
+ :text="item.title!"
50
+ :btn="item.color"
51
+ :disabled="!!item.disabled"
52
+ @click="item.action"
53
+ >
54
+ </base-btn>
55
+ </transition>
56
+ </div></div
57
+ ></base-fade-transition>
58
+ </section>
59
+ </aside>
60
+ </template>
61
+
62
+ <script lang="ts">
63
+ import { MenuItem } from '@/composables/classes';
64
+ import { RouteLocationNormalized } from 'vue-router';
65
+
66
+ export default defineComponent({
67
+ props: {
68
+ title: {
69
+ type: String,
70
+ default: 'Заголовок',
71
+ },
72
+ selected: {
73
+ type: Object as PropType<MenuItem>,
74
+ default: new MenuItem(),
75
+ },
76
+ hasBack: {
77
+ type: Boolean,
78
+ default: false,
79
+ },
80
+ backIcon: {
81
+ type: String,
82
+ default: 'mdi-arrow-left',
83
+ },
84
+ hasMore: {
85
+ type: Boolean,
86
+ default: false,
87
+ },
88
+ hideMoreOnLg: {
89
+ type: Boolean,
90
+ default: false,
91
+ },
92
+ moreIcon: {
93
+ type: String,
94
+ default: 'mdi-cog-outline',
95
+ },
96
+ },
97
+ emits: ['update:modelValue', 'onLink', 'onBack', 'onMore', 'clicked'],
98
+ setup(props, { emit }) {
99
+ const dataStore = useDataStore();
100
+ const router = useRouter();
101
+
102
+ const pickItem = (item: MenuItem) => {
103
+ if (item.title !== dataStore.menu.selectedItem.title) {
104
+ emit('update:modelValue', item);
105
+ if (typeof item.link === 'object') {
106
+ if (item.link && 'name' in item.link) {
107
+ router.push(item.link as RouteLocationNormalized);
108
+ } else {
109
+ dataStore.showToaster('warning', 'Отсутствует ссылка для перехода');
110
+ }
111
+ } else {
112
+ item.link ? emit('onLink', item) : emit('clicked', item);
113
+ }
114
+ }
115
+ };
116
+ const openTab = (item: MenuItem) => {
117
+ if (item.url) {
118
+ window.open(item.url, '_blank', 'noreferrer');
119
+ }
120
+ };
121
+
122
+ return { pickItem, openTab };
123
+ },
124
+ });
125
+ </script>