vue-stack-tabs 0.1.51 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +717 -3
- package/dist/index.d.ts +3 -0
- package/dist/src/lib/StackTabs.vue.d.ts +33 -161
- package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +6 -6
- package/dist/src/lib/components/ContextMenu/index.vue.d.ts +14 -14
- package/dist/src/lib/components/PageLoading.vue.d.ts +5 -5
- package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +24 -15
- package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +10 -8
- package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +31 -13
- package/dist/src/lib/components/TabHeader/index.vue.d.ts +172 -20
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +24 -0
- package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +1 -0
- package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -0
- package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -0
- package/dist/src/lib/hooks/tabPanel/state.d.ts +77 -0
- package/dist/src/lib/hooks/useContextMenu.d.ts +6 -1
- package/dist/src/lib/hooks/useTabActions.d.ts +68 -0
- package/dist/src/lib/hooks/{useTabMitt.d.ts → useTabEventBus.d.ts} +4 -3
- package/dist/src/lib/hooks/useTabLoading.d.ts +6 -0
- package/dist/src/lib/hooks/{useTabpanel.d.ts → useTabPanel.d.ts} +20 -14
- package/dist/src/lib/hooks/useTabPanel.spec.d.ts +1 -0
- package/dist/src/lib/hooks/useTabRouter.d.ts +14 -5
- package/dist/src/lib/i18n/index.d.ts +1 -1
- package/dist/src/lib/i18n/lang/en.d.ts +1 -0
- package/dist/src/lib/i18n/lang/zh-CN.d.ts +1 -0
- package/dist/src/lib/index.d.ts +5 -4
- package/dist/src/lib/model/TabModel.d.ts +20 -3
- package/dist/src/lib/model/TabModel.spec.d.ts +1 -0
- package/dist/src/lib/utils/iframeBridge.d.ts +31 -0
- package/dist/src/lib/utils/iframeBridge.spec.d.ts +1 -0
- package/dist/src/lib/utils/scrollUtils.d.ts +28 -0
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +19 -0
- package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +1 -0
- package/dist/src/lib/utils/urlParser.d.ts +19 -0
- package/dist/src/lib/utils/urlParser.spec.d.ts +1 -0
- package/dist/src/lib/versionLogger.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/vue-stack-tabs.css +1 -0
- package/dist/vue-stack-tabs.es.js +1795 -1679
- package/dist/vue-stack-tabs.umd.js +1 -5
- package/package.json +76 -39
- package/src/lib/StackTabs.vue +222 -47
- package/src/lib/assets/image/open.svg +1 -0
- package/src/lib/assets/style/common/var.scss +1 -1
- package/src/lib/assets/style/icon.scss +6 -0
- package/src/lib/assets/style/stackTab.scss +46 -3
- package/src/lib/assets/style/transition.scss +63 -62
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +7 -0
- package/src/lib/components/ContextMenu/index.vue +50 -38
- package/src/lib/components/PageLoading.vue +30 -10
- package/src/lib/components/TabHeader/TabHeaderButton.vue +12 -3
- package/src/lib/components/TabHeader/TabHeaderItem.vue +31 -27
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +83 -77
- package/src/lib/components/TabHeader/index.vue +45 -40
- package/src/lib/hooks/tabPanel/evict.spec.ts +85 -0
- package/src/lib/hooks/tabPanel/evict.ts +57 -0
- package/src/lib/hooks/tabPanel/scroll.ts +52 -0
- package/src/lib/hooks/tabPanel/session.ts +33 -0
- package/src/lib/hooks/tabPanel/state.ts +52 -0
- package/src/lib/hooks/useContextMenu.ts +22 -20
- package/src/lib/hooks/useTabActions.ts +148 -0
- package/src/lib/hooks/useTabEventBus.ts +31 -0
- package/src/lib/hooks/useTabLoading.ts +12 -4
- package/src/lib/hooks/useTabPanel.spec.ts +34 -0
- package/src/lib/hooks/useTabPanel.tsx +546 -0
- package/src/lib/hooks/useTabRouter.ts +114 -103
- package/src/lib/i18n/index.ts +14 -18
- package/src/lib/i18n/lang/en.ts +2 -1
- package/src/lib/i18n/lang/zh-CN.ts +2 -1
- package/src/lib/iframe.vue +6 -0
- package/src/lib/index.ts +27 -8
- package/src/lib/model/TabModel.spec.ts +40 -0
- package/src/lib/model/TabModel.ts +22 -9
- package/src/lib/nuxt/module.d.ts +19 -0
- package/src/lib/nuxt/module.ts +34 -0
- package/src/lib/nuxt/runtime/plugin.client.ts +11 -0
- package/src/lib/utils/iframeBridge.spec.ts +37 -0
- package/src/lib/utils/iframeBridge.ts +52 -0
- package/src/lib/utils/{TabScrollHelper.ts → scrollUtils.ts} +23 -29
- package/src/lib/utils/tabInfoEncoder.spec.ts +42 -0
- package/src/lib/utils/tabInfoEncoder.ts +65 -0
- package/src/lib/utils/urlParser.spec.ts +45 -0
- package/src/lib/utils/urlParser.ts +42 -0
- package/src/lib/{banner.ts → versionLogger.ts} +4 -5
- package/dist/src/lib/banner.d.ts +0 -2
- package/dist/src/lib/hooks/useStackTab.d.ts +0 -11
- package/dist/src/lib/utils/TabIdHelper.d.ts +0 -11
- package/dist/src/lib/utils/TabScrollHelper.d.ts +0 -33
- package/dist/src/lib/utils/UriHelper.d.ts +0 -4
- package/src/lib/hooks/useStackTab.ts +0 -119
- package/src/lib/hooks/useTabMitt.ts +0 -30
- package/src/lib/hooks/useTabpanel.tsx +0 -708
- package/src/lib/utils/TabIdHelper.ts +0 -70
- package/src/lib/utils/UriHelper.ts +0 -22
package/README.md
CHANGED
|
@@ -1,3 +1,717 @@
|
|
|
1
|
-
# Vue Stack Tabs
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Vue Stack Tabs
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/vue-stack-tabs)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A multi-tab management library for Vue 3 and Vue Router. Each tab has its own component stack with isolated scope, suitable for admin dashboards, multi-tab workspaces, and similar scenarios.
|
|
7
|
+
|
|
8
|
+
基于 Vue 3 和 Vue Router 的多标签页管理库。每个标签拥有独立的组件栈,作用域互不干扰,适合后台管理、多页签工作台等场景。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents / 目录
|
|
13
|
+
|
|
14
|
+
- [Features / 特性](#features--特性)
|
|
15
|
+
- [Installation / 安装](#installation--安装)
|
|
16
|
+
- [Quick Start / 快速开始](#quick-start--快速开始)
|
|
17
|
+
- [Props / 属性](#props--属性)
|
|
18
|
+
- [API](#api)
|
|
19
|
+
- [Nuxt](#nuxt)
|
|
20
|
+
- [iframe & PostMessage](#iframe--postmessage)
|
|
21
|
+
- [i18n / 国际化](#i18n--国际化)
|
|
22
|
+
- [Sub-routes / 子路由](#sub-routes-experimental--子路由试验性)
|
|
23
|
+
- [完整中文说明](#完整中文说明)
|
|
24
|
+
- [License / 许可证](#license--许可证)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Features / 特性
|
|
29
|
+
|
|
30
|
+
- **Route-based & iframe tabs** — Tab pages from Vue Router routes or embedded iframes
|
|
31
|
+
- **Tab operations** — Add, close, refresh, batch close (all / left / right / others)
|
|
32
|
+
- **Stack navigation** — Forward / backward within a tab
|
|
33
|
+
- **Scroll position** — Remember and restore scroll per page
|
|
34
|
+
- **Session recovery** — Restore last active tab on refresh
|
|
35
|
+
- **Nuxt 3/4 module** — Official Nuxt integration
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation / 安装
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add vue-stack-tabs
|
|
43
|
+
# or
|
|
44
|
+
npm install vue-stack-tabs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Peer dependencies:** Vue 3, Vue Router 5.x
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Quick Start / 快速开始
|
|
52
|
+
|
|
53
|
+
### Vue
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// main.ts
|
|
57
|
+
import { createApp } from 'vue'
|
|
58
|
+
import VueStackTabs from 'vue-stack-tabs'
|
|
59
|
+
import 'vue-stack-tabs/dist/style.css'
|
|
60
|
+
import App from './App.vue'
|
|
61
|
+
import router from './router'
|
|
62
|
+
|
|
63
|
+
const app = createApp(App)
|
|
64
|
+
app.use(router)
|
|
65
|
+
app.use(VueStackTabs)
|
|
66
|
+
app.mount('#app')
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```vue
|
|
70
|
+
<!-- App.vue or layout -->
|
|
71
|
+
<template>
|
|
72
|
+
<vue-stack-tabs iframe-path="/iframe" />
|
|
73
|
+
</template>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Configure the iframe route to match `iframe-path` (see [Route config](#route-config--路由配置)).
|
|
77
|
+
|
|
78
|
+
### Route Config / 路由配置
|
|
79
|
+
|
|
80
|
+
The parent route must wrap `<vue-stack-tabs>`, and the iframe path route must be registered.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// router.ts
|
|
84
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
85
|
+
import Frame from '@/components/layout/Frame.vue'
|
|
86
|
+
import { IFrame } from 'vue-stack-tabs'
|
|
87
|
+
|
|
88
|
+
const router = createRouter({
|
|
89
|
+
history: createWebHistory(),
|
|
90
|
+
routes: [
|
|
91
|
+
{
|
|
92
|
+
path: '/',
|
|
93
|
+
component: Frame, // contains <vue-stack-tabs>
|
|
94
|
+
children: [
|
|
95
|
+
{ path: '', component: () => import('@/views/home.vue') },
|
|
96
|
+
{ path: 'iframe', component: IFrame }, // must match iframe-path
|
|
97
|
+
{ path: ':pathMatch(.*)', redirect: '/404' }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Props / 属性
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default | Description |
|
|
109
|
+
|------|------|---------|-------------|
|
|
110
|
+
| `defaultTabs` | `ITabData[]` | `[]` | Initial tabs |
|
|
111
|
+
| `max` | `number` | `20` | Max open tabs |
|
|
112
|
+
| `iframePath` | `string` | — | iframe route path **(required)** |
|
|
113
|
+
| `iframeAllowedOrigins` | `string[]` | `[]` | Origins allowed for postMessage `openTab` |
|
|
114
|
+
| `pageTransition` | `string` | `stack-tab-swap` | Page transition name |
|
|
115
|
+
| `tabTransition` | `string` | `stack-tab-zoom` | Tab transition name |
|
|
116
|
+
| `tabScrollMode` | `'wheel' \| 'button' \| 'both'` | `both` | Tab bar scroll mode |
|
|
117
|
+
| `width` / `height` | `string` | `100%` | Component size |
|
|
118
|
+
| `i18n` | `string` | `zh-CN` | Locale (`zh-CN` \| `en`) |
|
|
119
|
+
| `space` | `number` | `300` | Tab scroll step (px) |
|
|
120
|
+
| `globalScroll` | `boolean` | `false` | Use page-level scroll |
|
|
121
|
+
| `contextmenu` | `boolean \| object` | `true` | Right-click menu |
|
|
122
|
+
| `sessionPrefix` | `string` | `''` | Prefix for sessionStorage key |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## API
|
|
127
|
+
|
|
128
|
+
### useTabActions
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
132
|
+
|
|
133
|
+
const {
|
|
134
|
+
openTab,
|
|
135
|
+
closeTab,
|
|
136
|
+
closeAllTabs,
|
|
137
|
+
refreshTab,
|
|
138
|
+
refreshAllTabs,
|
|
139
|
+
activeTab,
|
|
140
|
+
reset,
|
|
141
|
+
tabs,
|
|
142
|
+
openInNewWindow
|
|
143
|
+
} = useTabActions()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Method | Description |
|
|
147
|
+
|--------|-------------|
|
|
148
|
+
| `openTab(tab, renew?)` | Open tab. `renew=true` clears stack and reopens |
|
|
149
|
+
| `closeTab(id)` | Close tab |
|
|
150
|
+
| `closeAllTabs()` | Close all tabs |
|
|
151
|
+
| `refreshTab(id)` | Refresh current page of tab |
|
|
152
|
+
| `refreshAllTabs()` | Refresh all tabs |
|
|
153
|
+
| `activeTab(id, isRoute?)` | Activate tab |
|
|
154
|
+
| `reset()` | Reset all tabs |
|
|
155
|
+
| `openInNewWindow(id)` | Open iframe tab in new window (fallback) |
|
|
156
|
+
|
|
157
|
+
**openTab(tab: ITabData)**
|
|
158
|
+
|
|
159
|
+
| Field | Type | Required | Description |
|
|
160
|
+
|-------|------|----------|-------------|
|
|
161
|
+
| `id` | `string` | No | Tab id (auto-generated if omitted) |
|
|
162
|
+
| `title` | `string` | Yes | Tab title |
|
|
163
|
+
| `path` | `string` | Yes | Route path or iframe URL |
|
|
164
|
+
| `query` | `Record<string, string>` | No | Query params |
|
|
165
|
+
| `iframe` | `boolean` | No | Open as iframe |
|
|
166
|
+
| `iframeRefreshMode` | `'postMessage' \| 'reload'` | No | iframe refresh mode |
|
|
167
|
+
| `closable` | `boolean` | No | Default `true` |
|
|
168
|
+
| `refreshable` | `boolean` | No | Default `true` |
|
|
169
|
+
|
|
170
|
+
### useTabRouter
|
|
171
|
+
|
|
172
|
+
Used **inside tab pages** for in-tab navigation.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { useTabRouter } from 'vue-stack-tabs'
|
|
176
|
+
|
|
177
|
+
const { forward, backward, addScrollTarget } = useTabRouter()
|
|
178
|
+
|
|
179
|
+
// Forward: push new page
|
|
180
|
+
forward({ path: '/detail', query: { id: '1' } })
|
|
181
|
+
|
|
182
|
+
// Backward: pop stack
|
|
183
|
+
backward('/list') // back to first matching path
|
|
184
|
+
backward(1) // back 1 step
|
|
185
|
+
backward(2, { foo: 'bar' }) // back 2 steps, pass query to target
|
|
186
|
+
|
|
187
|
+
// Register scroll containers for position restore
|
|
188
|
+
addScrollTarget('.scroll-area', '#panel')
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### useTabLoading
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { useTabLoading } from 'vue-stack-tabs'
|
|
195
|
+
|
|
196
|
+
const { openTabLoading, closeTabLoading } = useTabLoading()
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Initial Tabs / 初始化页签
|
|
200
|
+
|
|
201
|
+
```vue
|
|
202
|
+
<vue-stack-tabs
|
|
203
|
+
iframe-path="/iframe"
|
|
204
|
+
:default-tabs="[
|
|
205
|
+
{ id: 'home', title: 'Home', path: '/', closable: false },
|
|
206
|
+
{ title: 'About', path: '/about' }
|
|
207
|
+
]"
|
|
208
|
+
/>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> The first tab should be non-closable (`closable: false`).
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Nuxt
|
|
216
|
+
|
|
217
|
+
### Module (recommended)
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
// nuxt.config.ts
|
|
221
|
+
export default defineNuxtConfig({
|
|
222
|
+
modules: ['vue-stack-tabs/nuxt'],
|
|
223
|
+
vueStackTabs: {
|
|
224
|
+
locale: 'zh-CN'
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The module auto-imports `useTabActions`, `useTabRouter`, `useTabLoading`.
|
|
230
|
+
|
|
231
|
+
### iframe route in Nuxt
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// nuxt.config.ts
|
|
235
|
+
export default defineNuxtConfig({
|
|
236
|
+
hooks: {
|
|
237
|
+
'pages:extend'(pages) {
|
|
238
|
+
pages.push({
|
|
239
|
+
name: 'iframe',
|
|
240
|
+
path: '/iframe',
|
|
241
|
+
file: '~/components/IFrame.vue' // minimal component with useTabRouter()
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## iframe & PostMessage
|
|
251
|
+
|
|
252
|
+
### Open tab from iframe
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { postOpenTab } from 'vue-stack-tabs'
|
|
256
|
+
|
|
257
|
+
postOpenTab({
|
|
258
|
+
title: 'New Page',
|
|
259
|
+
path: '/detail',
|
|
260
|
+
query: { id: '123' },
|
|
261
|
+
closable: true,
|
|
262
|
+
refreshable: true
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Parent must allow the iframe origin if cross-origin:
|
|
267
|
+
|
|
268
|
+
```vue
|
|
269
|
+
<vue-stack-tabs
|
|
270
|
+
iframe-path="/iframe"
|
|
271
|
+
:iframe-allowed-origins="['https://iframe-origin.com']"
|
|
272
|
+
/>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Receive refresh in iframe
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { onRefreshRequest } from 'vue-stack-tabs'
|
|
279
|
+
import { onUnmounted } from 'vue'
|
|
280
|
+
|
|
281
|
+
const unlisten = onRefreshRequest() // defaults to location.reload()
|
|
282
|
+
onUnmounted(unlisten)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Cross-origin iframe (reload mode)
|
|
286
|
+
|
|
287
|
+
When the iframe is cross-origin and you cannot modify its code:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
openTab({
|
|
291
|
+
title: 'External',
|
|
292
|
+
path: 'https://example.com',
|
|
293
|
+
iframe: true,
|
|
294
|
+
iframeRefreshMode: 'reload'
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## i18n / 国际化
|
|
301
|
+
|
|
302
|
+
Built-in: `zh-CN`, `en`.
|
|
303
|
+
|
|
304
|
+
```vue
|
|
305
|
+
<vue-stack-tabs iframe-path="/iframe" i18n="zh-CN" />
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Custom messages
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
app.use(VueStackTabs, [{
|
|
312
|
+
locale: 'zh-CN',
|
|
313
|
+
messages: {
|
|
314
|
+
VueStackTab: {
|
|
315
|
+
close: 'Close',
|
|
316
|
+
closeLefts: 'Close left',
|
|
317
|
+
closeRights: 'Close right',
|
|
318
|
+
closeOthers: 'Close others',
|
|
319
|
+
closeAll: 'Close all',
|
|
320
|
+
reload: 'Reload',
|
|
321
|
+
reloadAll: 'Reload all',
|
|
322
|
+
maximum: 'Maximize',
|
|
323
|
+
restore: 'Restore',
|
|
324
|
+
undefined: 'Undefined',
|
|
325
|
+
loading: 'Loading',
|
|
326
|
+
openInNewWindow: 'Open in new window'
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}])
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Slots
|
|
335
|
+
|
|
336
|
+
| Slot | Description |
|
|
337
|
+
|------|-------------|
|
|
338
|
+
| `leftButton` | Left side of tab bar |
|
|
339
|
+
| `rightButton` | Right side of tab bar |
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Events
|
|
344
|
+
|
|
345
|
+
| Event | Payload | Description |
|
|
346
|
+
|-------|---------|-------------|
|
|
347
|
+
| `onActive` | `id: string` | Fired when a tab is activated |
|
|
348
|
+
| `onPageLoaded` | — | Fired when page component is loaded |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Exports
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
import VueStackTabs, {
|
|
356
|
+
useTabActions,
|
|
357
|
+
useTabRouter,
|
|
358
|
+
useTabLoading,
|
|
359
|
+
TabHeaderButton,
|
|
360
|
+
IFrame,
|
|
361
|
+
postOpenTab,
|
|
362
|
+
onRefreshRequest,
|
|
363
|
+
MSG_REFRESH,
|
|
364
|
+
MSG_OPEN_TAB
|
|
365
|
+
} from 'vue-stack-tabs'
|
|
366
|
+
import type { ITabData, ITabBase, IframeOpenTabPayload } from 'vue-stack-tabs'
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Sub-routes (Experimental / 子路由试验性)
|
|
372
|
+
|
|
373
|
+
Nested routes with `children` and `forward`/`backward` are supported. See [SUBROUTE_EXPERIMENTAL.md](./docs/SUBROUTE_EXPERIMENTAL.md) for details. This feature is experimental.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Development / 开发
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
pnpm install
|
|
381
|
+
pnpm run dev # Demo app
|
|
382
|
+
pnpm run test # Unit tests
|
|
383
|
+
pnpm run test:prepack # Pre-pack: unit + Vue + Nuxt (source)
|
|
384
|
+
pnpm run test:packaged # Post-pack: Vue + Nuxt (dist)
|
|
385
|
+
pnpm run docs:dev # Docs preview
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). [AGENTS.md](./AGENTS.md) for AI assistants.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## 完整中文说明
|
|
393
|
+
|
|
394
|
+
### 安装与引入
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
// main.ts
|
|
398
|
+
import { createApp } from 'vue'
|
|
399
|
+
import VueStackTabs from 'vue-stack-tabs'
|
|
400
|
+
import 'vue-stack-tabs/dist/style.css'
|
|
401
|
+
import App from './App.vue'
|
|
402
|
+
import router from './router'
|
|
403
|
+
|
|
404
|
+
const app = createApp(App)
|
|
405
|
+
app.use(router)
|
|
406
|
+
app.use(VueStackTabs)
|
|
407
|
+
app.mount('#app')
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**依赖:** Vue 3、Vue Router 5.x
|
|
411
|
+
|
|
412
|
+
### 快速开始
|
|
413
|
+
|
|
414
|
+
```vue
|
|
415
|
+
<!-- App.vue 或布局组件 -->
|
|
416
|
+
<template>
|
|
417
|
+
<vue-stack-tabs iframe-path="/iframe" />
|
|
418
|
+
</template>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
父路由需包裹 `<vue-stack-tabs>`,并注册与 `iframe-path` 一致的 iframe 路由。
|
|
422
|
+
|
|
423
|
+
### 路由配置
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
// router.ts
|
|
427
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
428
|
+
import Frame from '@/components/layout/Frame.vue'
|
|
429
|
+
import { IFrame } from 'vue-stack-tabs'
|
|
430
|
+
|
|
431
|
+
const router = createRouter({
|
|
432
|
+
history: createWebHistory(),
|
|
433
|
+
routes: [
|
|
434
|
+
{
|
|
435
|
+
path: '/',
|
|
436
|
+
component: Frame, // 内部包含 <vue-stack-tabs>
|
|
437
|
+
children: [
|
|
438
|
+
{ path: '', component: () => import('@/views/home.vue') },
|
|
439
|
+
{ path: 'iframe', component: IFrame }, // 需与 iframe-path 一致
|
|
440
|
+
{ path: ':pathMatch(.*)', redirect: '/404' }
|
|
441
|
+
]
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### 属性
|
|
448
|
+
|
|
449
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
450
|
+
|------|------|--------|------|
|
|
451
|
+
| `defaultTabs` | `ITabData[]` | `[]` | 初始页签 |
|
|
452
|
+
| `max` | `number` | `20` | 最大打开数量 |
|
|
453
|
+
| `iframePath` | `string` | — | iframe 路由路径 **(必填)** |
|
|
454
|
+
| `iframeAllowedOrigins` | `string[]` | `[]` | 允许 postMessage 调用 openTab 的 iframe 来源 |
|
|
455
|
+
| `pageTransition` | `string` | `stack-tab-swap` | 页面转场效果 |
|
|
456
|
+
| `tabTransition` | `string` | `stack-tab-zoom` | 标签转场效果 |
|
|
457
|
+
| `tabScrollMode` | `'wheel' \| 'button' \| 'both'` | `both` | 标签栏滚动方式 |
|
|
458
|
+
| `width` / `height` | `string` | `100%` | 组件尺寸 |
|
|
459
|
+
| `i18n` | `string` | `zh-CN` | 语言(`zh-CN` \| `en`) |
|
|
460
|
+
| `space` | `number` | `300` | 标签滚动步长(px) |
|
|
461
|
+
| `globalScroll` | `boolean` | `false` | 是否使用页面级滚动 |
|
|
462
|
+
| `contextmenu` | `boolean \| object` | `true` | 右键菜单 |
|
|
463
|
+
| `sessionPrefix` | `string` | `''` | sessionStorage key 前缀 |
|
|
464
|
+
|
|
465
|
+
### API
|
|
466
|
+
|
|
467
|
+
#### useTabActions
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
471
|
+
|
|
472
|
+
const {
|
|
473
|
+
openTab,
|
|
474
|
+
closeTab,
|
|
475
|
+
closeAllTabs,
|
|
476
|
+
refreshTab,
|
|
477
|
+
refreshAllTabs,
|
|
478
|
+
activeTab,
|
|
479
|
+
reset,
|
|
480
|
+
tabs,
|
|
481
|
+
openInNewWindow
|
|
482
|
+
} = useTabActions()
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
| 方法 | 说明 |
|
|
486
|
+
|------|------|
|
|
487
|
+
| `openTab(tab, renew?)` | 打开标签;`renew=true` 时清空栈并重新打开 |
|
|
488
|
+
| `closeTab(id)` | 关闭指定标签 |
|
|
489
|
+
| `closeAllTabs()` | 关闭所有标签 |
|
|
490
|
+
| `refreshTab(id)` | 刷新指定标签当前页 |
|
|
491
|
+
| `refreshAllTabs()` | 刷新所有标签 |
|
|
492
|
+
| `activeTab(id, isRoute?)` | 激活指定标签 |
|
|
493
|
+
| `reset()` | 重置所有标签 |
|
|
494
|
+
| `openInNewWindow(id)` | iframe 标签在新窗口打开(无法嵌入时降级) |
|
|
495
|
+
|
|
496
|
+
**openTab(tab: ITabData)**
|
|
497
|
+
|
|
498
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
499
|
+
|------|------|------|------|
|
|
500
|
+
| `id` | `string` | 否 | 标签 id,不填则自动生成 |
|
|
501
|
+
| `title` | `string` | 是 | 标签名称 |
|
|
502
|
+
| `path` | `string` | 是 | 路由路径或 iframe URL |
|
|
503
|
+
| `query` | `Record<string, string>` | 否 | 查询参数 |
|
|
504
|
+
| `iframe` | `boolean` | 否 | 是否以 iframe 打开 |
|
|
505
|
+
| `iframeRefreshMode` | `'postMessage' \| 'reload'` | 否 | iframe 刷新方式 |
|
|
506
|
+
| `closable` | `boolean` | 否 | 默认 `true` |
|
|
507
|
+
| `refreshable` | `boolean` | 否 | 默认 `true` |
|
|
508
|
+
|
|
509
|
+
#### useTabRouter
|
|
510
|
+
|
|
511
|
+
在**标签内的页面**中使用,用于标签内前进/后退。
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
import { useTabRouter } from 'vue-stack-tabs'
|
|
515
|
+
|
|
516
|
+
const { forward, backward, addScrollTarget } = useTabRouter()
|
|
517
|
+
|
|
518
|
+
// 前进:入栈新页面
|
|
519
|
+
forward({ path: '/detail', query: { id: '1' } })
|
|
520
|
+
|
|
521
|
+
// 后退:出栈
|
|
522
|
+
backward('/list') // 回退到首个匹配路径
|
|
523
|
+
backward(1) // 回退 1 步
|
|
524
|
+
backward(2, { foo: 'bar' }) // 回退 2 步,并向目标页传递 query
|
|
525
|
+
|
|
526
|
+
// 注册滚动容器(用于位置记忆与恢复)
|
|
527
|
+
addScrollTarget('.scroll-area', '#panel')
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
#### useTabLoading
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
import { useTabLoading } from 'vue-stack-tabs'
|
|
534
|
+
|
|
535
|
+
const { openTabLoading, closeTabLoading } = useTabLoading()
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### 初始化页签
|
|
539
|
+
|
|
540
|
+
```vue
|
|
541
|
+
<vue-stack-tabs
|
|
542
|
+
iframe-path="/iframe"
|
|
543
|
+
:default-tabs="[
|
|
544
|
+
{ id: 'home', title: '首页', path: '/', closable: false },
|
|
545
|
+
{ title: '关于', path: '/about' }
|
|
546
|
+
]"
|
|
547
|
+
/>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
> 第一个页签建议设为不可关闭(`closable: false`)。
|
|
551
|
+
|
|
552
|
+
### Nuxt
|
|
553
|
+
|
|
554
|
+
#### 模块方式(推荐)
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
// nuxt.config.ts
|
|
558
|
+
export default defineNuxtConfig({
|
|
559
|
+
modules: ['vue-stack-tabs/nuxt'],
|
|
560
|
+
vueStackTabs: {
|
|
561
|
+
locale: 'zh-CN'
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
模块会自动导入 `useTabActions`、`useTabRouter`、`useTabLoading`。
|
|
567
|
+
|
|
568
|
+
#### Nuxt 中配置 iframe 路由
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
// nuxt.config.ts
|
|
572
|
+
export default defineNuxtConfig({
|
|
573
|
+
hooks: {
|
|
574
|
+
'pages:extend'(pages) {
|
|
575
|
+
pages.push({
|
|
576
|
+
name: 'iframe',
|
|
577
|
+
path: '/iframe',
|
|
578
|
+
file: '~/components/IFrame.vue' // 最小组件,含 useTabRouter() 即可
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### iframe 与父窗口通信
|
|
586
|
+
|
|
587
|
+
#### iframe 内打开新标签
|
|
588
|
+
|
|
589
|
+
```ts
|
|
590
|
+
import { postOpenTab } from 'vue-stack-tabs'
|
|
591
|
+
|
|
592
|
+
postOpenTab({
|
|
593
|
+
title: '新页面',
|
|
594
|
+
path: '/detail',
|
|
595
|
+
query: { id: '123' },
|
|
596
|
+
closable: true,
|
|
597
|
+
refreshable: true
|
|
598
|
+
})
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
跨域时父应用需配置 `iframeAllowedOrigins`:
|
|
602
|
+
|
|
603
|
+
```vue
|
|
604
|
+
<vue-stack-tabs
|
|
605
|
+
iframe-path="/iframe"
|
|
606
|
+
:iframe-allowed-origins="['https://iframe来源域名']"
|
|
607
|
+
/>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
#### iframe 内接收刷新指令
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
import { onRefreshRequest } from 'vue-stack-tabs'
|
|
614
|
+
import { onUnmounted } from 'vue'
|
|
615
|
+
|
|
616
|
+
const unlisten = onRefreshRequest() // 默认执行 location.reload()
|
|
617
|
+
onUnmounted(unlisten)
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
#### 跨域 iframe(reload 模式)
|
|
621
|
+
|
|
622
|
+
若 iframe 为跨域且无法修改其代码,可指定 `iframeRefreshMode: 'reload'`,刷新时直接重载 iframe:
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
openTab({
|
|
626
|
+
title: '外部页面',
|
|
627
|
+
path: 'https://example.com',
|
|
628
|
+
iframe: true,
|
|
629
|
+
iframeRefreshMode: 'reload'
|
|
630
|
+
})
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### 国际化
|
|
634
|
+
|
|
635
|
+
内置 `zh-CN`、`en`。
|
|
636
|
+
|
|
637
|
+
```vue
|
|
638
|
+
<vue-stack-tabs iframe-path="/iframe" i18n="zh-CN" />
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
#### 自定义文案
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
app.use(VueStackTabs, [{
|
|
645
|
+
locale: 'zh-CN',
|
|
646
|
+
messages: {
|
|
647
|
+
VueStackTab: {
|
|
648
|
+
close: '关闭',
|
|
649
|
+
closeLefts: '关闭左侧',
|
|
650
|
+
closeRights: '关闭右侧',
|
|
651
|
+
closeOthers: '关闭其他',
|
|
652
|
+
closeAll: '关闭全部',
|
|
653
|
+
reload: '刷新',
|
|
654
|
+
reloadAll: '刷新全部',
|
|
655
|
+
maximum: '最大化',
|
|
656
|
+
restore: '还原',
|
|
657
|
+
undefined: '未定义',
|
|
658
|
+
loading: '加载中',
|
|
659
|
+
openInNewWindow: '新窗口打开'
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}])
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 插槽
|
|
666
|
+
|
|
667
|
+
| 插槽 | 说明 |
|
|
668
|
+
|------|------|
|
|
669
|
+
| `leftButton` | 标签栏左侧 |
|
|
670
|
+
| `rightButton` | 标签栏右侧 |
|
|
671
|
+
|
|
672
|
+
### 事件
|
|
673
|
+
|
|
674
|
+
| 事件 | 参数 | 说明 |
|
|
675
|
+
|------|------|------|
|
|
676
|
+
| `onActive` | `id: string` | 标签被激活时触发 |
|
|
677
|
+
| `onPageLoaded` | — | 页面组件加载完成时触发 |
|
|
678
|
+
|
|
679
|
+
### 导出
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
import VueStackTabs, {
|
|
683
|
+
useTabActions,
|
|
684
|
+
useTabRouter,
|
|
685
|
+
useTabLoading,
|
|
686
|
+
TabHeaderButton,
|
|
687
|
+
IFrame,
|
|
688
|
+
postOpenTab,
|
|
689
|
+
onRefreshRequest,
|
|
690
|
+
MSG_REFRESH,
|
|
691
|
+
MSG_OPEN_TAB
|
|
692
|
+
} from 'vue-stack-tabs'
|
|
693
|
+
import type { ITabData, ITabBase, IframeOpenTabPayload } from 'vue-stack-tabs'
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### 子路由(试验性)
|
|
697
|
+
|
|
698
|
+
支持路由 `children` 与 `forward`/`backward` 多级导航。详见 [SUBROUTE_EXPERIMENTAL.md](./docs/SUBROUTE_EXPERIMENTAL.md)。该功能为试验性。
|
|
699
|
+
|
|
700
|
+
### 开发
|
|
701
|
+
|
|
702
|
+
```bash
|
|
703
|
+
pnpm install
|
|
704
|
+
pnpm run dev # 演示应用
|
|
705
|
+
pnpm run test # 单元测试
|
|
706
|
+
pnpm run test:prepack # 打包前:单元 + Vue + Nuxt(源码)
|
|
707
|
+
pnpm run test:packaged # 打包后:Vue + Nuxt(dist)
|
|
708
|
+
pnpm run docs:dev # 文档预览
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。[AGENTS.md](./AGENTS.md) 供 AI 助手参考。
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## License / 许可证
|
|
716
|
+
|
|
717
|
+
Apache License 2.0
|