vue-stack-tabs 1.0.0 → 1.1.0
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 +255 -538
- package/dist/iframe-child.html +75 -0
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +45 -7
- package/dist/src/lib/hooks/tabPanel/state.d.ts +2 -3
- package/dist/src/lib/hooks/useTabActions.d.ts +2 -2
- package/dist/src/lib/hooks/useTabPanel.d.ts +6 -4
- package/dist/src/lib/hooks/useTabRouter.d.ts +5 -5
- package/dist/src/lib/model/TabModel.d.ts +13 -3
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +3 -3
- package/dist/style.css +1 -1
- package/dist/vue-stack-tabs.css +1 -1
- package/dist/vue-stack-tabs.es.js +1290 -1481
- package/dist/vue-stack-tabs.umd.js +1 -1
- package/package.json +1 -1
- package/src/lib/StackTabs.vue +17 -20
- package/src/lib/assets/style/stackTab.scss +2 -0
- package/src/lib/hooks/tabPanel/evict.ts +61 -14
- package/src/lib/hooks/tabPanel/scroll.ts +6 -2
- package/src/lib/hooks/tabPanel/state.ts +1 -1
- package/src/lib/hooks/useTabPanel.tsx +310 -61
- package/src/lib/hooks/useTabRouter.ts +146 -31
- package/src/lib/model/TabModel.ts +18 -15
- package/src/lib/nuxt/runtime/plugin.client.ts +1 -3
- package/src/lib/utils/tabInfoEncoder.spec.ts +5 -15
- package/src/lib/utils/tabInfoEncoder.ts +7 -29
package/README.md
CHANGED
|
@@ -1,62 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vue-stack-tabs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](LICENSE)
|
|
3
|
+
> Vue 3 多标签页管理库,基于 Vue Router。用 Vue 作用域实现类似 iframe TabPanel 的效果——**每个 Tab 间组件作用域互不干扰**。
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
## ✨ 特性
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
- 🗂️ **路由级标签页** — 每个标签拥有独立的组件和缓存
|
|
8
|
+
- 📚 **栈式页内导航** — 标签内支持 forward / backward,类似浏览器历史栈
|
|
9
|
+
- 🔄 **标签刷新** — 单个刷新或全部刷新,完全重建组件实例
|
|
10
|
+
- 🌐 **iframe 标签** — 支持嵌入 iframe 页面,postMessage 通信
|
|
11
|
+
- 💾 **Session 持久化** — 浏览器刷新后恢复上次激活的标签
|
|
12
|
+
- 📜 **滚动位置记忆** — 切换标签后自动恢复上次滚动位置
|
|
13
|
+
- 🎨 **右键菜单** — 内置关闭/刷新操作
|
|
14
|
+
- 🌍 **国际化** — 内置中英文,可扩展
|
|
15
|
+
- 📦 **Nuxt 3/4 模块** — 开箱即用
|
|
9
16
|
|
|
10
17
|
---
|
|
11
18
|
|
|
12
|
-
##
|
|
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 / 安装
|
|
19
|
+
## 📦 安装
|
|
40
20
|
|
|
41
21
|
```bash
|
|
42
|
-
|
|
43
|
-
# or
|
|
22
|
+
# npm
|
|
44
23
|
npm install vue-stack-tabs
|
|
45
|
-
```
|
|
46
24
|
|
|
47
|
-
|
|
25
|
+
# pnpm
|
|
26
|
+
pnpm add vue-stack-tabs
|
|
27
|
+
```
|
|
48
28
|
|
|
49
29
|
---
|
|
50
30
|
|
|
51
|
-
##
|
|
31
|
+
## 🚀 快速接入(Vue 3)
|
|
52
32
|
|
|
53
|
-
###
|
|
33
|
+
### 1. 注册插件
|
|
54
34
|
|
|
55
35
|
```ts
|
|
56
36
|
// main.ts
|
|
57
37
|
import { createApp } from 'vue'
|
|
58
38
|
import VueStackTabs from 'vue-stack-tabs'
|
|
59
|
-
import 'vue-stack-tabs/dist/
|
|
39
|
+
import 'vue-stack-tabs/dist/vue-stack-tabs.css'
|
|
60
40
|
import App from './App.vue'
|
|
61
41
|
import router from './router'
|
|
62
42
|
|
|
@@ -66,155 +46,85 @@ app.use(VueStackTabs)
|
|
|
66
46
|
app.mount('#app')
|
|
67
47
|
```
|
|
68
48
|
|
|
69
|
-
|
|
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 / 路由配置
|
|
49
|
+
### 2. 配置路由
|
|
79
50
|
|
|
80
|
-
|
|
51
|
+
标签页依赖 Vue Router,需要一个承载 `<VueStackTabs>` 的父路由:
|
|
81
52
|
|
|
82
53
|
```ts
|
|
83
54
|
// router.ts
|
|
84
55
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
85
|
-
import Frame from '@/components/layout/Frame.vue'
|
|
86
|
-
import { IFrame } from 'vue-stack-tabs'
|
|
87
56
|
|
|
88
57
|
const router = createRouter({
|
|
89
58
|
history: createWebHistory(),
|
|
90
59
|
routes: [
|
|
91
60
|
{
|
|
92
61
|
path: '/',
|
|
93
|
-
component:
|
|
62
|
+
component: () => import('./Layout.vue'),
|
|
94
63
|
children: [
|
|
95
|
-
{ path: '', component: () => import('
|
|
96
|
-
{ path: '
|
|
97
|
-
{ path: '
|
|
64
|
+
{ path: '', component: () => import('./pages/Home.vue') },
|
|
65
|
+
{ path: 'about', component: () => import('./pages/About.vue') },
|
|
66
|
+
{ path: 'settings', component: () => import('./pages/Settings.vue') },
|
|
67
|
+
// iframe 占位路由(必须)
|
|
68
|
+
{ path: 'iframe', component: () => import('vue-stack-tabs').then((m) => m.IFrame) }
|
|
98
69
|
]
|
|
99
70
|
}
|
|
100
71
|
]
|
|
101
72
|
})
|
|
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
73
|
|
|
130
|
-
|
|
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()
|
|
74
|
+
export default router
|
|
144
75
|
```
|
|
145
76
|
|
|
146
|
-
|
|
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'
|
|
77
|
+
### 3. 使用组件
|
|
176
78
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
79
|
+
```vue
|
|
80
|
+
<!-- Layout.vue -->
|
|
81
|
+
<template>
|
|
82
|
+
<div style="width: 100%; height: 100vh">
|
|
83
|
+
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" :max="20" :contextmenu="true" />
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
181
86
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
backward(1) // back 1 step
|
|
185
|
-
backward(2, { foo: 'bar' }) // back 2 steps, pass query to target
|
|
87
|
+
<script setup lang="ts">
|
|
88
|
+
import type { ITabData } from 'vue-stack-tabs'
|
|
186
89
|
|
|
187
|
-
|
|
188
|
-
|
|
90
|
+
const defaultTabs: ITabData[] = [
|
|
91
|
+
{
|
|
92
|
+
title: '首页',
|
|
93
|
+
path: '/',
|
|
94
|
+
closable: false,
|
|
95
|
+
refreshable: true
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
</script>
|
|
189
99
|
```
|
|
190
100
|
|
|
191
|
-
###
|
|
101
|
+
### 4. 打开标签
|
|
192
102
|
|
|
193
103
|
```ts
|
|
194
|
-
import {
|
|
104
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
195
105
|
|
|
196
|
-
const {
|
|
197
|
-
|
|
106
|
+
const { openTab, closeTab, refreshTab } = useTabActions()
|
|
107
|
+
|
|
108
|
+
// 打开新标签
|
|
109
|
+
openTab({
|
|
110
|
+
id: 'about', // 可选,不传则自动生成
|
|
111
|
+
title: '关于',
|
|
112
|
+
path: '/about',
|
|
113
|
+
query: { id: '1' } // 可选
|
|
114
|
+
})
|
|
198
115
|
|
|
199
|
-
|
|
116
|
+
// 关闭标签
|
|
117
|
+
closeTab('about')
|
|
200
118
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
iframe-path="/iframe"
|
|
204
|
-
:default-tabs="[
|
|
205
|
-
{ id: 'home', title: 'Home', path: '/', closable: false },
|
|
206
|
-
{ title: 'About', path: '/about' }
|
|
207
|
-
]"
|
|
208
|
-
/>
|
|
119
|
+
// 刷新标签
|
|
120
|
+
refreshTab('about')
|
|
209
121
|
```
|
|
210
122
|
|
|
211
|
-
> The first tab should be non-closable (`closable: false`).
|
|
212
|
-
|
|
213
123
|
---
|
|
214
124
|
|
|
215
|
-
## Nuxt
|
|
125
|
+
## 🚀 快速接入(Nuxt 3/4)
|
|
216
126
|
|
|
217
|
-
###
|
|
127
|
+
### 1. 注册模块
|
|
218
128
|
|
|
219
129
|
```ts
|
|
220
130
|
// nuxt.config.ts
|
|
@@ -222,496 +132,303 @@ export default defineNuxtConfig({
|
|
|
222
132
|
modules: ['vue-stack-tabs/nuxt'],
|
|
223
133
|
vueStackTabs: {
|
|
224
134
|
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
|
-
}
|
|
135
|
+
},
|
|
136
|
+
css: ['vue-stack-tabs/dist/vue-stack-tabs.css']
|
|
245
137
|
})
|
|
246
138
|
```
|
|
247
139
|
|
|
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:
|
|
140
|
+
### 2. 在 Layout 中使用
|
|
267
141
|
|
|
268
142
|
```vue
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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)
|
|
143
|
+
<!-- layouts/default.vue -->
|
|
144
|
+
<template>
|
|
145
|
+
<div style="width: 100%; height: 100vh">
|
|
146
|
+
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" />
|
|
147
|
+
</div>
|
|
148
|
+
</template>
|
|
286
149
|
|
|
287
|
-
|
|
150
|
+
<script setup lang="ts">
|
|
151
|
+
import type { ITabData } from 'vue-stack-tabs'
|
|
288
152
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
title: 'External',
|
|
292
|
-
path: 'https://example.com',
|
|
293
|
-
iframe: true,
|
|
294
|
-
iframeRefreshMode: 'reload'
|
|
295
|
-
})
|
|
153
|
+
const defaultTabs: ITabData[] = [{ title: '首页', path: '/', closable: false, refreshable: true }]
|
|
154
|
+
</script>
|
|
296
155
|
```
|
|
297
156
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
## i18n / 国际化
|
|
301
|
-
|
|
302
|
-
Built-in: `zh-CN`, `en`.
|
|
157
|
+
### 3. 页面中使用
|
|
303
158
|
|
|
304
159
|
```vue
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
160
|
+
<!-- pages/index.vue -->
|
|
161
|
+
<template>
|
|
162
|
+
<div>
|
|
163
|
+
<h1>首页</h1>
|
|
164
|
+
<button @click="openTab({ title: '关于', path: '/about' })">打开关于页</button>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
309
167
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
}])
|
|
168
|
+
<script setup>
|
|
169
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
170
|
+
const { openTab } = useTabActions()
|
|
171
|
+
</script>
|
|
330
172
|
```
|
|
331
173
|
|
|
332
174
|
---
|
|
333
175
|
|
|
334
|
-
##
|
|
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 |
|
|
176
|
+
## 📖 API 参考
|
|
349
177
|
|
|
350
|
-
|
|
178
|
+
### useTabActions
|
|
351
179
|
|
|
352
|
-
|
|
180
|
+
标签级操作封装。
|
|
353
181
|
|
|
354
182
|
```ts
|
|
355
|
-
import
|
|
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 / 开发
|
|
183
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
378
184
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
185
|
+
const {
|
|
186
|
+
openTab, // (tab: ITabData, renew?: boolean) => Promise<string>
|
|
187
|
+
closeTab, // (id: string) => string
|
|
188
|
+
closeAllTabs, // () => void
|
|
189
|
+
refreshTab, // (id: string) => void
|
|
190
|
+
refreshAllTabs, // () => void
|
|
191
|
+
activeTab, // (id: string, isRoute?: boolean) => void
|
|
192
|
+
reset, // () => void
|
|
193
|
+
tabs // Ref<ITabItem[]>
|
|
194
|
+
} = useTabActions()
|
|
386
195
|
```
|
|
387
196
|
|
|
388
|
-
|
|
197
|
+
| 方法 | 说明 |
|
|
198
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
199
|
+
| `openTab(tab, renew?)` | 打开新标签。`renew=true` 时若已存在则清空页面栈后重新打开 |
|
|
200
|
+
| `closeTab(id)` | 关闭指定标签,返回新激活的标签 ID |
|
|
201
|
+
| `closeAllTabs()` | 关闭所有可关闭标签 |
|
|
202
|
+
| `refreshTab(id)` | 刷新指定标签(替换 ULID,重建组件实例) |
|
|
203
|
+
| `refreshAllTabs()` | 刷新所有标签 |
|
|
204
|
+
| `activeTab(id)` | 激活指定标签(切换 Tab) |
|
|
205
|
+
| `reset()` | 关闭所有标签并重置状态 |
|
|
389
206
|
|
|
390
207
|
---
|
|
391
208
|
|
|
392
|
-
|
|
209
|
+
### useTabRouter
|
|
393
210
|
|
|
394
|
-
|
|
211
|
+
标签内栈式导航,**只能在标签内的页面组件中使用**。
|
|
395
212
|
|
|
396
213
|
```ts
|
|
397
|
-
|
|
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
|
-
### 快速开始
|
|
214
|
+
import { useTabRouter } from 'vue-stack-tabs'
|
|
413
215
|
|
|
414
|
-
|
|
415
|
-
<!-- App.vue 或布局组件 -->
|
|
416
|
-
<template>
|
|
417
|
-
<vue-stack-tabs iframe-path="/iframe" />
|
|
418
|
-
</template>
|
|
216
|
+
const { forward, backward, addScrollTarget } = useTabRouter()
|
|
419
217
|
```
|
|
420
218
|
|
|
421
|
-
|
|
219
|
+
| 方法 | 说明 |
|
|
220
|
+
| --------------------------- | --------------------------------- |
|
|
221
|
+
| `forward(to)` | 在当前标签内前进到新页面 |
|
|
222
|
+
| `backward(to, backQuery?)` | 在当前标签内后退 |
|
|
223
|
+
| `addScrollTarget(selector)` | 注册需要记忆滚动位置的 DOM 选择器 |
|
|
422
224
|
|
|
423
|
-
|
|
225
|
+
#### forward
|
|
424
226
|
|
|
425
227
|
```ts
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
import Frame from '@/components/layout/Frame.vue'
|
|
429
|
-
import { IFrame } from 'vue-stack-tabs'
|
|
228
|
+
// 前进到 /detail 页面
|
|
229
|
+
forward({ path: '/detail', query: { id: '123' } })
|
|
430
230
|
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
})
|
|
231
|
+
// 循环压栈自身(同路由可多次入栈,每次创建独立缓存)
|
|
232
|
+
forward({ path: '/list', query: { page: '2' } })
|
|
445
233
|
```
|
|
446
234
|
|
|
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
|
|
235
|
+
#### backward
|
|
468
236
|
|
|
469
237
|
```ts
|
|
470
|
-
|
|
238
|
+
// 后退 1 步(默认)
|
|
239
|
+
backward(1)
|
|
471
240
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
closeTab,
|
|
475
|
-
closeAllTabs,
|
|
476
|
-
refreshTab,
|
|
477
|
-
refreshAllTabs,
|
|
478
|
-
activeTab,
|
|
479
|
-
reset,
|
|
480
|
-
tabs,
|
|
481
|
-
openInNewWindow
|
|
482
|
-
} = useTabActions()
|
|
483
|
-
```
|
|
241
|
+
// 后退 N 步
|
|
242
|
+
backward(3)
|
|
484
243
|
|
|
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
|
-
在**标签内的页面**中使用,用于标签内前进/后退。
|
|
244
|
+
// 回退到指定路径(自动从栈中查找)
|
|
245
|
+
backward('/list')
|
|
512
246
|
|
|
513
|
-
|
|
514
|
-
|
|
247
|
+
// 带参回退(目标页通过 props._back 接收)
|
|
248
|
+
backward('/list', { result: 'success', data: { id: 1 } })
|
|
249
|
+
```
|
|
515
250
|
|
|
516
|
-
|
|
251
|
+
**目标页接收参数:**
|
|
517
252
|
|
|
518
|
-
|
|
519
|
-
|
|
253
|
+
```vue
|
|
254
|
+
<script setup>
|
|
255
|
+
// 声明 props 接收后退参数
|
|
256
|
+
defineProps<{ _back?: { result: string; data: any } }>()
|
|
257
|
+
</script>
|
|
258
|
+
```
|
|
520
259
|
|
|
521
|
-
|
|
522
|
-
backward('/list') // 回退到首个匹配路径
|
|
523
|
-
backward(1) // 回退 1 步
|
|
524
|
-
backward(2, { foo: 'bar' }) // 回退 2 步,并向目标页传递 query
|
|
260
|
+
---
|
|
525
261
|
|
|
526
|
-
|
|
527
|
-
addScrollTarget('.scroll-area', '#panel')
|
|
528
|
-
```
|
|
262
|
+
### useTabLoading
|
|
529
263
|
|
|
530
|
-
|
|
264
|
+
页面加载状态控制,**只能在标签内的页面组件中使用**。
|
|
531
265
|
|
|
532
266
|
```ts
|
|
533
267
|
import { useTabLoading } from 'vue-stack-tabs'
|
|
534
268
|
|
|
535
269
|
const { openTabLoading, closeTabLoading } = useTabLoading()
|
|
536
|
-
```
|
|
537
270
|
|
|
538
|
-
|
|
271
|
+
// 显示加载遮罩
|
|
272
|
+
openTabLoading()
|
|
539
273
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
iframe-path="/iframe"
|
|
543
|
-
:default-tabs="[
|
|
544
|
-
{ id: 'home', title: '首页', path: '/', closable: false },
|
|
545
|
-
{ title: '关于', path: '/about' }
|
|
546
|
-
]"
|
|
547
|
-
/>
|
|
274
|
+
// 异步操作完成后关闭
|
|
275
|
+
fetchData().finally(() => closeTabLoading())
|
|
548
276
|
```
|
|
549
277
|
|
|
550
|
-
>
|
|
278
|
+
> 组件卸载时自动关闭 Loading,无需手动清理。
|
|
551
279
|
|
|
552
|
-
|
|
280
|
+
---
|
|
553
281
|
|
|
554
|
-
|
|
282
|
+
## ⚙️ Props
|
|
283
|
+
|
|
284
|
+
`<VueStackTabs>` 组件属性:
|
|
285
|
+
|
|
286
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
287
|
+
| ---------------------- | --------------------------- | ----------------------- | ------------------------------------------------- |
|
|
288
|
+
| `iframePath` | `string` | **必填** | iframe 占位路由的路径 |
|
|
289
|
+
| `defaultTabs` | `ITabData[]` | `[]` | 初始标签列表 |
|
|
290
|
+
| `max` | `number` | `20` | 最大标签数量 |
|
|
291
|
+
| `contextmenu` | `boolean \| object` | `true` | 是否启用右键菜单 |
|
|
292
|
+
| `pageTransition` | `string` | `'stack-tab-swap'` | 前进时的页面转场动画名 |
|
|
293
|
+
| `pageTransitionBack` | `string` | `'stack-tab-swap-back'` | 后退时的页面转场动画名 |
|
|
294
|
+
| `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'` | 标签增删时的过渡效果 |
|
|
295
|
+
| `tabScrollMode` | `TabScrollMode` | `'both'` | 标签栏滚动方式:`'wheel'` / `'button'` / `'both'` |
|
|
296
|
+
| `width` | `string` | `'100%'` | 容器宽度 |
|
|
297
|
+
| `height` | `string` | `'100%'` | 容器高度 |
|
|
298
|
+
| `i18n` | `string` | `'zh-CN'` | 国际化语言 |
|
|
299
|
+
| `globalScroll` | `boolean` | `false` | 是否使用页面级滚动记忆 |
|
|
300
|
+
| `sessionPrefix` | `string` | `''` | sessionStorage 键前缀 |
|
|
301
|
+
| `iframeAllowedOrigins` | `string[]` | 同源 | 允许 iframe postMessage 的来源列表 |
|
|
555
302
|
|
|
556
|
-
|
|
557
|
-
// nuxt.config.ts
|
|
558
|
-
export default defineNuxtConfig({
|
|
559
|
-
modules: ['vue-stack-tabs/nuxt'],
|
|
560
|
-
vueStackTabs: {
|
|
561
|
-
locale: 'zh-CN'
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
```
|
|
303
|
+
---
|
|
565
304
|
|
|
566
|
-
|
|
305
|
+
## 🎯 Events
|
|
567
306
|
|
|
568
|
-
|
|
307
|
+
| 事件 | 参数 | 说明 |
|
|
308
|
+
| -------------- | -------------- | ---------------------- |
|
|
309
|
+
| `onActive` | `(id: string)` | 标签被激活时触发 |
|
|
310
|
+
| `onPageLoaded` | — | 页面组件加载完成时触发 |
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 🖼️ iframe 标签页
|
|
315
|
+
|
|
316
|
+
### 打开 iframe 标签
|
|
569
317
|
|
|
570
318
|
```ts
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
pages.push({
|
|
576
|
-
name: 'iframe',
|
|
577
|
-
path: '/iframe',
|
|
578
|
-
file: '~/components/IFrame.vue' // 最小组件,含 useTabRouter() 即可
|
|
579
|
-
})
|
|
580
|
-
}
|
|
581
|
-
}
|
|
319
|
+
openTab({
|
|
320
|
+
title: '外部页面',
|
|
321
|
+
path: 'https://example.com',
|
|
322
|
+
iframe: true
|
|
582
323
|
})
|
|
583
324
|
```
|
|
584
325
|
|
|
585
|
-
### iframe
|
|
326
|
+
### iframe 内页面打开标签
|
|
586
327
|
|
|
587
|
-
|
|
328
|
+
在 iframe 内的页面中,可以通过 `postMessage` 打开宿主的标签:
|
|
588
329
|
|
|
589
330
|
```ts
|
|
590
331
|
import { postOpenTab } from 'vue-stack-tabs'
|
|
591
332
|
|
|
333
|
+
// 在 iframe 内调用
|
|
592
334
|
postOpenTab({
|
|
593
335
|
title: '新页面',
|
|
594
336
|
path: '/detail',
|
|
595
|
-
query: { id: '
|
|
596
|
-
closable: true,
|
|
597
|
-
refreshable: true
|
|
337
|
+
query: { id: '1' }
|
|
598
338
|
})
|
|
599
339
|
```
|
|
600
340
|
|
|
601
|
-
|
|
341
|
+
### iframe 刷新
|
|
602
342
|
|
|
603
|
-
|
|
604
|
-
<vue-stack-tabs
|
|
605
|
-
iframe-path="/iframe"
|
|
606
|
-
:iframe-allowed-origins="['https://iframe来源域名']"
|
|
607
|
-
/>
|
|
608
|
-
```
|
|
343
|
+
iframe 刷新有两种模式:
|
|
609
344
|
|
|
610
|
-
|
|
345
|
+
- `postMessage`(默认):向 iframe 发送消息,由内页自行刷新
|
|
346
|
+
- `reload`:重建 iframe DOM(适用于跨域页面)
|
|
611
347
|
|
|
612
348
|
```ts
|
|
349
|
+
// iframe 内页面监听刷新请求
|
|
613
350
|
import { onRefreshRequest } from 'vue-stack-tabs'
|
|
614
|
-
import { onUnmounted } from 'vue'
|
|
615
351
|
|
|
616
|
-
|
|
617
|
-
|
|
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'
|
|
352
|
+
onRefreshRequest(() => {
|
|
353
|
+
// 执行刷新逻辑
|
|
354
|
+
location.reload()
|
|
630
355
|
})
|
|
631
356
|
```
|
|
632
357
|
|
|
633
|
-
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 🌍 国际化
|
|
634
361
|
|
|
635
|
-
|
|
362
|
+
内置语言:`zh-CN`(中文)、`en-US`(英文)。
|
|
636
363
|
|
|
637
|
-
```
|
|
638
|
-
|
|
364
|
+
```ts
|
|
365
|
+
// 切换语言
|
|
366
|
+
app.use(VueStackTabs, [{ locale: 'en-US' }])
|
|
639
367
|
```
|
|
640
368
|
|
|
641
|
-
|
|
369
|
+
自定义语言包:
|
|
642
370
|
|
|
643
371
|
```ts
|
|
644
|
-
app.use(VueStackTabs, [
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
close: '
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
closeOthers: '关闭其他',
|
|
652
|
-
closeAll: '关闭全部',
|
|
653
|
-
reload: '刷新',
|
|
654
|
-
reloadAll: '刷新全部',
|
|
655
|
-
maximum: '最大化',
|
|
656
|
-
restore: '还原',
|
|
657
|
-
undefined: '未定义',
|
|
658
|
-
loading: '加载中',
|
|
659
|
-
openInNewWindow: '新窗口打开'
|
|
372
|
+
app.use(VueStackTabs, [
|
|
373
|
+
{
|
|
374
|
+
locale: 'ja-JP',
|
|
375
|
+
messages: {
|
|
376
|
+
'VueStackTab.close': '閉じる',
|
|
377
|
+
'VueStackTab.refresh': '更新'
|
|
378
|
+
// ...
|
|
660
379
|
}
|
|
661
380
|
}
|
|
662
|
-
|
|
381
|
+
])
|
|
663
382
|
```
|
|
664
383
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
| 插槽 | 说明 |
|
|
668
|
-
|------|------|
|
|
669
|
-
| `leftButton` | 标签栏左侧 |
|
|
670
|
-
| `rightButton` | 标签栏右侧 |
|
|
671
|
-
|
|
672
|
-
### 事件
|
|
673
|
-
|
|
674
|
-
| 事件 | 参数 | 说明 |
|
|
675
|
-
|------|------|------|
|
|
676
|
-
| `onActive` | `id: string` | 标签被激活时触发 |
|
|
677
|
-
| `onPageLoaded` | — | 页面组件加载完成时触发 |
|
|
384
|
+
---
|
|
678
385
|
|
|
679
|
-
|
|
386
|
+
## 📄 类型定义
|
|
680
387
|
|
|
681
388
|
```ts
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
389
|
+
/** 打开标签时传入的数据 */
|
|
390
|
+
interface ITabData {
|
|
391
|
+
id?: string // 标签 ID(不传则自动生成 ULID)
|
|
392
|
+
title: string // 标签标题
|
|
393
|
+
path: string // 路由路径或 iframe URL
|
|
394
|
+
query?: Record<string, string> // 路由参数
|
|
395
|
+
closable?: boolean // 是否可关闭(默认 true)
|
|
396
|
+
refreshable?: boolean // 是否可刷新(默认 true)
|
|
397
|
+
iframe?: boolean // 是否为 iframe 标签(默认 false)
|
|
398
|
+
iframeRefreshMode?: 'postMessage' | 'reload' // iframe 刷新方式
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** 标签栏滚动模式 */
|
|
402
|
+
enum TabScrollMode {
|
|
403
|
+
WHEEL = 'wheel',
|
|
404
|
+
BUTTON = 'button',
|
|
405
|
+
BOTH = 'both'
|
|
406
|
+
}
|
|
694
407
|
```
|
|
695
408
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
支持路由 `children` 与 `forward`/`backward` 多级导航。详见 [SUBROUTE_EXPERIMENTAL.md](./docs/SUBROUTE_EXPERIMENTAL.md)。该功能为试验性。
|
|
409
|
+
---
|
|
699
410
|
|
|
700
|
-
|
|
411
|
+
## 📁 项目结构
|
|
701
412
|
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
413
|
+
```
|
|
414
|
+
src/lib/
|
|
415
|
+
├── StackTabs.vue # 主组件
|
|
416
|
+
├── index.ts # 入口与导出
|
|
417
|
+
├── hooks/ # 核心逻辑
|
|
418
|
+
│ ├── useTabActions.ts # 对外 API
|
|
419
|
+
│ ├── useTabRouter.ts # 栈内导航
|
|
420
|
+
│ ├── useTabLoading.ts # Loading 状态
|
|
421
|
+
│ └── useTabPanel.tsx # 核心引擎
|
|
422
|
+
├── model/TabModel.ts # 类型定义
|
|
423
|
+
├── components/ # UI 组件
|
|
424
|
+
├── nuxt/ # Nuxt 模块
|
|
425
|
+
└── assets/style/ # 样式
|
|
709
426
|
```
|
|
710
427
|
|
|
711
|
-
|
|
428
|
+
详细架构设计请参阅 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。
|
|
712
429
|
|
|
713
430
|
---
|
|
714
431
|
|
|
715
|
-
## License
|
|
432
|
+
## 📝 License
|
|
716
433
|
|
|
717
|
-
|
|
434
|
+
[LGPL-2.1](./LICENSE)
|