vue-stack-tabs 1.3.1 → 1.4.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 (97) hide show
  1. package/README.en.md +119 -52
  2. package/README.md +103 -42
  3. package/README.ru.md +125 -54
  4. package/dist/{src/lib/StackTabs.vue.d.ts → StackTabs.vue.d.ts} +5 -5
  5. package/dist/{src/lib/components → components}/ContextMenu/index.vue.d.ts +1 -1
  6. package/dist/components/StackKeepAlive/emptyPlaceholder.d.ts +3 -0
  7. package/dist/{src/lib/components → components}/TabHeader/TabHeaderItem.vue.d.ts +9 -0
  8. package/dist/{src/lib/components → components}/TabHeader/index.vue.d.ts +3 -3
  9. package/dist/{src/lib/hooks → hooks}/stackTabsContext.d.ts +5 -2
  10. package/dist/hooks/tabPanel/pageComponentFactory.d.ts +23 -0
  11. package/dist/{src/lib/hooks → hooks}/tabPanel/refresh.d.ts +6 -3
  12. package/dist/{src/lib/hooks → hooks}/tabPanel/scroll.d.ts +2 -0
  13. package/dist/hooks/tabPanel/tabStore.d.ts +69 -0
  14. package/dist/hooks/useScrollShadow.d.ts +20 -0
  15. package/dist/{src/lib/hooks → hooks}/useTabActions.d.ts +7 -2
  16. package/dist/{src/lib/hooks → hooks}/useTabEventBus.d.ts +2 -1
  17. package/dist/{src/lib/hooks → hooks}/useTabPanel.d.ts +16 -4
  18. package/dist/iframe-bridge.mjs +1 -1
  19. package/dist/index.d.ts +22 -6
  20. package/dist/{src/lib/model → model}/TabModel.d.ts +21 -0
  21. package/dist/nuxt/module.mjs +10 -6
  22. package/dist/nuxt/runtime/plugin.mjs +1600 -4
  23. package/dist/style.css +1 -1
  24. package/dist/utils/iframeBridge.d.ts +7 -1
  25. package/dist/{src/lib/utils → utils}/scrollUtils.d.ts +7 -1
  26. package/dist/{src/lib/versionLogger.d.ts → versionLogger.d.ts} +1 -1
  27. package/dist/vue-stack-tabs.css +1 -1
  28. package/dist/vue-stack-tabs.es.js +1 -1
  29. package/package.json +64 -40
  30. package/src/lib/StackTabs.vue +16 -3
  31. package/src/lib/assets/style/common/var.scss +2 -0
  32. package/src/lib/assets/style/stackTab.scss +17 -3
  33. package/src/lib/components/ContextMenu/index.vue +12 -3
  34. package/src/lib/components/PageLoading.vue +10 -2
  35. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +3 -9
  36. package/src/lib/components/StackKeepAlive/emptyPlaceholder.tsx +18 -0
  37. package/src/lib/components/TabHeader/index.vue +4 -3
  38. package/src/lib/hooks/stackTabsContext.ts +23 -18
  39. package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +136 -0
  40. package/src/lib/hooks/tabPanel/refresh.ts +20 -13
  41. package/src/lib/hooks/tabPanel/scroll.ts +110 -7
  42. package/src/lib/hooks/tabPanel/session.ts +29 -3
  43. package/src/lib/hooks/tabPanel/tabStore.ts +595 -0
  44. package/src/lib/hooks/useContextMenu.ts +57 -11
  45. package/src/lib/hooks/useIframeManager.ts +18 -2
  46. package/src/lib/hooks/useScrollShadow.ts +124 -0
  47. package/src/lib/hooks/useTabActions.ts +18 -8
  48. package/src/lib/hooks/useTabEventBus.ts +5 -3
  49. package/src/lib/hooks/useTabPanel.tsx +183 -302
  50. package/src/lib/hooks/useTabRouter.ts +42 -38
  51. package/src/lib/i18n/index.ts +3 -3
  52. package/src/lib/model/TabModel.ts +40 -1
  53. package/src/lib/nuxt/module.ts +10 -3
  54. package/src/lib/nuxt/runtime/plugin.ts +13 -2
  55. package/src/lib/utils/iframeBridge.ts +9 -1
  56. package/src/lib/utils/scrollUtils.ts +35 -2
  57. package/src/lib/utils/tabInfoEncoder.ts +21 -8
  58. package/src/lib/utils/urlParser.ts +3 -1
  59. package/src/lib/versionLogger.ts +6 -2
  60. package/dist/favicon.svg +0 -10
  61. package/dist/iframe-bridge.html +0 -77
  62. package/dist/iframe-child.html +0 -88
  63. package/dist/nuxt/favicon.svg +0 -10
  64. package/dist/nuxt/iframe-bridge.html +0 -77
  65. package/dist/nuxt/iframe-child.html +0 -88
  66. package/dist/nuxt/plugin.mjs +0 -9
  67. package/dist/src/lib/iframe-bridge.d.ts +0 -8
  68. package/dist/src/lib/index.d.ts +0 -22
  69. package/dist/src/lib/nuxt/module.d.ts +0 -6
  70. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +0 -2
  71. package/dist/src/lib/nuxt/runtime/plugin.d.ts +0 -2
  72. package/dist/src/lib/utils/iframeBridge.d.ts +0 -39
  73. package/src/lib/nuxt/runtime/plugin.client.ts +0 -10
  74. /package/dist/{src/lib/components → components}/ContextMenu/ContextMenuItem.vue.d.ts +0 -0
  75. /package/dist/{src/lib/components → components}/PageLoading.vue.d.ts +0 -0
  76. /package/dist/{src/lib/components → components}/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -0
  77. /package/dist/{src/lib/components → components}/StackKeepAlive/StackKeepAlive.vue.d.ts +0 -0
  78. /package/dist/{src/lib/components → components}/TabHeader/TabHeaderButton.vue.d.ts +0 -0
  79. /package/dist/{src/lib/components → components}/TabHeader/TabHeaderScroll.vue.d.ts +0 -0
  80. /package/dist/{src/lib/hooks → hooks}/tabPanel/evict.d.ts +0 -0
  81. /package/dist/{src/lib/hooks → hooks}/tabPanel/navigationTransaction.d.ts +0 -0
  82. /package/dist/{src/lib/hooks → hooks}/tabPanel/session.d.ts +0 -0
  83. /package/dist/{src/lib/hooks → hooks}/tabPanel/state.d.ts +0 -0
  84. /package/dist/{src/lib/hooks → hooks}/useContextMenu.d.ts +0 -0
  85. /package/dist/{src/lib/hooks → hooks}/useIframeManager.d.ts +0 -0
  86. /package/dist/{src/lib/hooks → hooks}/useTabLoading.d.ts +0 -0
  87. /package/dist/{src/lib/hooks → hooks}/useTabRouter.d.ts +0 -0
  88. /package/dist/{src/lib/i18n → i18n}/index.d.ts +0 -0
  89. /package/dist/{src/lib/i18n → i18n}/lang/en.d.ts +0 -0
  90. /package/dist/{src/lib/i18n → i18n}/lang/zh-CN.d.ts +0 -0
  91. /package/dist/{src/lib/i18n → i18n}/stackTabsLocale.d.ts +0 -0
  92. /package/dist/{src/lib/iframe.vue.d.ts → iframe.vue.d.ts} +0 -0
  93. /package/dist/{src/lib/utils → utils}/stackTabsA11y.d.ts +0 -0
  94. /package/dist/{src/lib/utils → utils}/stackTabsMessage.d.ts +0 -0
  95. /package/dist/{src/lib/utils → utils}/tabInfoEncoder.d.ts +0 -0
  96. /package/dist/{src/lib/utils → utils}/typeGuards.d.ts +0 -0
  97. /package/dist/{src/lib/utils → utils}/urlParser.d.ts +0 -0
package/README.en.md CHANGED
@@ -1,8 +1,16 @@
1
1
  # vue-stack-tabs
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/vue-stack-tabs.svg)](https://www.npmjs.com/package/vue-stack-tabs)
4
+ [![npm downloads](https://img.shields.io/npm/dm/vue-stack-tabs.svg)](https://www.npmjs.com/package/vue-stack-tabs)
5
+ [![license](https://img.shields.io/npm/l/vue-stack-tabs.svg)](./LICENSE)
6
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/vue-stack-tabs)](https://bundlephobia.com/package/vue-stack-tabs)
7
+ [![docs](https://img.shields.io/badge/docs-GitHub%20Pages-377dff)](https://jktantan.github.io/vue-stack-tabs/)
8
+
3
9
  [中文](./README.md) | **English** | [Русский](./README.ru.md)
4
10
 
5
- > A Vue 3 multi-tab management library based on Vue Router. Achieves iframe-like TabPanel behavior using Vue scopes — **each tab has its own isolated component scope**.
11
+ **📖 [Docs](https://jktantan.github.io/vue-stack-tabs/) · 🎮 [Live demo](https://jktantan.github.io/vue-stack-tabs/demo.html)**
12
+
13
+ > A Vue 3 multi-tab manager built on Vue Router. It delivers iframe-like tab isolation through independent Vue scopes: every tab keeps its own page instance, cache, and navigation stack.
6
14
 
7
15
  ## Features
8
16
 
@@ -13,6 +21,7 @@
13
21
  - **Session persistence** — restore last active tabs after browser refresh
14
22
  - **Scroll position memory** — automatically restore scroll position when switching tabs
15
23
  - **Context menu** — built-in close/refresh actions
24
+ - **Slots** — tab bar left/right buttons, custom iframe loading/error placeholders
16
25
  - **i18n** — built-in Chinese & English, extensible
17
26
  - **Nuxt 3/4 module** — works out of the box
18
27
 
@@ -38,7 +47,7 @@ pnpm add vue-stack-tabs
38
47
  // main.ts
39
48
  import { createApp } from 'vue'
40
49
  import VueStackTabs from 'vue-stack-tabs'
41
- import 'vue-stack-tabs/dist/vue-stack-tabs.css'
50
+ import 'vue-stack-tabs/dist/style.css'
42
51
  import App from './App.vue'
43
52
  import router from './router'
44
53
 
@@ -48,6 +57,8 @@ app.use(VueStackTabs)
48
57
  app.mount('#app')
49
58
  ```
50
59
 
60
+ > `vue-stack-tabs` supports only one `<VueStackTabs>` instance per Vue app. The default export is a Vue plugin for `app.use(VueStackTabs)`; if you need a local component import, use the named export below.
61
+
51
62
  ### 2. Configure Routes
52
63
 
53
64
  Tabs depend on Vue Router. You need a parent route to host `<VueStackTabs>`:
@@ -100,6 +111,12 @@ const defaultTabs: ITabData[] = [
100
111
  </script>
101
112
  ```
102
113
 
114
+ If you prefer a local component import, use the `VueStackTabs` named export; the `StackTab` named export is kept as a compatibility alias:
115
+
116
+ ```ts
117
+ import { VueStackTabs as VueStackTabsComponent } from 'vue-stack-tabs'
118
+ ```
119
+
103
120
  ### 4. Open Tabs
104
121
 
105
122
  ```ts
@@ -133,9 +150,9 @@ refreshTab('about')
133
150
  export default defineNuxtConfig({
134
151
  modules: ['vue-stack-tabs/nuxt'],
135
152
  vueStackTabs: {
136
- locale: 'en-US'
153
+ locale: 'en' // built-in options: 'zh-CN' | 'en'
137
154
  },
138
- css: ['vue-stack-tabs/dist/vue-stack-tabs.css']
155
+ css: ['vue-stack-tabs/dist/style.css']
139
156
  })
140
157
  ```
141
158
 
@@ -183,7 +200,7 @@ Main application continues to use the root entry:
183
200
 
184
201
  ```ts
185
202
  import VueStackTabs from 'vue-stack-tabs'
186
- import 'vue-stack-tabs/dist/vue-stack-tabs.css'
203
+ import 'vue-stack-tabs/dist/style.css'
187
204
  ```
188
205
 
189
206
  For iframe inner pages, use the side-effect-free sub-entry:
@@ -233,26 +250,36 @@ Tab-level operation composable.
233
250
  import { useTabActions } from 'vue-stack-tabs'
234
251
 
235
252
  const {
236
- openTab, // (tab: ITabData, renew?: boolean) => Promise<string>
253
+ openTab, // (tab: ITabData, renew?: boolean) => Promise<string | undefined>
237
254
  closeTab, // (id: string) => string
238
255
  closeAllTabs, // () => void
239
256
  refreshTab, // (id: string) => void
240
257
  refreshAllTabs, // () => void
241
258
  activeTab, // (id: string, isRoute?: boolean) => void
259
+ moveTab, // (id: string, targetIndex: number) => boolean
242
260
  reset, // () => void
243
- tabs // Ref<ITabItem[]>
261
+ tabs, // Ref<ITabItem[]>
262
+ setIFramePath, // (path: string) => void
263
+ getWrapper, // ({ tabId?, pageId? }) => HTMLElement | undefined
264
+ openInNewWindow // (id: string) => void
244
265
  } = useTabActions()
245
266
  ```
246
267
 
247
- | Method | Description |
248
- | ---------------------- | ------------------------------------------------------------------------ |
249
- | `openTab(tab, renew?)` | Open a new tab. When `renew=true`, clears page stack and reopens if exists |
250
- | `closeTab(id)` | Close the specified tab, returns newly activated tab ID |
251
- | `closeAllTabs()` | Close all closable tabs |
252
- | `refreshTab(id)` | Refresh the specified tab (replaces cache ID, rebuilds component instance) |
253
- | `refreshAllTabs()` | Refresh all tabs |
254
- | `activeTab(id)` | Activate the specified tab (switch tab) |
255
- | `reset()` | Close all tabs and reset state |
268
+ | Method | Description |
269
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
270
+ | `openTab(tab, renew?)` | Open a new tab. When `renew=true`, clears page stack and reopens if exists |
271
+ | `closeTab(id)` | Close the specified tab, returns newly activated tab ID |
272
+ | `closeAllTabs()` | Close all closable tabs |
273
+ | `refreshTab(id)` | Refresh the specified tab (replaces cache ID, rebuilds component instance) |
274
+ | `refreshAllTabs()` | Refresh all tabs |
275
+ | `activeTab(id)` | Activate the specified tab (switch tab) |
276
+ | `moveTab(id, targetIndex)` | Move a tab to a given index; the tab bar also supports mouse drag reordering |
277
+ | `reset()` | Close all tabs and reset state |
278
+ | `setIFramePath(path)` | Set the iframe placeholder route path (called internally on mount) |
279
+ | `getWrapper({ tabId?, pageId? })` | Get the content container DOM node; without arguments returns the first container, pass `tabId` or `pageId` when multiple tabs exist |
280
+ | `openInNewWindow(id)` | Open an iframe tab in a new window (fallback when embedding fails) |
281
+
282
+ > Close left / right / others are context-menu-only operations, handled inside `TabHeader`/`ContextMenu` through `useTabPanel`.
256
283
 
257
284
  ---
258
285
 
@@ -266,11 +293,11 @@ import { useTabRouter } from 'vue-stack-tabs'
266
293
  const { forward, backward, addScrollTarget } = useTabRouter()
267
294
  ```
268
295
 
269
- | Method | Description |
270
- | ----------------------------- | --------------------------------------------------- |
271
- | `forward(to)` | Navigate forward to a new page within current tab |
272
- | `backward(to, backQuery?)` | Navigate backward within current tab |
273
- | `addScrollTarget(selector)` | Register a DOM selector for scroll position memory |
296
+ | Method | Description |
297
+ | --------------------------- | -------------------------------------------------- |
298
+ | `forward(to)` | Navigate forward to a new page within current tab |
299
+ | `backward(to, backQuery?)` | Navigate backward within current tab |
300
+ | `addScrollTarget(selector)` | Register a DOM selector for scroll position memory |
274
301
 
275
302
  #### forward
276
303
 
@@ -333,36 +360,68 @@ fetchData().finally(() => closeTabLoading())
333
360
 
334
361
  `<VueStackTabs>` component properties:
335
362
 
336
- | Prop | Type | Default | Description |
337
- | ---------------------- | --------------------------- | ----------------------- | -------------------------------------------------------- |
338
- | `iframePath` | `string` | **Required** | Path of the iframe placeholder route |
363
+ | Prop | Type | Default | Description |
364
+ | ---------------------- | --------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
365
+ | `iframePath` | `string` | **Required** | Path of the iframe placeholder route |
339
366
  | `iframeSandbox` | `string` | `allow-scripts allow-forms allow-popups allow-downloads allow-same-origin` | iframe sandbox policy; default is compatibility-first, not strict isolation — remove `allow-same-origin` for strict isolation; pass empty string to disable sandbox (not recommended) |
340
- | `iframeReferrerPolicy` | `ReferrerPolicy` | `strict-origin-when-cross-origin` | iframe referrerpolicy attribute |
341
- | `iframeAllow` | `string` | `''` | iframe allow attribute, e.g. `fullscreen` |
342
- | `iframeLoadTimeout` | `number` | `15000` | iframe load timeout in ms |
343
- | `defaultTabs` | `ITabData[]` | `[]` | Initial tab list |
344
- | `max` | `number` | `20` | Maximum number of tabs |
345
- | `contextmenu` | `boolean \| object` | `true` | Enable context menu |
346
- | `pageTransition` | `string` | `'stack-tab-swap'` | Page transition animation name for forward navigation |
347
- | `pageTransitionBack` | `string` | `'stack-tab-swap-back'` | Page transition animation name for backward navigation |
348
- | `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'` | Transition effect for tab add/remove |
349
- | `tabScrollMode` | `TabScrollMode` | `'both'` | Tab bar scroll mode: `'wheel'` / `'button'` / `'both'` |
350
- | `width` | `string` | `'100%'` | Container width |
351
- | `height` | `string` | `'100%'` | Container height |
352
- | `i18n` | `string` | `'zh-CN'` | i18n locale |
353
- | `globalScroll` | `boolean` | `false` | Enable page-level scroll memory |
354
- | `sessionPrefix` | `string` | `''` | sessionStorage key prefix |
355
- | `iframeAllowedOrigins` | `string[]` | Same origin | Allowed origins for iframe postMessage |
367
+ | `iframeReferrerPolicy` | `ReferrerPolicy` | `strict-origin-when-cross-origin` | iframe referrerpolicy attribute |
368
+ | `iframeAllow` | `string` | `''` | iframe allow attribute, e.g. `fullscreen` |
369
+ | `iframeLoadTimeout` | `number` | `15000` | iframe load timeout in ms |
370
+ | `defaultTabs` | `ITabData[]` | `[]` | Initial tab list |
371
+ | `max` | `number` | `20` | Maximum number of tabs |
372
+ | `contextmenu` | `boolean \| object` | `true` | Enable context menu |
373
+ | `pageTransition` | `string` | `'stack-tab-swap'` | Page transition animation name for forward navigation |
374
+ | `pageTransitionBack` | `string` | `'stack-tab-swap-back'` | Page transition animation name for backward navigation |
375
+ | `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'` | Transition effect for tab add/remove |
376
+ | `tabScrollMode` | `TabScrollMode` | `'both'` | Tab bar scroll mode: `'wheel'` / `'button'` / `'both'` |
377
+ | `space` | `number` | `300` | Pixel distance scrolled per tab bar step |
378
+ | `width` | `string` | `'100%'` | Container width |
379
+ | `height` | `string` | `'100%'` | Container height |
380
+ | `i18n` | `string` | `'zh-CN'` | i18n locale, built-in `'zh-CN'` / `'en'` |
381
+ | `globalScroll` | `boolean` | `false` | Enable page-level scroll memory |
382
+ | `sessionPrefix` | `string` | `''` | sessionStorage key prefix; useful for multiple instances on the same origin |
383
+ | `iframeAllowedOrigins` | `string[]` | `[]` (plus current origin) | Allowed origins for iframe postMessage openTab |
356
384
 
357
385
  ---
358
386
 
359
387
  ## Events
360
388
 
361
- | Event | Params | Description |
362
- | -------------- | -------------- | ---------------------------------- |
363
- | `onActive` | `(id: string)` | Triggered when a tab is activated |
389
+ | Event | Params | Description |
390
+ | -------------- | -------------- | ----------------------------------- |
391
+ | `onActive` | `(id: string)` | Triggered when a tab is activated |
364
392
  | `onPageLoaded` | — | Triggered when page component loads |
365
393
 
394
+ ```vue
395
+ <VueStackTabs
396
+ iframe-path="/iframe"
397
+ @on-active="(id) => console.log('active:', id)"
398
+ @on-page-loaded="() => console.log('page loaded')"
399
+ />
400
+ ```
401
+
402
+ ---
403
+
404
+ ## Slots
405
+
406
+ | Slot | Slot props | Description |
407
+ | --------------- | ---------------- | -------------------------------------------------------------------- |
408
+ | `leftButton` | — | Left area of the tab bar, usually with `TabHeaderButton` |
409
+ | `rightButton` | — | Right area of the tab bar |
410
+ | `iframeLoading` | `{ tab }` | Custom loading placeholder for an iframe tab |
411
+ | `iframeError` | `{ tab, retry }` | Custom error placeholder for an iframe tab; call `retry()` to reload |
412
+
413
+ ```vue
414
+ <VueStackTabs iframe-path="/iframe">
415
+ <template #leftButton>
416
+ <TabHeaderButton title="Toggle" @click="collapse = !collapse" />
417
+ </template>
418
+ <template #iframeError="{ tab, retry }">
419
+ <span>{{ tab.title }} failed to load</span>
420
+ <button @click="retry">Retry</button>
421
+ </template>
422
+ </VueStackTabs>
423
+ ```
424
+
366
425
  ---
367
426
 
368
427
  ## iframe Tabs
@@ -451,23 +510,29 @@ You can specify refresh behavior via `iframeRefreshMode` when calling `openTab`:
451
510
 
452
511
  ## i18n
453
512
 
454
- Built-in languages: `zh-CN` (Chinese), `en-US` (English).
513
+ Built-in languages: `zh-CN` (Chinese), `en` (English).
455
514
 
456
515
  ```ts
457
- // Switch language
458
- app.use(VueStackTabs, [{ locale: 'en-US' }])
516
+ // Switch language via component prop
517
+ // <VueStackTabs iframe-path="/iframe" i18n="en" />
518
+
519
+ // Or when installing the plugin
520
+ app.use(VueStackTabs, [{ locale: 'en' }])
459
521
  ```
460
522
 
461
- Custom language pack:
523
+ Custom language pack (`messages` is a nested object and is deep-merged with the built-in languages):
462
524
 
463
525
  ```ts
464
526
  app.use(VueStackTabs, [
465
527
  {
466
528
  locale: 'ja-JP',
467
529
  messages: {
468
- 'VueStackTab.close': 'Close',
469
- 'VueStackTab.refresh': 'Refresh'
470
- // ...
530
+ VueStackTab: {
531
+ close: 'Close',
532
+ reload: 'Reload',
533
+ loading: 'Loading'
534
+ // ...
535
+ }
471
536
  }
472
537
  }
473
538
  ])
@@ -478,12 +543,14 @@ app.use(VueStackTabs, [
478
543
  ## Type Definitions
479
544
 
480
545
  ```ts
546
+ import type { LocationQueryRaw } from 'vue-router'
547
+
481
548
  /** Data passed when opening a tab */
482
549
  interface ITabData {
483
550
  id?: string // Tab ID (auto-generated UUID if omitted)
484
551
  title: string // Tab title
485
552
  path: string // Route path or iframe URL
486
- query?: Record<string, string> // Route params
553
+ query?: LocationQueryRaw // Route params
487
554
  closable?: boolean // Whether closable (default true)
488
555
  refreshable?: boolean // Whether refreshable (default true)
489
556
  iframe?: boolean // Whether it's an iframe tab (default false)
@@ -523,4 +590,4 @@ For detailed architecture design, see [`ARCHITECTURE.md`](./ARCHITECTURE.md).
523
590
 
524
591
  ## License
525
592
 
526
- [LGPL-2.1](./LICENSE)
593
+ [Apache-2.0](./LICENSE)
package/README.md CHANGED
@@ -1,8 +1,20 @@
1
1
  # vue-stack-tabs
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/vue-stack-tabs.svg)](https://www.npmjs.com/package/vue-stack-tabs)
4
+ [![npm downloads](https://img.shields.io/npm/dm/vue-stack-tabs.svg)](https://www.npmjs.com/package/vue-stack-tabs)
5
+ [![license](https://img.shields.io/npm/l/vue-stack-tabs.svg)](./LICENSE)
6
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/vue-stack-tabs)](https://bundlephobia.com/package/vue-stack-tabs)
7
+ [![docs](https://img.shields.io/badge/docs-GitHub%20Pages-377dff)](https://jktantan.github.io/vue-stack-tabs/)
8
+
3
9
  **中文** | [English](./README.en.md) | [Русский](./README.ru.md)
4
10
 
5
- > Vue 3 多标签页管理库,基于 Vue Router。用 Vue 作用域实现类似 iframe TabPanel 的效果——**每个 Tab 间组件作用域互不干扰**。
11
+ **📖 [在线文档](https://jktantan.github.io/vue-stack-tabs/) · 🎮 [在线演示](https://jktantan.github.io/vue-stack-tabs/demo.html)**
12
+
13
+ > A Vue 3 multi-tab manager built on Vue Router. It delivers iframe-like tab isolation through independent Vue scopes: every tab keeps its own page instance, cache, and navigation stack.
14
+ >
15
+ > 基于 Vue Router 的 Vue 3 多标签页管理库。以独立 Vue 作用域实现类似 iframe 的标签隔离体验:每个标签都有自己的页面实例、缓存和导航栈,彼此互不干扰。
16
+
17
+ **Features** — route-level tabs with isolated component scope · stack-based forward/backward navigation · per-tab page refresh · iframe tabs with postMessage bridge · session restore · scroll position memory · context menu · i18n · Nuxt 3/4 module.
6
18
 
7
19
  ## ✨ 特性
8
20
 
@@ -13,6 +25,7 @@
13
25
  - 💾 **Session 持久化** — 浏览器刷新后恢复上次激活的标签
14
26
  - 📜 **滚动位置记忆** — 切换标签后自动恢复上次滚动位置
15
27
  - 🎨 **右键菜单** — 内置关闭/刷新操作
28
+ - 🧩 **自定义插槽** — 标签栏左右按钮位、iframe 加载/失败占位
16
29
  - 🌍 **国际化** — 内置中英文,可扩展
17
30
  - 📦 **Nuxt 3/4 模块** — 开箱即用
18
31
 
@@ -141,7 +154,7 @@ refreshTab('about')
141
154
  export default defineNuxtConfig({
142
155
  modules: ['vue-stack-tabs/nuxt'],
143
156
  vueStackTabs: {
144
- locale: 'zh-CN'
157
+ locale: 'zh-CN' // 内置可选 'zh-CN' | 'en'
145
158
  },
146
159
  css: ['vue-stack-tabs/dist/style.css']
147
160
  })
@@ -241,7 +254,7 @@ const off = onRefreshRequest(() => window.location.reload(), {
241
254
  import { useTabActions } from 'vue-stack-tabs'
242
255
 
243
256
  const {
244
- openTab, // (tab: ITabData, renew?: boolean) => Promise<string>
257
+ openTab, // (tab: ITabData, renew?: boolean) => Promise<string | undefined>
245
258
  closeTab, // (id: string) => string
246
259
  closeAllTabs, // () => void
247
260
  refreshTab, // (id: string) => void
@@ -249,20 +262,28 @@ const {
249
262
  activeTab, // (id: string, isRoute?: boolean) => void
250
263
  moveTab, // (id: string, targetIndex: number) => boolean
251
264
  reset, // () => void
252
- tabs // Ref<ITabItem[]>
265
+ tabs, // Ref<ITabItem[]>
266
+ setIFramePath, // (path: string) => void
267
+ getWrapper, // ({ tabId?, pageId? }) => HTMLElement | undefined
268
+ openInNewWindow // (id: string) => void
253
269
  } = useTabActions()
254
270
  ```
255
271
 
256
- | 方法 | 说明 |
257
- | ---------------------- | --------------------------------------------------------- |
258
- | `openTab(tab, renew?)` | 打开新标签。`renew=true` 时若已存在则清空页面栈后重新打开 |
259
- | `closeTab(id)` | 关闭指定标签,返回新激活的标签 ID |
260
- | `closeAllTabs()` | 关闭所有可关闭标签 |
261
- | `refreshTab(id)` | 刷新指定标签(替换缓存 ID,重建组件实例) |
262
- | `refreshAllTabs()` | 刷新所有标签 |
263
- | `activeTab(id)` | 激活指定标签(切换 Tab) |
264
- | `moveTab(id, targetIndex)` | 将标签移动到指定索引;标签栏也支持鼠标拖放排序 |
265
- | `reset()` | 关闭所有标签并重置状态 |
272
+ | 方法 | 说明 |
273
+ | --------------------------------- | ------------------------------------------------------------------------------------------------ |
274
+ | `openTab(tab, renew?)` | 打开新标签。`renew=true` 时若已存在则清空页面栈后重新打开 |
275
+ | `closeTab(id)` | 关闭指定标签,返回新激活的标签 ID |
276
+ | `closeAllTabs()` | 关闭所有可关闭标签 |
277
+ | `refreshTab(id)` | 刷新指定标签(替换缓存 ID,重建组件实例) |
278
+ | `refreshAllTabs()` | 刷新所有标签 |
279
+ | `activeTab(id)` | 激活指定标签(切换 Tab) |
280
+ | `moveTab(id, targetIndex)` | 将标签移动到指定索引;标签栏也支持鼠标拖放排序 |
281
+ | `reset()` | 关闭所有标签并重置状态 |
282
+ | `setIFramePath(path)` | 设置 iframe 占位路由路径(`<VueStackTabs>` 挂载时内部调用) |
283
+ | `getWrapper({ tabId?, pageId? })` | 获取页面内容容器 DOM;不传参数返回首个容器,多标签场景应传 `tabId` 或 `pageId`,避免取到错误页面 |
284
+ | `openInNewWindow(id)` | iframe 标签无法内嵌时在新窗口打开 |
285
+
286
+ > 关闭左侧 / 右侧 / 其他是标签栏右键菜单专属操作,在 `TabHeader`/`ContextMenu` 内部通过 `useTabPanel` 完成。
266
287
 
267
288
  ---
268
289
 
@@ -343,26 +364,27 @@ fetchData().finally(() => closeTabLoading())
343
364
 
344
365
  `<VueStackTabs>` 组件属性:
345
366
 
346
- | Prop | 类型 | 默认值 | 说明 |
347
- | ---------------------- | --------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------- |
348
- | `iframePath` | `string` | **必填** | iframe 占位路由的路径 |
367
+ | Prop | 类型 | 默认值 | 说明 |
368
+ | ---------------------- | --------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
369
+ | `iframePath` | `string` | **必填** | iframe 占位路由的路径 |
349
370
  | `iframeSandbox` | `string` | `allow-scripts allow-forms allow-popups allow-downloads allow-same-origin` | iframe sandbox 策略;默认兼容优先不是强隔离,强隔离请移除 `allow-same-origin`;传空字符串可关闭 sandbox(不推荐) |
350
- | `iframeReferrerPolicy` | `ReferrerPolicy` | `strict-origin-when-cross-origin` | iframe referrerpolicy 属性 |
351
- | `iframeAllow` | `string` | `''` | iframe allow 属性,例如 `fullscreen` |
352
- | `iframeLoadTimeout` | `number` | `15000` | iframe 加载超时时间,单位 ms |
353
- | `defaultTabs` | `ITabData[]` | `[]` | 初始标签列表 |
354
- | `max` | `number` | `20` | 最大标签数量 |
355
- | `contextmenu` | `boolean \| object` | `true` | 是否启用右键菜单 |
356
- | `pageTransition` | `string` | `'stack-tab-swap'` | 前进时的页面转场动画名 |
357
- | `pageTransitionBack` | `string` | `'stack-tab-swap-back'` | 后退时的页面转场动画名 |
358
- | `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'` | 标签增删时的过渡效果 |
359
- | `tabScrollMode` | `TabScrollMode` | `'both'` | 标签栏滚动方式:`'wheel'` / `'button'` / `'both'` |
360
- | `width` | `string` | `'100%'` | 容器宽度 |
361
- | `height` | `string` | `'100%'` | 容器高度 |
362
- | `i18n` | `string` | `'zh-CN'` | 国际化语言 |
363
- | `globalScroll` | `boolean` | `false` | 是否使用页面级滚动记忆 |
364
- | `sessionPrefix` | `string` | `''` | sessionStorage 键前缀 |
365
- | `iframeAllowedOrigins` | `string[]` | 同源 | 允许 iframe postMessage 的来源列表 |
371
+ | `iframeReferrerPolicy` | `ReferrerPolicy` | `strict-origin-when-cross-origin` | iframe referrerpolicy 属性 |
372
+ | `iframeAllow` | `string` | `''` | iframe allow 属性,例如 `fullscreen` |
373
+ | `iframeLoadTimeout` | `number` | `15000` | iframe 加载超时时间,单位 ms |
374
+ | `defaultTabs` | `ITabData[]` | `[]` | 初始标签列表 |
375
+ | `max` | `number` | `20` | 最大标签数量 |
376
+ | `contextmenu` | `boolean \| object` | `true` | 是否启用右键菜单 |
377
+ | `pageTransition` | `string` | `'stack-tab-swap'` | 前进时的页面转场动画名 |
378
+ | `pageTransitionBack` | `string` | `'stack-tab-swap-back'` | 后退时的页面转场动画名 |
379
+ | `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'` | 标签增删时的过渡效果 |
380
+ | `tabScrollMode` | `TabScrollMode` | `'both'` | 标签栏滚动方式:`'wheel'` / `'button'` / `'both'` |
381
+ | `space` | `number` | `300` | 标签栏滚动一步的像素距离 |
382
+ | `width` | `string` | `'100%'` | 容器宽度 |
383
+ | `height` | `string` | `'100%'` | 容器高度 |
384
+ | `i18n` | `string` | `'zh-CN'` | 国际化语言,内置 `'zh-CN'` / `'en'` |
385
+ | `globalScroll` | `boolean` | `false` | 是否使用页面级滚动记忆 |
386
+ | `sessionPrefix` | `string` | `''` | sessionStorage 键前缀;同一 origin 多实例时用于隔离 |
387
+ | `iframeAllowedOrigins` | `string[]` | `[]`(叠加当前 origin) | 允许 iframe postMessage 调用 openTab 的来源列表 |
366
388
 
367
389
  ---
368
390
 
@@ -373,6 +395,37 @@ fetchData().finally(() => closeTabLoading())
373
395
  | `onActive` | `(id: string)` | 标签被激活时触发 |
374
396
  | `onPageLoaded` | — | 页面组件加载完成时触发 |
375
397
 
398
+ ```vue
399
+ <VueStackTabs
400
+ iframe-path="/iframe"
401
+ @on-active="(id) => console.log('active:', id)"
402
+ @on-page-loaded="() => console.log('page loaded')"
403
+ />
404
+ ```
405
+
406
+ ---
407
+
408
+ ## 🧩 自定义插槽
409
+
410
+ | 插槽 | 作用域参数 | 说明 |
411
+ | --------------- | ---------------- | ----------------------------------------------------- |
412
+ | `leftButton` | — | 标签栏左侧按钮区,配合 `TabHeaderButton` 使用 |
413
+ | `rightButton` | — | 标签栏右侧按钮区 |
414
+ | `iframeLoading` | `{ tab }` | 自定义 iframe 加载中占位 |
415
+ | `iframeError` | `{ tab, retry }` | 自定义 iframe 加载失败占位,调用 `retry()` 可重新加载 |
416
+
417
+ ```vue
418
+ <VueStackTabs iframe-path="/iframe">
419
+ <template #leftButton>
420
+ <TabHeaderButton title="展开" @click="collapse = !collapse" />
421
+ </template>
422
+ <template #iframeError="{ tab, retry }">
423
+ <span>{{ tab.title }} 加载失败</span>
424
+ <button @click="retry">重试</button>
425
+ </template>
426
+ </VueStackTabs>
427
+ ```
428
+
376
429
  ---
377
430
 
378
431
  ## 🖼️ iframe 标签页
@@ -461,23 +514,29 @@ window.addEventListener('message', (ev) => {
461
514
 
462
515
  ## 🌍 国际化
463
516
 
464
- 内置语言:`zh-CN`(中文)、`en-US`(英文)。
517
+ 内置语言:`zh-CN`(中文)、`en`(英文)。
465
518
 
466
519
  ```ts
467
- // 切换语言
468
- app.use(VueStackTabs, [{ locale: 'en-US' }])
520
+ // 通过组件 prop 切换
521
+ // <VueStackTabs iframe-path="/iframe" i18n="en" />
522
+
523
+ // 或在安装插件时指定
524
+ app.use(VueStackTabs, [{ locale: 'en' }])
469
525
  ```
470
526
 
471
- 自定义语言包:
527
+ 自定义语言包(`messages` 为嵌套对象,会与内置语言深度合并):
472
528
 
473
529
  ```ts
474
530
  app.use(VueStackTabs, [
475
531
  {
476
532
  locale: 'ja-JP',
477
533
  messages: {
478
- 'VueStackTab.close': '閉じる',
479
- 'VueStackTab.refresh': '更新'
480
- // ...
534
+ VueStackTab: {
535
+ close: '閉じる',
536
+ reload: '更新',
537
+ loading: '読み込み中'
538
+ // ...
539
+ }
481
540
  }
482
541
  }
483
542
  ])
@@ -488,12 +547,14 @@ app.use(VueStackTabs, [
488
547
  ## 📄 类型定义
489
548
 
490
549
  ```ts
550
+ import type { LocationQueryRaw } from 'vue-router'
551
+
491
552
  /** 打开标签时传入的数据 */
492
553
  interface ITabData {
493
554
  id?: string // 标签 ID(不传则自动生成 UUID)
494
555
  title: string // 标签标题
495
556
  path: string // 路由路径或 iframe URL
496
- query?: Record<string, string> // 路由参数
557
+ query?: LocationQueryRaw // 路由参数
497
558
  closable?: boolean // 是否可关闭(默认 true)
498
559
  refreshable?: boolean // 是否可刷新(默认 true)
499
560
  iframe?: boolean // 是否为 iframe 标签(默认 false)
@@ -533,4 +594,4 @@ src/lib/
533
594
 
534
595
  ## 📝 License
535
596
 
536
- [LGPL-2.1](./LICENSE)
597
+ [Apache-2.0](./LICENSE)