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.
- package/README.en.md +119 -52
- package/README.md +103 -42
- package/README.ru.md +125 -54
- package/dist/{src/lib/StackTabs.vue.d.ts → StackTabs.vue.d.ts} +5 -5
- package/dist/{src/lib/components → components}/ContextMenu/index.vue.d.ts +1 -1
- package/dist/components/StackKeepAlive/emptyPlaceholder.d.ts +3 -0
- package/dist/{src/lib/components → components}/TabHeader/TabHeaderItem.vue.d.ts +9 -0
- package/dist/{src/lib/components → components}/TabHeader/index.vue.d.ts +3 -3
- package/dist/{src/lib/hooks → hooks}/stackTabsContext.d.ts +5 -2
- package/dist/hooks/tabPanel/pageComponentFactory.d.ts +23 -0
- package/dist/{src/lib/hooks → hooks}/tabPanel/refresh.d.ts +6 -3
- package/dist/{src/lib/hooks → hooks}/tabPanel/scroll.d.ts +2 -0
- package/dist/hooks/tabPanel/tabStore.d.ts +69 -0
- package/dist/hooks/useScrollShadow.d.ts +20 -0
- package/dist/{src/lib/hooks → hooks}/useTabActions.d.ts +7 -2
- package/dist/{src/lib/hooks → hooks}/useTabEventBus.d.ts +2 -1
- package/dist/{src/lib/hooks → hooks}/useTabPanel.d.ts +16 -4
- package/dist/iframe-bridge.mjs +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/{src/lib/model → model}/TabModel.d.ts +21 -0
- package/dist/nuxt/module.mjs +10 -6
- package/dist/nuxt/runtime/plugin.mjs +1600 -4
- package/dist/style.css +1 -1
- package/dist/utils/iframeBridge.d.ts +7 -1
- package/dist/{src/lib/utils → utils}/scrollUtils.d.ts +7 -1
- package/dist/{src/lib/versionLogger.d.ts → versionLogger.d.ts} +1 -1
- package/dist/vue-stack-tabs.css +1 -1
- package/dist/vue-stack-tabs.es.js +1 -1
- package/package.json +64 -40
- package/src/lib/StackTabs.vue +16 -3
- package/src/lib/assets/style/common/var.scss +2 -0
- package/src/lib/assets/style/stackTab.scss +17 -3
- package/src/lib/components/ContextMenu/index.vue +12 -3
- package/src/lib/components/PageLoading.vue +10 -2
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +3 -9
- package/src/lib/components/StackKeepAlive/emptyPlaceholder.tsx +18 -0
- package/src/lib/components/TabHeader/index.vue +4 -3
- package/src/lib/hooks/stackTabsContext.ts +23 -18
- package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +136 -0
- package/src/lib/hooks/tabPanel/refresh.ts +20 -13
- package/src/lib/hooks/tabPanel/scroll.ts +110 -7
- package/src/lib/hooks/tabPanel/session.ts +29 -3
- package/src/lib/hooks/tabPanel/tabStore.ts +595 -0
- package/src/lib/hooks/useContextMenu.ts +57 -11
- package/src/lib/hooks/useIframeManager.ts +18 -2
- package/src/lib/hooks/useScrollShadow.ts +124 -0
- package/src/lib/hooks/useTabActions.ts +18 -8
- package/src/lib/hooks/useTabEventBus.ts +5 -3
- package/src/lib/hooks/useTabPanel.tsx +183 -302
- package/src/lib/hooks/useTabRouter.ts +42 -38
- package/src/lib/i18n/index.ts +3 -3
- package/src/lib/model/TabModel.ts +40 -1
- package/src/lib/nuxt/module.ts +10 -3
- package/src/lib/nuxt/runtime/plugin.ts +13 -2
- package/src/lib/utils/iframeBridge.ts +9 -1
- package/src/lib/utils/scrollUtils.ts +35 -2
- package/src/lib/utils/tabInfoEncoder.ts +21 -8
- package/src/lib/utils/urlParser.ts +3 -1
- package/src/lib/versionLogger.ts +6 -2
- package/dist/favicon.svg +0 -10
- package/dist/iframe-bridge.html +0 -77
- package/dist/iframe-child.html +0 -88
- package/dist/nuxt/favicon.svg +0 -10
- package/dist/nuxt/iframe-bridge.html +0 -77
- package/dist/nuxt/iframe-child.html +0 -88
- package/dist/nuxt/plugin.mjs +0 -9
- package/dist/src/lib/iframe-bridge.d.ts +0 -8
- package/dist/src/lib/index.d.ts +0 -22
- package/dist/src/lib/nuxt/module.d.ts +0 -6
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +0 -2
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +0 -2
- package/dist/src/lib/utils/iframeBridge.d.ts +0 -39
- package/src/lib/nuxt/runtime/plugin.client.ts +0 -10
- /package/dist/{src/lib/components → components}/ContextMenu/ContextMenuItem.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/PageLoading.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackKeepAlive.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderButton.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderScroll.vue.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/evict.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/navigationTransaction.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/session.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/state.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useContextMenu.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useIframeManager.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabLoading.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabRouter.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/index.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/en.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/zh-CN.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/stackTabsLocale.d.ts +0 -0
- /package/dist/{src/lib/iframe.vue.d.ts → iframe.vue.d.ts} +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsA11y.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsMessage.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/tabInfoEncoder.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/typeGuards.d.ts +0 -0
- /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
|
+
[](https://www.npmjs.com/package/vue-stack-tabs)
|
|
4
|
+
[](https://www.npmjs.com/package/vue-stack-tabs)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://bundlephobia.com/package/vue-stack-tabs)
|
|
7
|
+
[](https://jktantan.github.io/vue-stack-tabs/)
|
|
8
|
+
|
|
3
9
|
[中文](./README.md) | **English** | [Русский](./README.ru.md)
|
|
4
10
|
|
|
5
|
-
|
|
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/
|
|
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-
|
|
153
|
+
locale: 'en' // built-in options: 'zh-CN' | 'en'
|
|
137
154
|
},
|
|
138
|
-
css: ['vue-stack-tabs/dist/
|
|
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/
|
|
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
|
|
248
|
-
|
|
|
249
|
-
| `openTab(tab, renew?)`
|
|
250
|
-
| `closeTab(id)`
|
|
251
|
-
| `closeAllTabs()`
|
|
252
|
-
| `refreshTab(id)`
|
|
253
|
-
| `refreshAllTabs()`
|
|
254
|
-
| `activeTab(id)`
|
|
255
|
-
| `
|
|
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
|
|
270
|
-
|
|
|
271
|
-
| `forward(to)`
|
|
272
|
-
| `backward(to, backQuery?)`
|
|
273
|
-
| `addScrollTarget(selector)`
|
|
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
|
|
337
|
-
| ---------------------- | --------------------------- |
|
|
338
|
-
| `iframePath` | `string` | **Required**
|
|
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[]` | `[]`
|
|
344
|
-
| `max` | `number` | `20`
|
|
345
|
-
| `contextmenu` | `boolean \| object` | `true`
|
|
346
|
-
| `pageTransition` | `string` | `'stack-tab-swap'`
|
|
347
|
-
| `pageTransitionBack` | `string` | `'stack-tab-swap-back'`
|
|
348
|
-
| `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'`
|
|
349
|
-
| `tabScrollMode` | `TabScrollMode` | `'both'`
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
355
|
-
| `
|
|
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
|
|
513
|
+
Built-in languages: `zh-CN` (Chinese), `en` (English).
|
|
455
514
|
|
|
456
515
|
```ts
|
|
457
|
-
// Switch language
|
|
458
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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?:
|
|
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
|
-
[
|
|
593
|
+
[Apache-2.0](./LICENSE)
|
package/README.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# vue-stack-tabs
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/vue-stack-tabs)
|
|
4
|
+
[](https://www.npmjs.com/package/vue-stack-tabs)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://bundlephobia.com/package/vue-stack-tabs)
|
|
7
|
+
[](https://jktantan.github.io/vue-stack-tabs/)
|
|
8
|
+
|
|
3
9
|
**中文** | [English](./README.en.md) | [Русский](./README.ru.md)
|
|
4
10
|
|
|
5
|
-
|
|
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?)`
|
|
259
|
-
| `closeTab(id)`
|
|
260
|
-
| `closeAllTabs()`
|
|
261
|
-
| `refreshTab(id)`
|
|
262
|
-
| `refreshAllTabs()`
|
|
263
|
-
| `activeTab(id)`
|
|
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
|
-
| `
|
|
361
|
-
| `
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
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
|
|
517
|
+
内置语言:`zh-CN`(中文)、`en`(英文)。
|
|
465
518
|
|
|
466
519
|
```ts
|
|
467
|
-
//
|
|
468
|
-
|
|
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
|
-
|
|
479
|
-
|
|
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?:
|
|
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
|
-
[
|
|
597
|
+
[Apache-2.0](./LICENSE)
|