@melt-js/libs-vben 0.0.0-stage → 2026.11.8-2

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 (174) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +86 -2
  3. package/package.json +98 -3
  4. package/src/access/access-control.vue +47 -0
  5. package/src/access/accessible.ts +236 -0
  6. package/src/access/directive.ts +42 -0
  7. package/src/access/index.ts +4 -0
  8. package/src/access/use-access.ts +53 -0
  9. package/src/composables/index.ts +14 -0
  10. package/src/composables/use-is-mobile.ts +7 -0
  11. package/src/composables/use-layout-style.ts +51 -0
  12. package/src/composables/use-layout-viewport-height.ts +71 -0
  13. package/src/composables/use-namespace.ts +106 -0
  14. package/src/composables/use-priority-value.ts +96 -0
  15. package/src/composables/use-scroll-lock.ts +151 -0
  16. package/src/composables/use-simple-locale/README.md +3 -0
  17. package/src/composables/use-simple-locale/index.ts +27 -0
  18. package/src/composables/use-simple-locale/messages.ts +31 -0
  19. package/src/composables/use-sortable.ts +32 -0
  20. package/src/constants/core.ts +76 -0
  21. package/src/constants/index.ts +2 -0
  22. package/src/core-icons/create-icon.ts +14 -0
  23. package/src/core-icons/index.ts +11 -0
  24. package/src/core-icons/lucide.ts +95 -0
  25. package/src/core-preferences/config.ts +172 -0
  26. package/src/core-preferences/constants.ts +116 -0
  27. package/src/core-preferences/index.ts +24 -0
  28. package/src/core-preferences/preferences.ts +503 -0
  29. package/src/core-preferences/types.ts +471 -0
  30. package/src/core-preferences/update-css-variables.ts +129 -0
  31. package/src/core-preferences/use-preferences.ts +282 -0
  32. package/src/design/css/global.css +1 -0
  33. package/src/design/css/nprogress.css +61 -0
  34. package/src/design/css/transition.css +238 -0
  35. package/src/design/css/ui.css +101 -0
  36. package/src/design/design-tokens/dark.css +446 -0
  37. package/src/design/design-tokens/default.css +491 -0
  38. package/src/design/design-tokens/index.ts +2 -0
  39. package/src/design/index.ts +6 -0
  40. package/src/design/scss-bem/bem.scss +34 -0
  41. package/src/design/scss-bem/constants.scss +5 -0
  42. package/src/hooks/index.ts +9 -0
  43. package/src/hooks/use-app-config.ts +39 -0
  44. package/src/hooks/use-content-maximize.ts +24 -0
  45. package/src/hooks/use-design-tokens.ts +421 -0
  46. package/src/hooks/use-hover-toggle.ts +163 -0
  47. package/src/hooks/use-pagination.ts +72 -0
  48. package/src/hooks/use-refresh.ts +16 -0
  49. package/src/hooks/use-tabs.ts +143 -0
  50. package/src/hooks/use-watermark.ts +84 -0
  51. package/src/icons/iconify/index.ts +5 -0
  52. package/src/icons/icons/empty-icon.vue +27 -0
  53. package/src/icons/index.ts +3 -0
  54. package/src/icons/svg/icons/antdv-logo.svg +29 -0
  55. package/src/icons/svg/icons/antdv-next-logo.svg +1 -0
  56. package/src/icons/svg/icons/avatar-1.svg +1 -0
  57. package/src/icons/svg/icons/avatar-2.svg +1 -0
  58. package/src/icons/svg/icons/avatar-3.svg +1 -0
  59. package/src/icons/svg/icons/avatar-4.svg +1 -0
  60. package/src/icons/svg/icons/bell.svg +1 -0
  61. package/src/icons/svg/icons/cake.svg +1 -0
  62. package/src/icons/svg/icons/card.svg +1 -0
  63. package/src/icons/svg/icons/dingding.svg +1 -0
  64. package/src/icons/svg/icons/download.svg +1 -0
  65. package/src/icons/svg/icons/github.svg +1 -0
  66. package/src/icons/svg/icons/google.svg +1 -0
  67. package/src/icons/svg/icons/qqchat.svg +1 -0
  68. package/src/icons/svg/icons/tdesign-logo.svg +39 -0
  69. package/src/icons/svg/icons/wechat.svg +1 -0
  70. package/src/icons/svg/index.ts +39 -0
  71. package/src/icons/svg/load.ts +81 -0
  72. package/src/locales/i18n.ts +147 -0
  73. package/src/locales/index.ts +30 -0
  74. package/src/locales/langs/en-US/authentication.json +65 -0
  75. package/src/locales/langs/en-US/common.json +27 -0
  76. package/src/locales/langs/en-US/preferences.json +245 -0
  77. package/src/locales/langs/en-US/profile.json +4 -0
  78. package/src/locales/langs/en-US/ui.json +165 -0
  79. package/src/locales/langs/zh-CN/authentication.json +65 -0
  80. package/src/locales/langs/zh-CN/common.json +27 -0
  81. package/src/locales/langs/zh-CN/preferences.json +245 -0
  82. package/src/locales/langs/zh-CN/profile.json +4 -0
  83. package/src/locales/langs/zh-CN/ui.json +165 -0
  84. package/src/locales/langs/zh-TW/authentication.json +65 -0
  85. package/src/locales/langs/zh-TW/common.json +27 -0
  86. package/src/locales/langs/zh-TW/preferences.json +245 -0
  87. package/src/locales/langs/zh-TW/profile.json +4 -0
  88. package/src/locales/langs/zh-TW/ui.json +165 -0
  89. package/src/locales/typing.ts +27 -0
  90. package/src/preferences/index.ts +42 -0
  91. package/src/request/index.ts +2 -0
  92. package/src/request/request-client/index.ts +3 -0
  93. package/src/request/request-client/modules/downloader.ts +60 -0
  94. package/src/request/request-client/modules/interceptor.ts +40 -0
  95. package/src/request/request-client/modules/sse.ts +134 -0
  96. package/src/request/request-client/modules/uploader.ts +79 -0
  97. package/src/request/request-client/preset-interceptors.ts +178 -0
  98. package/src/request/request-client/request-client.ts +163 -0
  99. package/src/request/request-client/types.ts +90 -0
  100. package/src/shared/cache/README.md +418 -0
  101. package/src/shared/cache/index.ts +5 -0
  102. package/src/shared/cache/indexeddb-driver.ts +137 -0
  103. package/src/shared/cache/local-storage-driver.ts +70 -0
  104. package/src/shared/cache/memory-storage-driver.ts +32 -0
  105. package/src/shared/cache/storage-manager.ts +146 -0
  106. package/src/shared/cache/types.ts +39 -0
  107. package/src/shared/color/color.ts +9 -0
  108. package/src/shared/color/convert.ts +62 -0
  109. package/src/shared/color/generator.ts +45 -0
  110. package/src/shared/color/index.ts +3 -0
  111. package/src/shared/constants/globals.ts +20 -0
  112. package/src/shared/constants/index.ts +2 -0
  113. package/src/shared/constants/vben.ts +30 -0
  114. package/src/shared/global-state.ts +45 -0
  115. package/src/shared/store.ts +1 -0
  116. package/src/shared/utils/cn.ts +10 -0
  117. package/src/shared/utils/date.ts +80 -0
  118. package/src/shared/utils/diff.ts +114 -0
  119. package/src/shared/utils/dom.ts +126 -0
  120. package/src/shared/utils/download.ts +186 -0
  121. package/src/shared/utils/error.ts +19 -0
  122. package/src/shared/utils/index.ts +22 -0
  123. package/src/shared/utils/inference.ts +164 -0
  124. package/src/shared/utils/letter.ts +47 -0
  125. package/src/shared/utils/merge.ts +11 -0
  126. package/src/shared/utils/nprogress.ts +52 -0
  127. package/src/shared/utils/resources.ts +21 -0
  128. package/src/shared/utils/stack.ts +103 -0
  129. package/src/shared/utils/state-handler.ts +61 -0
  130. package/src/shared/utils/to.ts +21 -0
  131. package/src/shared/utils/tree.ts +159 -0
  132. package/src/shared/utils/unique.ts +15 -0
  133. package/src/shared/utils/update-css-variables.ts +47 -0
  134. package/src/shared/utils/util.ts +50 -0
  135. package/src/shared/utils/window.ts +37 -0
  136. package/src/stores/index.ts +3 -0
  137. package/src/stores/modules/access.ts +130 -0
  138. package/src/stores/modules/dict.ts +244 -0
  139. package/src/stores/modules/index.ts +5 -0
  140. package/src/stores/modules/tabbar.ts +797 -0
  141. package/src/stores/modules/timezone.ts +132 -0
  142. package/src/stores/modules/user.ts +42 -0
  143. package/src/stores/setup.ts +79 -0
  144. package/src/stores/shim-pinia.d.ts +11 -0
  145. package/src/styles/antd/index.css +78 -0
  146. package/src/styles/antdv-next/index.css +90 -0
  147. package/src/styles/ele/index.css +44 -0
  148. package/src/styles/global/index.scss +1 -0
  149. package/src/styles/index.ts +1 -0
  150. package/src/styles/naive/index.css +26 -0
  151. package/src/styles/style-exports.d.ts +3 -0
  152. package/src/theme.css +592 -0
  153. package/src/types/global.d.ts +28 -0
  154. package/src/types/index.ts +2 -0
  155. package/src/types/shims.d.ts +1 -0
  156. package/src/types/user.ts +20 -0
  157. package/src/typings/app.d.ts +128 -0
  158. package/src/typings/basic.d.ts +42 -0
  159. package/src/typings/helper.d.ts +147 -0
  160. package/src/typings/index.ts +16 -0
  161. package/src/typings/languages.ts +28 -0
  162. package/src/typings/menu-record.ts +82 -0
  163. package/src/typings/tabs.ts +8 -0
  164. package/src/typings/vue-router.d.ts +169 -0
  165. package/src/utils/helpers/find-menu-by-path.ts +37 -0
  166. package/src/utils/helpers/generate-menus.ts +89 -0
  167. package/src/utils/helpers/generate-routes-backend.ts +108 -0
  168. package/src/utils/helpers/generate-routes-frontend.ts +58 -0
  169. package/src/utils/helpers/get-popup-container.ts +10 -0
  170. package/src/utils/helpers/index.ts +8 -0
  171. package/src/utils/helpers/merge-route-modules.ts +28 -0
  172. package/src/utils/helpers/reset-routes.ts +30 -0
  173. package/src/utils/helpers/unmount-global-loading.ts +31 -0
  174. package/src/utils/index.ts +4 -0
@@ -0,0 +1,797 @@
1
+ import type { ComputedRef, VNode } from 'vue';
2
+ import type {
3
+ RouteLocationNormalized,
4
+ RouteLocationNormalizedLoaded,
5
+ RouteLocationNormalizedLoadedGeneric,
6
+ Router,
7
+ RouteRecordNormalized,
8
+ } from 'vue-router';
9
+
10
+ import type { TabDefinition } from '@melt-js/libs-vben/typings';
11
+
12
+ import { markRaw, nextTick, toRaw } from 'vue';
13
+
14
+ import { preferences } from '@melt-js/libs-vben/core-preferences';
15
+ import {
16
+ createStack,
17
+ openWindow,
18
+ Stack,
19
+ startProgress,
20
+ stopProgress,
21
+ } from '@melt-js/libs-vben/shared/utils';
22
+
23
+ import { acceptHMRUpdate, defineStore } from 'pinia';
24
+
25
+ interface RouteCached {
26
+ component: VNode;
27
+ key: string;
28
+ route: RouteLocationNormalizedLoadedGeneric;
29
+ }
30
+
31
+ interface TabbarState {
32
+ cachedRoutes: Map<string, RouteCached>;
33
+ /**
34
+ * @zh_CN 当前打开的标签页列表缓存
35
+ */
36
+ cachedTabs: Set<string>;
37
+ /**
38
+ * @zh_CN 拖拽结束的索引
39
+ */
40
+ dragEndIndex: number;
41
+ /**
42
+ * @zh_CN 需要排除缓存的标签页
43
+ */
44
+ excludeCachedTabs: Set<string>;
45
+ /**
46
+ * @zh_CN 标签右键菜单列表
47
+ */
48
+ menuList: string[];
49
+ /**
50
+ * @zh_CN 是否刷新
51
+ */
52
+ renderRouteView?: boolean;
53
+ /**
54
+ * @zh_CN 当前打开的标签页列表
55
+ */
56
+ tabs: TabDefinition[];
57
+ /**
58
+ * @zh_CN 更新时间,用于一些更新场景,使用watch深度监听的话,会损耗性能
59
+ */
60
+ updateTime?: number;
61
+ /**
62
+ * @zh_CN 上一个标签页打开的标签
63
+ */
64
+ visitHistory: Stack<string>;
65
+ }
66
+
67
+ /**
68
+ * @zh_CN 访问历史记录最大数量
69
+ */
70
+ const MAX_VISIT_HISTORY = 50;
71
+
72
+ /**
73
+ * @zh_CN 访问权限相关
74
+ */
75
+ export const useTabbarStore = defineStore('core-tabbar', {
76
+ actions: {
77
+ /**
78
+ * Close tabs in bulk
79
+ */
80
+ async _bulkCloseByKeys(keys: string[]) {
81
+ const keySet = new Set(keys);
82
+ this.tabs = this.tabs.filter(
83
+ (item) => !keySet.has(getTabKeyFromTab(item)),
84
+ );
85
+ if (isVisitHistory()) {
86
+ this.visitHistory.remove(...keys);
87
+ }
88
+
89
+ await this.updateCacheTabs();
90
+ },
91
+ /**
92
+ * @zh_CN 关闭标签页
93
+ * @param tab
94
+ */
95
+ _close(tab: TabDefinition) {
96
+ if (isAffixTab(tab)) {
97
+ return;
98
+ }
99
+ const index = this.tabs.findIndex((item) => equalTab(item, tab));
100
+ index !== -1 && this.tabs.splice(index, 1);
101
+ },
102
+ /**
103
+ * @zh_CN 跳转到默认标签页
104
+ */
105
+ async _goToDefaultTab(router: Router) {
106
+ if (this.getTabs.length <= 0) {
107
+ return;
108
+ }
109
+ const firstTab = this.getTabs[0];
110
+ if (firstTab) {
111
+ await this._goToTab(firstTab, router);
112
+ }
113
+ },
114
+ /**
115
+ * @zh_CN 跳转到标签页
116
+ * @param tab
117
+ * @param router
118
+ */
119
+ async _goToTab(tab: TabDefinition, router: Router) {
120
+ const { params, path, query } = tab;
121
+ const toParams = {
122
+ params: params || {},
123
+ path,
124
+ query: query || {},
125
+ };
126
+ await router.replace(toParams);
127
+ },
128
+ /**
129
+ * @zh_CN 添加标签页
130
+ * @param routeTab
131
+ */
132
+ addTab(routeTab: TabDefinition): TabDefinition {
133
+ let tab = cloneTab(routeTab);
134
+ if (!tab.key) {
135
+ tab.key = getTabKey(routeTab);
136
+ }
137
+ if (!isTabShown(tab)) {
138
+ return tab;
139
+ }
140
+
141
+ const tabIndex = this.tabs.findIndex((item) => {
142
+ return equalTab(item, tab);
143
+ });
144
+
145
+ if (tabIndex === -1) {
146
+ const maxCount = preferences.tabbar.maxCount;
147
+ // 获取动态路由打开数,超过 0 即代表需要控制打开数
148
+ const maxNumOfOpenTab = routeTab?.meta?.maxNumOfOpenTab ?? -1;
149
+ // 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了
150
+ // 获取到已经打开的动态路由数, 判断是否大于某一个值
151
+ if (
152
+ maxNumOfOpenTab > 0 &&
153
+ this.tabs.filter((tab) => tab.name === routeTab.name).length >=
154
+ maxNumOfOpenTab
155
+ ) {
156
+ // 关闭第一个
157
+ const index = this.tabs.findIndex(
158
+ (item) => item.name === routeTab.name,
159
+ );
160
+ index !== -1 && this.tabs.splice(index, 1);
161
+ } else if (maxCount > 0 && this.tabs.length >= maxCount) {
162
+ // 关闭第一个
163
+ const index = this.tabs.findIndex(
164
+ (item) =>
165
+ !Reflect.has(item.meta, 'affixTab') || !item.meta.affixTab,
166
+ );
167
+ index !== -1 && this.tabs.splice(index, 1);
168
+ }
169
+ this.tabs.push(tab);
170
+ } else {
171
+ // 页面已经存在,不重复添加选项卡,只更新选项卡参数
172
+ const currentTab = toRaw(this.tabs)[tabIndex];
173
+ const mergedTab = {
174
+ ...currentTab,
175
+ ...tab,
176
+ meta: { ...currentTab?.meta, ...tab.meta },
177
+ };
178
+ if (currentTab) {
179
+ const curMeta = currentTab.meta;
180
+ if (Reflect.has(curMeta, 'affixTab')) {
181
+ mergedTab.meta.affixTab = curMeta.affixTab;
182
+ }
183
+ if (Reflect.has(curMeta, 'newTabTitle')) {
184
+ mergedTab.meta.newTabTitle = curMeta.newTabTitle;
185
+ }
186
+ }
187
+ tab = mergedTab;
188
+ this.tabs.splice(tabIndex, 1, mergedTab);
189
+ }
190
+ void this.updateCacheTabs();
191
+ // 添加访问历史记录
192
+ if (isVisitHistory()) {
193
+ this.visitHistory.push(tab.key as string);
194
+ }
195
+ return tab;
196
+ },
197
+ /**
198
+ * @zh_CN 关闭所有标签页
199
+ */
200
+ async closeAllTabs(router: Router) {
201
+ const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
202
+ this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
203
+ // 设置访问历史记录
204
+ if (isVisitHistory()) {
205
+ this.visitHistory.retain(
206
+ this.tabs.map((item) => getTabKeyFromTab(item)),
207
+ );
208
+ }
209
+ await this._goToDefaultTab(router);
210
+ await this.updateCacheTabs();
211
+ },
212
+ /**
213
+ * @zh_CN 关闭左侧标签页
214
+ * @param tab
215
+ */
216
+ async closeLeftTabs(tab: TabDefinition) {
217
+ const index = this.tabs.findIndex((item) => equalTab(item, tab));
218
+
219
+ if (index < 1) {
220
+ return;
221
+ }
222
+
223
+ const leftTabs = this.tabs.slice(0, index);
224
+ const keys: string[] = [];
225
+
226
+ for (const item of leftTabs) {
227
+ if (!isAffixTab(item)) {
228
+ keys.push(item.key as string);
229
+ }
230
+ }
231
+ await this._bulkCloseByKeys(keys);
232
+ },
233
+ /**
234
+ * @zh_CN 关闭其他标签页
235
+ * @param tab
236
+ */
237
+ async closeOtherTabs(tab: TabDefinition) {
238
+ const closeKeys = this.tabs.map((item) => getTabKeyFromTab(item));
239
+
240
+ const keys: string[] = [];
241
+
242
+ for (const key of closeKeys) {
243
+ if (key !== getTabKeyFromTab(tab)) {
244
+ const closeTab = this.tabs.find(
245
+ (item) => getTabKeyFromTab(item) === key,
246
+ );
247
+ if (!closeTab) {
248
+ continue;
249
+ }
250
+ if (!isAffixTab(closeTab)) {
251
+ keys.push(closeTab.key as string);
252
+ }
253
+ }
254
+ }
255
+ await this._bulkCloseByKeys(keys);
256
+ },
257
+ /**
258
+ * @zh_CN 关闭右侧标签页
259
+ * @param tab
260
+ */
261
+ async closeRightTabs(tab: TabDefinition) {
262
+ const index = this.tabs.findIndex((item) => equalTab(item, tab));
263
+
264
+ if (index !== -1 && index < this.tabs.length - 1) {
265
+ const rightTabs = this.tabs.slice(index + 1);
266
+
267
+ const keys: string[] = [];
268
+ for (const item of rightTabs) {
269
+ if (!isAffixTab(item)) {
270
+ keys.push(item.key as string);
271
+ }
272
+ }
273
+ await this._bulkCloseByKeys(keys);
274
+ }
275
+ },
276
+
277
+ /**
278
+ * @zh_CN 关闭标签页
279
+ * @param tab
280
+ * @param router
281
+ */
282
+ async closeTab(tab: TabDefinition, router: Router) {
283
+ const { currentRoute } = router;
284
+ const currentTabKey = getTabKey(currentRoute.value);
285
+ // 关闭不是激活选项卡
286
+ if (currentTabKey !== getTabKeyFromTab(tab)) {
287
+ this._close(tab);
288
+ await this.updateCacheTabs();
289
+ // 移除访问历史记录
290
+ if (isVisitHistory()) {
291
+ this.visitHistory.remove(getTabKeyFromTab(tab));
292
+ }
293
+ return;
294
+ }
295
+ if (this.getTabs.length <= 1) {
296
+ console.error('Failed to close the tab; only one tab remains open.');
297
+ return;
298
+ }
299
+ // 从访问历史记录中移除当前关闭的tab
300
+ if (isVisitHistory()) {
301
+ this.visitHistory.remove(currentTabKey);
302
+ this._close(tab);
303
+
304
+ let previousTab: TabDefinition | undefined;
305
+ let previousTabKey: string | undefined;
306
+ while (true) {
307
+ previousTabKey = this.visitHistory.pop();
308
+ if (!previousTabKey) {
309
+ break;
310
+ }
311
+ previousTab = this.getTabByKey(previousTabKey);
312
+ if (previousTab) {
313
+ break;
314
+ }
315
+ }
316
+ await (previousTab
317
+ ? this._goToTab(previousTab, router)
318
+ : this._goToDefaultTab(router));
319
+ return;
320
+ }
321
+ // 未开启访问历史记录,直接跳转下一个或上一个tab
322
+ const index = this.getTabs.findIndex(
323
+ (item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value),
324
+ );
325
+
326
+ const before = this.getTabs[index - 1];
327
+ const after = this.getTabs[index + 1];
328
+
329
+ // 下一个tab存在,跳转到下一个
330
+ if (after) {
331
+ this._close(tab);
332
+ await this._goToTab(after, router);
333
+ // 上一个tab存在,跳转到上一个
334
+ } else if (before) {
335
+ this._close(tab);
336
+ await this._goToTab(before, router);
337
+ }
338
+ },
339
+
340
+ /**
341
+ * @zh_CN 通过key关闭标签页
342
+ * @param key
343
+ * @param router
344
+ */
345
+ async closeTabByKey(key: string, router: Router) {
346
+ // 标签页的 key 在创建时已经解码过,优先按原样匹配;重复解码会改变
347
+ // 已解码 key 中的 "%xx" 片段,匹配到别的标签页或抛 URIError
348
+ let index = this.tabs.findIndex((item) => getTabKeyFromTab(item) === key);
349
+ if (index === -1) {
350
+ let decodedKey: string;
351
+ try {
352
+ decodedKey = decodeURIComponent(key);
353
+ } catch {
354
+ return;
355
+ }
356
+ index = this.tabs.findIndex(
357
+ (item) => getTabKeyFromTab(item) === decodedKey,
358
+ );
359
+ }
360
+ if (index === -1) {
361
+ return;
362
+ }
363
+
364
+ const tab = this.tabs[index];
365
+ if (tab) {
366
+ await this.closeTab(tab, router);
367
+ }
368
+ },
369
+
370
+ /**
371
+ * 根据tab的key获取tab
372
+ * @param key
373
+ */
374
+ getTabByKey(key: string) {
375
+ return this.getTabs.find(
376
+ (item) => getTabKeyFromTab(item) === key,
377
+ ) as TabDefinition;
378
+ },
379
+ /**
380
+ * @zh_CN 新窗口打开标签页
381
+ * @param tab
382
+ */
383
+ async openTabInNewWindow(tab: TabDefinition, router: Router) {
384
+ const href = router.resolve(tab.fullPath || tab.path).href;
385
+ openWindow(new URL(href, location.href).href, { target: '_blank' });
386
+ },
387
+
388
+ /**
389
+ * @zh_CN 固定标签页
390
+ * @param tab
391
+ */
392
+ async pinTab(tab: TabDefinition) {
393
+ const index = this.tabs.findIndex((item) => equalTab(item, tab));
394
+ if (index === -1) {
395
+ return;
396
+ }
397
+ const oldTab = this.tabs[index];
398
+ tab.meta.affixTab = true;
399
+ tab.meta.title = oldTab?.meta?.title as string;
400
+ // this.addTab(tab);
401
+ this.tabs.splice(index, 1, tab);
402
+ // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
403
+ const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
404
+ // 获得固定tabs的index
405
+ const newIndex = affixTabs.findIndex((item) => equalTab(item, tab));
406
+ // sortTabs 的源下标同样要按渲染顺序取,否则固定标签在原始数组靠后时会动到别的标签页
407
+ const renderedIndex = this.getTabs.findIndex((item) =>
408
+ equalTab(item, tab),
409
+ );
410
+ // 交换位置重新排序
411
+ await this.sortTabs(renderedIndex, newIndex);
412
+ },
413
+
414
+ /**
415
+ * 刷新标签页
416
+ */
417
+ async refresh(router: Router | string) {
418
+ // 如果是Router路由,那么就根据当前路由刷新
419
+ // 如果是string字符串,为路由名称,则定向刷新指定标签页,不能是当前路由名称,否则不会刷新
420
+ if (typeof router === 'string') {
421
+ return await this.refreshByName(router);
422
+ }
423
+
424
+ const { currentRoute } = router;
425
+ const { name } = currentRoute.value;
426
+
427
+ this.excludeCachedTabs.add(name as string);
428
+ this.renderRouteView = false;
429
+ // startProgress 内部是动态导入 nprogress,导入失败不应中断刷新流程:
430
+ // 否则 renderRouteView 停留 false(内容区空白)、标签页永久滞留在
431
+ // excludeCachedTabs 里。与各应用 guard.ts 一致 fire-and-forget。
432
+ void startProgress();
433
+
434
+ await nextTick();
435
+ // await new Promise((resolve) => setTimeout(resolve, 200));
436
+
437
+ this.excludeCachedTabs.delete(name as string);
438
+ this.renderRouteView = true;
439
+ void stopProgress();
440
+ },
441
+
442
+ /**
443
+ * 根据路由名称刷新指定标签页
444
+ */
445
+ async refreshByName(name: string) {
446
+ this.excludeCachedTabs.add(name);
447
+ await new Promise((resolve) => setTimeout(resolve, 200));
448
+ this.excludeCachedTabs.delete(name);
449
+ },
450
+
451
+ /**
452
+ * @zh_CN 重置标签页标题
453
+ */
454
+ async resetTabTitle(tab: TabDefinition) {
455
+ if (tab?.meta?.newTabTitle) {
456
+ return;
457
+ }
458
+ const findTab = this.tabs.find((item) => equalTab(item, tab));
459
+ if (findTab) {
460
+ findTab.meta.newTabTitle = undefined;
461
+ await this.updateCacheTabs();
462
+ }
463
+ },
464
+
465
+ /**
466
+ * 设置固定标签页
467
+ * @param tabs
468
+ */
469
+ setAffixTabs(tabs: RouteRecordNormalized[]) {
470
+ for (const tab of tabs) {
471
+ tab.meta.affixTab = true;
472
+ this.addTab(routeToTab(tab));
473
+ }
474
+ },
475
+
476
+ /**
477
+ * @zh_CN 更新菜单列表
478
+ * @param list
479
+ */
480
+ setMenuList(list: string[]) {
481
+ this.menuList = list;
482
+ },
483
+
484
+ /**
485
+ * @zh_CN 设置标签页标题
486
+ *
487
+ * @zh_CN 支持设置静态标题字符串或计算属性作为动态标题
488
+ * @zh_CN 当标题为计算属性时,标题会随计算属性值变化而自动更新
489
+ * @zh_CN 适用于需要根据状态或多语言动态更新标题的场景
490
+ *
491
+ * @param {TabDefinition} tab - 标签页对象
492
+ * @param {ComputedRef<string> | string} title - 标题内容,支持静态字符串或计算属性
493
+ *
494
+ * @example
495
+ * // 设置静态标题
496
+ * setTabTitle(tab, '新标签页');
497
+ *
498
+ * @example
499
+ * // 设置动态标题
500
+ * setTabTitle(tab, computed(() => t('common.dashboard')));
501
+ */
502
+ async setTabTitle(tab: TabDefinition, title: ComputedRef<string> | string) {
503
+ const findTab = this.tabs.find((item) => equalTab(item, tab));
504
+
505
+ if (findTab) {
506
+ findTab.meta.newTabTitle = title;
507
+
508
+ await this.updateCacheTabs();
509
+ }
510
+ },
511
+ setUpdateTime() {
512
+ this.updateTime = Date.now();
513
+ },
514
+ /**
515
+ * @zh_CN 设置标签页顺序
516
+ * @param oldIndex
517
+ * @param newIndex
518
+ */
519
+ async sortTabs(oldIndex: number, newIndex: number) {
520
+ // 拖拽回调(use-tabs-drag)给出的是渲染顺序的下标,而 this.tabs 的原始顺序
521
+ // 可能把固定标签页排在后面,直接按下标改 this.tabs 会动到别的标签页。
522
+ const currentTab = this.getTabs[oldIndex];
523
+ if (!currentTab) {
524
+ return;
525
+ }
526
+ const sortedTabs = this.getTabs.toSpliced(oldIndex, 1);
527
+ sortedTabs.splice(newIndex, 0, currentTab);
528
+ this.tabs = sortedTabs;
529
+ this.dragEndIndex = this.dragEndIndex + 1;
530
+ },
531
+
532
+ /**
533
+ * @zh_CN 切换固定标签页
534
+ * @param tab
535
+ */
536
+ async toggleTabPin(tab: TabDefinition) {
537
+ const affixTab = tab?.meta?.affixTab ?? false;
538
+
539
+ await (affixTab ? this.unpinTab(tab) : this.pinTab(tab));
540
+ },
541
+
542
+ /**
543
+ * @zh_CN 取消固定标签页
544
+ * @param tab
545
+ */
546
+ async unpinTab(tab: TabDefinition) {
547
+ const index = this.tabs.findIndex((item) => equalTab(item, tab));
548
+ if (index === -1) {
549
+ return;
550
+ }
551
+ const oldTab = this.tabs[index];
552
+ tab.meta.affixTab = false;
553
+ tab.meta.title = oldTab?.meta?.title as string;
554
+ // this.addTab(tab);
555
+ this.tabs.splice(index, 1, tab);
556
+ // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
557
+ const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
558
+ // 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置
559
+ const newIndex = affixTabs.length;
560
+ // sortTabs 的源下标同样要按渲染顺序取,否则固定标签在原始数组靠后时会动到别的标签页
561
+ const renderedIndex = this.getTabs.findIndex((item) =>
562
+ equalTab(item, tab),
563
+ );
564
+ // 交换位置重新排序
565
+ await this.sortTabs(renderedIndex, newIndex);
566
+ },
567
+ /**
568
+ * 根据当前打开的选项卡更新缓存
569
+ */
570
+ async updateCacheTabs() {
571
+ const cacheMap = new Set<string>();
572
+
573
+ for (const tab of this.tabs) {
574
+ // 跳过不需要持久化的标签页
575
+ const keepAlive = tab.meta?.keepAlive;
576
+ if (!keepAlive) {
577
+ continue;
578
+ }
579
+ (tab.matched || []).forEach((t, i) => {
580
+ if (i > 0) {
581
+ cacheMap.add(t.name as string);
582
+ }
583
+ });
584
+
585
+ const name = tab.name as string;
586
+ cacheMap.add(name);
587
+ }
588
+ this.cachedTabs = cacheMap;
589
+ },
590
+ /**
591
+ * 添加缓存的route
592
+ * @param component
593
+ * @param route
594
+ */
595
+ addCachedRoute(component: VNode, route: RouteLocationNormalizedLoaded) {
596
+ const key = getTabKey(route);
597
+ if (this.cachedRoutes.has(key)) {
598
+ return;
599
+ }
600
+ this.cachedRoutes.set(key, {
601
+ key,
602
+ component: markRaw(component),
603
+ route: markRaw(route),
604
+ });
605
+ },
606
+ removeCachedRoute(key: string) {
607
+ this.cachedRoutes.delete(key);
608
+ },
609
+ },
610
+ getters: {
611
+ affixTabs(): TabDefinition[] {
612
+ const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
613
+
614
+ return affixTabs.toSorted((a, b) => {
615
+ const orderA = a.meta?.affixTabOrder ?? 0;
616
+ const orderB = b.meta?.affixTabOrder ?? 0;
617
+ return orderA - orderB;
618
+ });
619
+ },
620
+ getCachedTabs(): string[] {
621
+ return [...this.cachedTabs];
622
+ },
623
+ getExcludeCachedTabs(): string[] {
624
+ return [...this.excludeCachedTabs];
625
+ },
626
+ getMenuList(): string[] {
627
+ return this.menuList;
628
+ },
629
+ getTabs(): TabDefinition[] {
630
+ const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
631
+ return [...this.affixTabs, ...normalTabs].filter(Boolean);
632
+ },
633
+ getCachedRoutes(): Map<string, RouteCached> {
634
+ return this.cachedRoutes;
635
+ },
636
+ },
637
+ persist: [
638
+ // tabs不需要保存在localStorage
639
+ {
640
+ pick: ['tabs', 'visitHistory'],
641
+ storage: sessionStorage,
642
+ serializer: {
643
+ serialize: JSON.stringify,
644
+ deserialize(value: string) {
645
+ const parsed = JSON.parse(value);
646
+ // Stack 类实例经 JSON 序列化后会变成普通对象 {dedup, items, maxSize},
647
+ // 丢失所有方法和 getter,需要重新构建 Stack 实例
648
+ if (parsed.visitHistory && !(parsed.visitHistory instanceof Stack)) {
649
+ const raw = parsed.visitHistory;
650
+ const stack = createStack<string>(true, MAX_VISIT_HISTORY);
651
+ if (Array.isArray(raw.items)) {
652
+ stack.push(...raw.items);
653
+ }
654
+ parsed.visitHistory = stack;
655
+ }
656
+ return parsed;
657
+ },
658
+ },
659
+ },
660
+ ],
661
+ state: (): TabbarState => ({
662
+ visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),
663
+ cachedRoutes: new Map<string, RouteCached>(),
664
+ cachedTabs: new Set(),
665
+ dragEndIndex: 0,
666
+ excludeCachedTabs: new Set(),
667
+ menuList: [
668
+ 'close',
669
+ 'affix',
670
+ 'maximize',
671
+ 'reload',
672
+ 'open-in-new-window',
673
+ 'close-left',
674
+ 'close-right',
675
+ 'close-other',
676
+ 'close-all',
677
+ ],
678
+ renderRouteView: true,
679
+ tabs: [],
680
+ updateTime: Date.now(),
681
+ }),
682
+ });
683
+
684
+ // 解决热更新问题
685
+ const hot = import.meta.hot;
686
+ if (hot) {
687
+ hot.accept(acceptHMRUpdate(useTabbarStore, hot));
688
+ }
689
+
690
+ /**
691
+ * @zh_CN 克隆路由,防止路由被修改
692
+ * @param route
693
+ */
694
+ function cloneTab(route: TabDefinition): TabDefinition {
695
+ if (!route) {
696
+ return route;
697
+ }
698
+ const { matched, meta, ...opt } = route;
699
+ return {
700
+ ...opt,
701
+ matched: (matched
702
+ ? matched.map((item) => ({
703
+ meta: item.meta,
704
+ name: item.name,
705
+ path: item.path,
706
+ }))
707
+ : undefined) as RouteRecordNormalized[],
708
+ meta: {
709
+ ...meta,
710
+ newTabTitle: meta.newTabTitle,
711
+ },
712
+ };
713
+ }
714
+
715
+ /**
716
+ * @zh_CN 是否是固定标签页
717
+ * @param tab
718
+ */
719
+ function isAffixTab(tab: TabDefinition) {
720
+ return tab?.meta?.affixTab ?? false;
721
+ }
722
+
723
+ /**
724
+ * @zh_CN 是否显示标签
725
+ * @param tab
726
+ */
727
+ function isTabShown(tab: TabDefinition) {
728
+ const matched = tab?.matched ?? [];
729
+ return !tab.meta.hideInTab && matched.every((item) => !item.meta.hideInTab);
730
+ }
731
+
732
+ /**
733
+ * 从route获取tab页的key
734
+ * @param tab
735
+ */
736
+ function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
737
+ // 运行时传入的记录可能缺省 meta/query(测试与部分调用点给的是不完整对象),
738
+ // 按可选视图解构,让解构默认值真正承担兜底职责。
739
+ const {
740
+ fullPath,
741
+ path,
742
+ meta: { fullPathKey } = {},
743
+ query = {},
744
+ } = tab as Partial<RouteLocationNormalized> &
745
+ Pick<RouteLocationNormalized, 'fullPath' | 'path'>;
746
+ // pageKey可能是数组(查询参数重复时可能出现)
747
+ const pageKey = Array.isArray(query.pageKey)
748
+ ? query.pageKey[0]
749
+ : query.pageKey;
750
+ let rawKey;
751
+ if (pageKey) {
752
+ rawKey = pageKey;
753
+ } else {
754
+ rawKey = fullPathKey === false ? path : (fullPath ?? path);
755
+ }
756
+ try {
757
+ return decodeURIComponent(rawKey);
758
+ } catch {
759
+ return rawKey;
760
+ }
761
+ }
762
+
763
+ /**
764
+ * @zh_CN 是否开启访问历史记录
765
+ */
766
+ function isVisitHistory() {
767
+ return preferences.tabbar.visitHistory;
768
+ }
769
+
770
+ /**
771
+ * 从tab获取tab页的key
772
+ * 如果tab没有key,那么就从route获取key
773
+ * @param tab
774
+ */
775
+ function getTabKeyFromTab(tab: TabDefinition): string {
776
+ return tab.key ?? getTabKey(tab);
777
+ }
778
+
779
+ /**
780
+ * 比较两个tab是否相等
781
+ * @param a
782
+ * @param b
783
+ */
784
+ function equalTab(a: TabDefinition, b: TabDefinition) {
785
+ return getTabKeyFromTab(a) === getTabKeyFromTab(b);
786
+ }
787
+
788
+ function routeToTab(route: RouteRecordNormalized) {
789
+ return {
790
+ meta: route.meta,
791
+ name: route.name,
792
+ path: route.path,
793
+ key: getTabKey(route),
794
+ } as TabDefinition;
795
+ }
796
+
797
+ export { getTabKey };