vue-stack-tabs 1.0.1 → 1.3.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.en.md +526 -0
- package/README.md +337 -520
- package/README.ru.md +526 -0
- package/dist/iframe-bridge.d.ts +8 -0
- package/dist/iframe-bridge.html +77 -0
- package/dist/iframe-bridge.mjs +20 -0
- package/dist/iframe-child.html +88 -0
- package/dist/nuxt/favicon.svg +10 -0
- package/dist/nuxt/iframe-bridge.html +77 -0
- package/dist/nuxt/iframe-child.html +88 -0
- package/dist/nuxt/module.d.ts +6 -0
- package/dist/nuxt/module.mjs +28 -0
- package/dist/nuxt/plugin.mjs +9 -0
- package/dist/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/nuxt/runtime/plugin.mjs +9 -0
- package/dist/src/lib/StackTabs.vue.d.ts +30 -16
- package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
- package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
- package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
- package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
- package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
- package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
- package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
- package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
- package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
- package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -23
- package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
- package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
- package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
- package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
- package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -73
- package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
- package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
- package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
- package/dist/src/lib/hooks/useTabPanel.d.ts +17 -74
- package/dist/src/lib/hooks/useTabRouter.d.ts +5 -5
- package/dist/src/lib/i18n/index.d.ts +6 -4
- package/dist/src/lib/i18n/lang/en.d.ts +5 -0
- package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
- package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
- package/dist/src/lib/iframe-bridge.d.ts +8 -0
- package/dist/src/lib/iframe.vue.d.ts +2 -1
- package/dist/src/lib/index.d.ts +4 -5
- package/dist/src/lib/model/TabModel.d.ts +14 -33
- package/dist/src/lib/nuxt/module.d.ts +6 -0
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
- package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
- package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +1 -16
- package/dist/src/lib/utils/typeGuards.d.ts +2 -0
- package/dist/src/lib/utils/urlParser.d.ts +24 -3
- package/dist/src/lib/versionLogger.d.ts +1 -0
- package/dist/style.css +2 -1
- package/dist/utils/iframeBridge.d.ts +39 -0
- package/dist/vue-stack-tabs.css +2 -1
- package/dist/vue-stack-tabs.es.js +1 -2411
- package/package.json +68 -47
- package/src/lib/StackTabs.vue +216 -192
- package/src/lib/assets/style/stackTab.scss +84 -12
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
- package/src/lib/components/ContextMenu/index.vue +94 -10
- package/src/lib/components/PageLoading.vue +9 -7
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
- package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
- package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
- package/src/lib/components/TabHeader/TabHeaderItem.vue +45 -16
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
- package/src/lib/components/TabHeader/index.vue +109 -12
- package/src/lib/hooks/stackTabsContext.ts +135 -0
- package/src/lib/hooks/tabPanel/evict.ts +123 -41
- package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
- package/src/lib/hooks/tabPanel/refresh.ts +109 -0
- package/src/lib/hooks/tabPanel/scroll.ts +57 -36
- package/src/lib/hooks/tabPanel/session.ts +84 -22
- package/src/lib/hooks/tabPanel/state.ts +41 -44
- package/src/lib/hooks/useIframeManager.ts +253 -0
- package/src/lib/hooks/useTabActions.ts +62 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +415 -213
- package/src/lib/hooks/useTabRouter.ts +172 -43
- package/src/lib/i18n/index.ts +53 -45
- package/src/lib/i18n/lang/en.ts +6 -1
- package/src/lib/i18n/lang/zh-CN.ts +6 -1
- package/src/lib/i18n/stackTabsLocale.ts +8 -0
- package/src/lib/iframe-bridge.ts +12 -0
- package/src/lib/index.ts +14 -19
- package/src/lib/model/TabModel.ts +34 -86
- package/src/lib/nuxt/module.d.ts +4 -0
- package/src/lib/nuxt/module.ts +8 -3
- package/src/lib/nuxt/runtime/plugin.client.ts +2 -3
- package/src/lib/nuxt/runtime/plugin.ts +11 -0
- package/src/lib/utils/iframeBridge.ts +29 -5
- package/src/lib/utils/scrollUtils.ts +9 -4
- package/src/lib/utils/stackTabsA11y.ts +12 -0
- package/src/lib/utils/stackTabsMessage.ts +99 -0
- package/src/lib/utils/tabInfoEncoder.ts +69 -42
- package/src/lib/utils/typeGuards.ts +3 -0
- package/src/lib/utils/urlParser.ts +144 -14
- package/src/lib/versionLogger.ts +7 -19
- package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
- package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
- package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
- package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
- package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
- package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
- package/dist/vue-stack-tabs.umd.js +0 -1
- package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
- package/src/lib/hooks/useTabPanel.spec.ts +0 -34
- package/src/lib/model/TabModel.spec.ts +0 -40
- package/src/lib/utils/iframeBridge.spec.ts +0 -37
- package/src/lib/utils/tabInfoEncoder.spec.ts +0 -42
- package/src/lib/utils/urlParser.spec.ts +0 -45
package/README.md
CHANGED
|
@@ -1,56 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vue-stack-tabs
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
[](LICENSE)
|
|
3
|
+
**中文** | [English](./README.en.md) | [Русский](./README.ru.md)
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
> Vue 3 多标签页管理库,基于 Vue Router。用 Vue 作用域实现类似 iframe TabPanel 的效果——**每个 Tab 间组件作用域互不干扰**。
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
## ✨ 特性
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
|
9
|
+
- 🗂️ **路由级标签页** — 每个标签拥有独立的组件和缓存
|
|
10
|
+
- 📚 **栈式页内导航** — 标签内支持 forward / backward,类似浏览器历史栈
|
|
11
|
+
- 🔄 **标签刷新** — 单个刷新或全部刷新,完全重建组件实例
|
|
12
|
+
- 🌐 **iframe 标签** — 支持嵌入 iframe 页面,postMessage 通信
|
|
13
|
+
- 💾 **Session 持久化** — 浏览器刷新后恢复上次激活的标签
|
|
14
|
+
- 📜 **滚动位置记忆** — 切换标签后自动恢复上次滚动位置
|
|
15
|
+
- 🎨 **右键菜单** — 内置关闭/刷新操作
|
|
16
|
+
- 🌍 **国际化** — 内置中英文,可扩展
|
|
17
|
+
- 📦 **Nuxt 3/4 模块** — 开箱即用
|
|
36
18
|
|
|
37
19
|
---
|
|
38
20
|
|
|
39
|
-
##
|
|
21
|
+
## 📦 安装
|
|
40
22
|
|
|
41
23
|
```bash
|
|
42
|
-
|
|
43
|
-
# or
|
|
24
|
+
# npm
|
|
44
25
|
npm install vue-stack-tabs
|
|
45
|
-
```
|
|
46
26
|
|
|
47
|
-
|
|
27
|
+
# pnpm
|
|
28
|
+
pnpm add vue-stack-tabs
|
|
29
|
+
```
|
|
48
30
|
|
|
49
31
|
---
|
|
50
32
|
|
|
51
|
-
##
|
|
33
|
+
## 🚀 快速接入(Vue 3)
|
|
52
34
|
|
|
53
|
-
###
|
|
35
|
+
### 1. 注册插件
|
|
54
36
|
|
|
55
37
|
```ts
|
|
56
38
|
// main.ts
|
|
@@ -66,155 +48,93 @@ app.use(VueStackTabs)
|
|
|
66
48
|
app.mount('#app')
|
|
67
49
|
```
|
|
68
50
|
|
|
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--路由配置)).
|
|
51
|
+
> `vue-stack-tabs` 每个 Vue app 只支持一个 `<VueStackTabs>` 实例。默认导入 `VueStackTabs` 是 Vue plugin,用于 `app.use(VueStackTabs)`;如果需要本地组件导入,请使用 named export。
|
|
77
52
|
|
|
78
|
-
###
|
|
53
|
+
### 2. 配置路由
|
|
79
54
|
|
|
80
|
-
|
|
55
|
+
标签页依赖 Vue Router,需要一个承载 `<VueStackTabs>` 的父路由:
|
|
81
56
|
|
|
82
57
|
```ts
|
|
83
58
|
// router.ts
|
|
84
59
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
85
|
-
import Frame from '@/components/layout/Frame.vue'
|
|
86
|
-
import { IFrame } from 'vue-stack-tabs'
|
|
87
60
|
|
|
88
61
|
const router = createRouter({
|
|
89
62
|
history: createWebHistory(),
|
|
90
63
|
routes: [
|
|
91
64
|
{
|
|
92
65
|
path: '/',
|
|
93
|
-
component:
|
|
66
|
+
component: () => import('./Layout.vue'),
|
|
94
67
|
children: [
|
|
95
|
-
{ path: '', component: () => import('
|
|
96
|
-
{ path: '
|
|
97
|
-
{ path: '
|
|
68
|
+
{ path: '', component: () => import('./pages/Home.vue') },
|
|
69
|
+
{ path: 'about', component: () => import('./pages/About.vue') },
|
|
70
|
+
{ path: 'settings', component: () => import('./pages/Settings.vue') },
|
|
71
|
+
// iframe 占位路由(必须)
|
|
72
|
+
{ path: 'iframe', component: () => import('vue-stack-tabs').then((m) => m.IFrame) }
|
|
98
73
|
]
|
|
99
74
|
}
|
|
100
75
|
]
|
|
101
76
|
})
|
|
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
77
|
|
|
124
|
-
|
|
78
|
+
export default router
|
|
79
|
+
```
|
|
125
80
|
|
|
126
|
-
|
|
81
|
+
### 3. 使用组件
|
|
127
82
|
|
|
128
|
-
|
|
83
|
+
```vue
|
|
84
|
+
<!-- Layout.vue -->
|
|
85
|
+
<template>
|
|
86
|
+
<div style="width: 100%; height: 100vh">
|
|
87
|
+
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" :max="20" :contextmenu="true" />
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
129
90
|
|
|
130
|
-
|
|
131
|
-
import {
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import type { ITabData } from 'vue-stack-tabs'
|
|
132
93
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
openInNewWindow
|
|
143
|
-
} = useTabActions()
|
|
94
|
+
const defaultTabs: ITabData[] = [
|
|
95
|
+
{
|
|
96
|
+
title: '首页',
|
|
97
|
+
path: '/',
|
|
98
|
+
closable: false,
|
|
99
|
+
refreshable: true
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
</script>
|
|
144
103
|
```
|
|
145
104
|
|
|
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.
|
|
105
|
+
如果需要本地组件导入,推荐使用 `VueStackTabs` named export;`StackTab` named export 会继续保留作为兼容别名:
|
|
173
106
|
|
|
174
107
|
```ts
|
|
175
|
-
import {
|
|
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')
|
|
108
|
+
import { VueStackTabs as VueStackTabsComponent } from 'vue-stack-tabs'
|
|
189
109
|
```
|
|
190
110
|
|
|
191
|
-
###
|
|
111
|
+
### 4. 打开标签
|
|
192
112
|
|
|
193
113
|
```ts
|
|
194
|
-
import {
|
|
114
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
195
115
|
|
|
196
|
-
const {
|
|
197
|
-
```
|
|
116
|
+
const { openTab, closeTab, refreshTab } = useTabActions()
|
|
198
117
|
|
|
199
|
-
|
|
118
|
+
// 打开新标签
|
|
119
|
+
openTab({
|
|
120
|
+
id: 'about', // 可选,不传则自动生成
|
|
121
|
+
title: '关于',
|
|
122
|
+
path: '/about',
|
|
123
|
+
query: { id: '1' } // 可选
|
|
124
|
+
})
|
|
200
125
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
iframe-path="/iframe"
|
|
204
|
-
:default-tabs="[
|
|
205
|
-
{ id: 'home', title: 'Home', path: '/', closable: false },
|
|
206
|
-
{ title: 'About', path: '/about' }
|
|
207
|
-
]"
|
|
208
|
-
/>
|
|
209
|
-
```
|
|
126
|
+
// 关闭标签
|
|
127
|
+
closeTab('about')
|
|
210
128
|
|
|
211
|
-
|
|
129
|
+
// 刷新标签
|
|
130
|
+
refreshTab('about')
|
|
131
|
+
```
|
|
212
132
|
|
|
213
133
|
---
|
|
214
134
|
|
|
215
|
-
## Nuxt
|
|
135
|
+
## 🚀 快速接入(Nuxt 3/4)
|
|
216
136
|
|
|
217
|
-
###
|
|
137
|
+
### 1. 注册模块
|
|
218
138
|
|
|
219
139
|
```ts
|
|
220
140
|
// nuxt.config.ts
|
|
@@ -222,496 +142,393 @@ export default defineNuxtConfig({
|
|
|
222
142
|
modules: ['vue-stack-tabs/nuxt'],
|
|
223
143
|
vueStackTabs: {
|
|
224
144
|
locale: 'zh-CN'
|
|
225
|
-
}
|
|
145
|
+
},
|
|
146
|
+
css: ['vue-stack-tabs/dist/style.css']
|
|
226
147
|
})
|
|
227
148
|
```
|
|
228
149
|
|
|
229
|
-
|
|
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:
|
|
150
|
+
### 2. 在 Layout 中使用
|
|
267
151
|
|
|
268
152
|
```vue
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
/>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
### Receive refresh in iframe
|
|
153
|
+
<!-- layouts/default.vue -->
|
|
154
|
+
<template>
|
|
155
|
+
<div style="width: 100%; height: 100vh">
|
|
156
|
+
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" />
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
276
159
|
|
|
277
|
-
|
|
278
|
-
import {
|
|
279
|
-
import { onUnmounted } from 'vue'
|
|
160
|
+
<script setup lang="ts">
|
|
161
|
+
import type { ITabData } from 'vue-stack-tabs'
|
|
280
162
|
|
|
281
|
-
const
|
|
282
|
-
|
|
163
|
+
const defaultTabs: ITabData[] = [{ title: '首页', path: '/', closable: false, refreshable: true }]
|
|
164
|
+
</script>
|
|
283
165
|
```
|
|
284
166
|
|
|
285
|
-
###
|
|
167
|
+
### 3. 页面中使用
|
|
286
168
|
|
|
287
|
-
|
|
169
|
+
```vue
|
|
170
|
+
<!-- pages/index.vue -->
|
|
171
|
+
<template>
|
|
172
|
+
<div>
|
|
173
|
+
<h1>首页</h1>
|
|
174
|
+
<button @click="openTab({ title: '关于', path: '/about' })">打开关于页</button>
|
|
175
|
+
</div>
|
|
176
|
+
</template>
|
|
288
177
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
iframe: true,
|
|
294
|
-
iframeRefreshMode: 'reload'
|
|
295
|
-
})
|
|
178
|
+
<script setup>
|
|
179
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
180
|
+
const { openTab } = useTabActions()
|
|
181
|
+
</script>
|
|
296
182
|
```
|
|
297
183
|
|
|
298
184
|
---
|
|
299
185
|
|
|
300
|
-
##
|
|
186
|
+
## ESM-only 与 iframe bridge 子入口
|
|
301
187
|
|
|
302
|
-
|
|
188
|
+
`vue-stack-tabs` 从当前版本开始只承诺 ESM import,不再支持 `require('vue-stack-tabs')`。
|
|
303
189
|
|
|
304
|
-
|
|
305
|
-
<vue-stack-tabs iframe-path="/iframe" i18n="zh-CN" />
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### Custom messages
|
|
190
|
+
主应用中继续使用 root 入口:
|
|
309
191
|
|
|
310
192
|
```ts
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
}])
|
|
193
|
+
import VueStackTabs from 'vue-stack-tabs'
|
|
194
|
+
import 'vue-stack-tabs/dist/style.css'
|
|
330
195
|
```
|
|
331
196
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
## Slots
|
|
197
|
+
iframe 内部页面建议使用无样式副作用的子入口:
|
|
335
198
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
| `leftButton` | Left side of tab bar |
|
|
339
|
-
| `rightButton` | Right side of tab bar |
|
|
199
|
+
```ts
|
|
200
|
+
import { postOpenTab, onRefreshRequest } from 'vue-stack-tabs/iframe-bridge'
|
|
340
201
|
|
|
341
|
-
|
|
202
|
+
postOpenTab(
|
|
203
|
+
{ title: '订单详情', path: '/orders/1' },
|
|
204
|
+
{ targetOrigin: 'https://parent.example.com' }
|
|
205
|
+
)
|
|
342
206
|
|
|
343
|
-
|
|
207
|
+
const off = onRefreshRequest(() => window.location.reload(), {
|
|
208
|
+
allowedOrigins: ['https://parent.example.com']
|
|
209
|
+
})
|
|
210
|
+
```
|
|
344
211
|
|
|
345
|
-
|
|
346
|
-
|-------|---------|-------------|
|
|
347
|
-
| `onActive` | `id: string` | Fired when a tab is activated |
|
|
348
|
-
| `onPageLoaded` | — | Fired when page component is loaded |
|
|
212
|
+
默认未传 `targetOrigin` 时使用 iframe 页面自身的 `window.location.origin`,适合同源父页面;跨域父页面必须显式传 `targetOrigin`。生产环境建议同时显式传入 `targetOrigin` 和 `allowedOrigins`。
|
|
349
213
|
|
|
350
214
|
---
|
|
351
215
|
|
|
352
|
-
##
|
|
216
|
+
## iframe 安全策略
|
|
353
217
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
MSG_OPEN_TAB
|
|
365
|
-
} from 'vue-stack-tabs'
|
|
366
|
-
import type { ITabData, ITabBase, IframeOpenTabPayload } from 'vue-stack-tabs'
|
|
218
|
+
`VueStackTabs` 支持配置 iframe 安全属性:
|
|
219
|
+
|
|
220
|
+
```vue
|
|
221
|
+
<VueStackTabs
|
|
222
|
+
iframe-path="/__iframe"
|
|
223
|
+
iframe-sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-same-origin"
|
|
224
|
+
iframe-referrer-policy="strict-origin-when-cross-origin"
|
|
225
|
+
iframe-allow="fullscreen"
|
|
226
|
+
:iframe-load-timeout="15000"
|
|
227
|
+
/>
|
|
367
228
|
```
|
|
368
229
|
|
|
230
|
+
默认 sandbox 保留常见业务页面能力,属于兼容优先,不是强隔离;强隔离请移除 `allow-same-origin`,并按业务能力继续减少 sandbox token。如果你的同源业务页面确实不能在 sandbox 下工作,可以传入空字符串关闭 sandbox,但这会降低隔离强度。
|
|
231
|
+
|
|
369
232
|
---
|
|
370
233
|
|
|
371
|
-
##
|
|
234
|
+
## 📖 API 参考
|
|
372
235
|
|
|
373
|
-
|
|
236
|
+
### useTabActions
|
|
374
237
|
|
|
375
|
-
|
|
238
|
+
标签级操作封装。
|
|
376
239
|
|
|
377
|
-
|
|
240
|
+
```ts
|
|
241
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
378
242
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
243
|
+
const {
|
|
244
|
+
openTab, // (tab: ITabData, renew?: boolean) => Promise<string>
|
|
245
|
+
closeTab, // (id: string) => string
|
|
246
|
+
closeAllTabs, // () => void
|
|
247
|
+
refreshTab, // (id: string) => void
|
|
248
|
+
refreshAllTabs, // () => void
|
|
249
|
+
activeTab, // (id: string, isRoute?: boolean) => void
|
|
250
|
+
reset, // () => void
|
|
251
|
+
tabs // Ref<ITabItem[]>
|
|
252
|
+
} = useTabActions()
|
|
386
253
|
```
|
|
387
254
|
|
|
388
|
-
|
|
255
|
+
| 方法 | 说明 |
|
|
256
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
257
|
+
| `openTab(tab, renew?)` | 打开新标签。`renew=true` 时若已存在则清空页面栈后重新打开 |
|
|
258
|
+
| `closeTab(id)` | 关闭指定标签,返回新激活的标签 ID |
|
|
259
|
+
| `closeAllTabs()` | 关闭所有可关闭标签 |
|
|
260
|
+
| `refreshTab(id)` | 刷新指定标签(替换缓存 ID,重建组件实例) |
|
|
261
|
+
| `refreshAllTabs()` | 刷新所有标签 |
|
|
262
|
+
| `activeTab(id)` | 激活指定标签(切换 Tab) |
|
|
263
|
+
| `reset()` | 关闭所有标签并重置状态 |
|
|
389
264
|
|
|
390
265
|
---
|
|
391
266
|
|
|
392
|
-
|
|
267
|
+
### useTabRouter
|
|
393
268
|
|
|
394
|
-
|
|
269
|
+
标签内栈式导航,**只能在标签内的页面组件中使用**。
|
|
395
270
|
|
|
396
271
|
```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
|
-
### 快速开始
|
|
272
|
+
import { useTabRouter } from 'vue-stack-tabs'
|
|
413
273
|
|
|
414
|
-
|
|
415
|
-
<!-- App.vue 或布局组件 -->
|
|
416
|
-
<template>
|
|
417
|
-
<vue-stack-tabs iframe-path="/iframe" />
|
|
418
|
-
</template>
|
|
274
|
+
const { forward, backward, addScrollTarget } = useTabRouter()
|
|
419
275
|
```
|
|
420
276
|
|
|
421
|
-
|
|
277
|
+
| 方法 | 说明 |
|
|
278
|
+
| --------------------------- | --------------------------------- |
|
|
279
|
+
| `forward(to)` | 在当前标签内前进到新页面 |
|
|
280
|
+
| `backward(to, backQuery?)` | 在当前标签内后退 |
|
|
281
|
+
| `addScrollTarget(selector)` | 注册需要记忆滚动位置的 DOM 选择器 |
|
|
422
282
|
|
|
423
|
-
|
|
283
|
+
#### forward
|
|
424
284
|
|
|
425
285
|
```ts
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
import Frame from '@/components/layout/Frame.vue'
|
|
429
|
-
import { IFrame } from 'vue-stack-tabs'
|
|
286
|
+
// 前进到 /detail 页面
|
|
287
|
+
forward({ path: '/detail', query: { id: '123' } })
|
|
430
288
|
|
|
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
|
-
})
|
|
289
|
+
// 循环压栈自身(同路由可多次入栈,每次创建独立缓存)
|
|
290
|
+
forward({ path: '/list', query: { page: '2' } })
|
|
445
291
|
```
|
|
446
292
|
|
|
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
|
|
293
|
+
#### backward
|
|
468
294
|
|
|
469
295
|
```ts
|
|
470
|
-
|
|
296
|
+
// 后退 1 步(默认)
|
|
297
|
+
backward(1)
|
|
471
298
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
closeTab,
|
|
475
|
-
closeAllTabs,
|
|
476
|
-
refreshTab,
|
|
477
|
-
refreshAllTabs,
|
|
478
|
-
activeTab,
|
|
479
|
-
reset,
|
|
480
|
-
tabs,
|
|
481
|
-
openInNewWindow
|
|
482
|
-
} = useTabActions()
|
|
483
|
-
```
|
|
299
|
+
// 后退 N 步
|
|
300
|
+
backward(3)
|
|
484
301
|
|
|
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
|
-
在**标签内的页面**中使用,用于标签内前进/后退。
|
|
302
|
+
// 回退到指定路径(自动从栈中查找)
|
|
303
|
+
backward('/list')
|
|
512
304
|
|
|
513
|
-
|
|
514
|
-
|
|
305
|
+
// 带参回退(目标页通过 props._back 接收)
|
|
306
|
+
backward('/list', { result: 'success', data: { id: 1 } })
|
|
307
|
+
```
|
|
515
308
|
|
|
516
|
-
|
|
309
|
+
**目标页接收参数:**
|
|
517
310
|
|
|
518
|
-
|
|
519
|
-
|
|
311
|
+
```vue
|
|
312
|
+
<script setup>
|
|
313
|
+
// 声明 props 接收后退参数
|
|
314
|
+
defineProps<{ _back?: { result: string; data: any } }>()
|
|
315
|
+
</script>
|
|
316
|
+
```
|
|
520
317
|
|
|
521
|
-
|
|
522
|
-
backward('/list') // 回退到首个匹配路径
|
|
523
|
-
backward(1) // 回退 1 步
|
|
524
|
-
backward(2, { foo: 'bar' }) // 回退 2 步,并向目标页传递 query
|
|
318
|
+
---
|
|
525
319
|
|
|
526
|
-
|
|
527
|
-
addScrollTarget('.scroll-area', '#panel')
|
|
528
|
-
```
|
|
320
|
+
### useTabLoading
|
|
529
321
|
|
|
530
|
-
|
|
322
|
+
页面加载状态控制,**只能在标签内的页面组件中使用**。
|
|
531
323
|
|
|
532
324
|
```ts
|
|
533
325
|
import { useTabLoading } from 'vue-stack-tabs'
|
|
534
326
|
|
|
535
327
|
const { openTabLoading, closeTabLoading } = useTabLoading()
|
|
536
|
-
```
|
|
537
328
|
|
|
538
|
-
|
|
329
|
+
// 显示加载遮罩
|
|
330
|
+
openTabLoading()
|
|
539
331
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
iframe-path="/iframe"
|
|
543
|
-
:default-tabs="[
|
|
544
|
-
{ id: 'home', title: '首页', path: '/', closable: false },
|
|
545
|
-
{ title: '关于', path: '/about' }
|
|
546
|
-
]"
|
|
547
|
-
/>
|
|
332
|
+
// 异步操作完成后关闭
|
|
333
|
+
fetchData().finally(() => closeTabLoading())
|
|
548
334
|
```
|
|
549
335
|
|
|
550
|
-
>
|
|
336
|
+
> 组件卸载时自动关闭 Loading,无需手动清理。
|
|
551
337
|
|
|
552
|
-
|
|
338
|
+
---
|
|
553
339
|
|
|
554
|
-
|
|
340
|
+
## ⚙️ Props
|
|
341
|
+
|
|
342
|
+
`<VueStackTabs>` 组件属性:
|
|
343
|
+
|
|
344
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
345
|
+
| ---------------------- | --------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
346
|
+
| `iframePath` | `string` | **必填** | iframe 占位路由的路径 |
|
|
347
|
+
| `iframeSandbox` | `string` | `allow-scripts allow-forms allow-popups allow-downloads allow-same-origin` | iframe sandbox 策略;默认兼容优先不是强隔离,强隔离请移除 `allow-same-origin`;传空字符串可关闭 sandbox(不推荐) |
|
|
348
|
+
| `iframeReferrerPolicy` | `ReferrerPolicy` | `strict-origin-when-cross-origin` | iframe referrerpolicy 属性 |
|
|
349
|
+
| `iframeAllow` | `string` | `''` | iframe allow 属性,例如 `fullscreen` |
|
|
350
|
+
| `iframeLoadTimeout` | `number` | `15000` | iframe 加载超时时间,单位 ms |
|
|
351
|
+
| `defaultTabs` | `ITabData[]` | `[]` | 初始标签列表 |
|
|
352
|
+
| `max` | `number` | `20` | 最大标签数量 |
|
|
353
|
+
| `contextmenu` | `boolean \| object` | `true` | 是否启用右键菜单 |
|
|
354
|
+
| `pageTransition` | `string` | `'stack-tab-swap'` | 前进时的页面转场动画名 |
|
|
355
|
+
| `pageTransitionBack` | `string` | `'stack-tab-swap-back'` | 后退时的页面转场动画名 |
|
|
356
|
+
| `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'` | 标签增删时的过渡效果 |
|
|
357
|
+
| `tabScrollMode` | `TabScrollMode` | `'both'` | 标签栏滚动方式:`'wheel'` / `'button'` / `'both'` |
|
|
358
|
+
| `width` | `string` | `'100%'` | 容器宽度 |
|
|
359
|
+
| `height` | `string` | `'100%'` | 容器高度 |
|
|
360
|
+
| `i18n` | `string` | `'zh-CN'` | 国际化语言 |
|
|
361
|
+
| `globalScroll` | `boolean` | `false` | 是否使用页面级滚动记忆 |
|
|
362
|
+
| `sessionPrefix` | `string` | `''` | sessionStorage 键前缀 |
|
|
363
|
+
| `iframeAllowedOrigins` | `string[]` | 同源 | 允许 iframe postMessage 的来源列表 |
|
|
555
364
|
|
|
556
|
-
|
|
557
|
-
// nuxt.config.ts
|
|
558
|
-
export default defineNuxtConfig({
|
|
559
|
-
modules: ['vue-stack-tabs/nuxt'],
|
|
560
|
-
vueStackTabs: {
|
|
561
|
-
locale: 'zh-CN'
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
```
|
|
365
|
+
---
|
|
565
366
|
|
|
566
|
-
|
|
367
|
+
## 🎯 Events
|
|
567
368
|
|
|
568
|
-
|
|
369
|
+
| 事件 | 参数 | 说明 |
|
|
370
|
+
| -------------- | -------------- | ---------------------- |
|
|
371
|
+
| `onActive` | `(id: string)` | 标签被激活时触发 |
|
|
372
|
+
| `onPageLoaded` | — | 页面组件加载完成时触发 |
|
|
569
373
|
|
|
570
|
-
|
|
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
|
-
```
|
|
374
|
+
---
|
|
584
375
|
|
|
585
|
-
|
|
376
|
+
## 🖼️ iframe 标签页
|
|
586
377
|
|
|
587
|
-
|
|
378
|
+
### 打开 iframe 标签
|
|
588
379
|
|
|
589
380
|
```ts
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
path: '/detail',
|
|
595
|
-
query: { id: '123' },
|
|
596
|
-
closable: true,
|
|
597
|
-
refreshable: true
|
|
381
|
+
openTab({
|
|
382
|
+
title: '外部页面',
|
|
383
|
+
path: 'https://example.com',
|
|
384
|
+
iframe: true
|
|
598
385
|
})
|
|
599
386
|
```
|
|
600
387
|
|
|
601
|
-
|
|
388
|
+
### iframe 页面内操作
|
|
602
389
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
:iframe-allowed-origins="['https://iframe来源域名']"
|
|
607
|
-
/>
|
|
608
|
-
```
|
|
390
|
+
在 iframe 标签页内,可以通过 `postMessage` 与宿主容器进行通信,实现打开新标签或响应刷新操作。
|
|
391
|
+
|
|
392
|
+
#### 1. 使用 Bridge 工具(推荐)
|
|
609
393
|
|
|
610
|
-
|
|
394
|
+
如果 iframe 内页能引用库代码,推荐使用无样式副作用的 `vue-stack-tabs/iframe-bridge` 子入口:
|
|
611
395
|
|
|
612
396
|
```ts
|
|
613
|
-
import { onRefreshRequest } from 'vue-stack-tabs'
|
|
614
|
-
import { onUnmounted } from 'vue'
|
|
397
|
+
import { postOpenTab, onRefreshRequest } from 'vue-stack-tabs/iframe-bridge'
|
|
615
398
|
|
|
616
|
-
const
|
|
617
|
-
|
|
399
|
+
const parentOrigin = 'https://parent.example.com'
|
|
400
|
+
|
|
401
|
+
// 打开标签
|
|
402
|
+
postOpenTab(
|
|
403
|
+
{
|
|
404
|
+
title: '新页面',
|
|
405
|
+
path: '/detail',
|
|
406
|
+
query: { id: '1' }
|
|
407
|
+
},
|
|
408
|
+
{ targetOrigin: parentOrigin }
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
// 监听刷新请求
|
|
412
|
+
const off = onRefreshRequest(
|
|
413
|
+
() => {
|
|
414
|
+
// 自定义刷新逻辑
|
|
415
|
+
location.reload()
|
|
416
|
+
},
|
|
417
|
+
{ allowedOrigins: [parentOrigin] }
|
|
418
|
+
)
|
|
618
419
|
```
|
|
619
420
|
|
|
620
|
-
####
|
|
421
|
+
#### 2. 原生对接方式
|
|
621
422
|
|
|
622
|
-
|
|
423
|
+
如果无法引入库工具,或希望保持零依赖,可以使用原生 API。生产环境必须使用明确的父页面 origin,并校验刷新消息来源。`postOpenTab` 的默认 `targetOrigin` 是 iframe 页面的 `window.location.origin`,跨域父页面必须显式传入父页面 origin:
|
|
623
424
|
|
|
624
425
|
```ts
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
426
|
+
const parentOrigin = 'https://parent.example.com'
|
|
427
|
+
|
|
428
|
+
// 打开标签
|
|
429
|
+
window.parent.postMessage(
|
|
430
|
+
{
|
|
431
|
+
type: 'vue-stack-tabs:openTab',
|
|
432
|
+
payload: {
|
|
433
|
+
title: '原生新页面',
|
|
434
|
+
path: '/detail'
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
parentOrigin
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
// 监听刷新请求
|
|
441
|
+
window.addEventListener('message', (ev) => {
|
|
442
|
+
if (ev.origin !== parentOrigin) return
|
|
443
|
+
if (ev.source !== window.parent) return
|
|
444
|
+
if (ev.data?.type === 'vue-stack-tabs:refresh') {
|
|
445
|
+
// 执行刷新
|
|
446
|
+
location.reload()
|
|
447
|
+
}
|
|
630
448
|
})
|
|
631
449
|
```
|
|
632
450
|
|
|
633
|
-
###
|
|
451
|
+
### iframe 刷新模式
|
|
634
452
|
|
|
635
|
-
|
|
453
|
+
在 `openTab` 时可通过 `iframeRefreshMode` 指定刷新行为:
|
|
636
454
|
|
|
637
|
-
|
|
638
|
-
|
|
455
|
+
- `postMessage`(默认):宿主向 iframe 发送 `vue-stack-tabs:refresh` 消息,由内页自行决定如何刷新(动画更丝滑)。
|
|
456
|
+
- `reload`:宿主直接重置 iframe 的 `src` 或 `key` 触发浏览器强制重载(适用于跨域或无法修改代码的页面)。
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## 🌍 国际化
|
|
461
|
+
|
|
462
|
+
内置语言:`zh-CN`(中文)、`en-US`(英文)。
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
// 切换语言
|
|
466
|
+
app.use(VueStackTabs, [{ locale: 'en-US' }])
|
|
639
467
|
```
|
|
640
468
|
|
|
641
|
-
|
|
469
|
+
自定义语言包:
|
|
642
470
|
|
|
643
471
|
```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: '新窗口打开'
|
|
472
|
+
app.use(VueStackTabs, [
|
|
473
|
+
{
|
|
474
|
+
locale: 'ja-JP',
|
|
475
|
+
messages: {
|
|
476
|
+
'VueStackTab.close': '閉じる',
|
|
477
|
+
'VueStackTab.refresh': '更新'
|
|
478
|
+
// ...
|
|
660
479
|
}
|
|
661
480
|
}
|
|
662
|
-
|
|
481
|
+
])
|
|
663
482
|
```
|
|
664
483
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
| 插槽 | 说明 |
|
|
668
|
-
|------|------|
|
|
669
|
-
| `leftButton` | 标签栏左侧 |
|
|
670
|
-
| `rightButton` | 标签栏右侧 |
|
|
671
|
-
|
|
672
|
-
### 事件
|
|
673
|
-
|
|
674
|
-
| 事件 | 参数 | 说明 |
|
|
675
|
-
|------|------|------|
|
|
676
|
-
| `onActive` | `id: string` | 标签被激活时触发 |
|
|
677
|
-
| `onPageLoaded` | — | 页面组件加载完成时触发 |
|
|
484
|
+
---
|
|
678
485
|
|
|
679
|
-
|
|
486
|
+
## 📄 类型定义
|
|
680
487
|
|
|
681
488
|
```ts
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
489
|
+
/** 打开标签时传入的数据 */
|
|
490
|
+
interface ITabData {
|
|
491
|
+
id?: string // 标签 ID(不传则自动生成 UUID)
|
|
492
|
+
title: string // 标签标题
|
|
493
|
+
path: string // 路由路径或 iframe URL
|
|
494
|
+
query?: Record<string, string> // 路由参数
|
|
495
|
+
closable?: boolean // 是否可关闭(默认 true)
|
|
496
|
+
refreshable?: boolean // 是否可刷新(默认 true)
|
|
497
|
+
iframe?: boolean // 是否为 iframe 标签(默认 false)
|
|
498
|
+
iframeRefreshMode?: 'postMessage' | 'reload' // iframe 刷新方式
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** 标签栏滚动模式 */
|
|
502
|
+
enum TabScrollMode {
|
|
503
|
+
WHEEL = 'wheel',
|
|
504
|
+
BUTTON = 'button',
|
|
505
|
+
BOTH = 'both'
|
|
506
|
+
}
|
|
694
507
|
```
|
|
695
508
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
支持路由 `children` 与 `forward`/`backward` 多级导航。详见 [SUBROUTE_EXPERIMENTAL.md](./docs/SUBROUTE_EXPERIMENTAL.md)。该功能为试验性。
|
|
509
|
+
---
|
|
699
510
|
|
|
700
|
-
|
|
511
|
+
## 📁 项目结构
|
|
701
512
|
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
513
|
+
```
|
|
514
|
+
src/lib/
|
|
515
|
+
├── StackTabs.vue # 主组件
|
|
516
|
+
├── index.ts # 入口与导出
|
|
517
|
+
├── hooks/ # 核心逻辑
|
|
518
|
+
│ ├── useTabActions.ts # 对外 API
|
|
519
|
+
│ ├── useTabRouter.ts # 栈内导航
|
|
520
|
+
│ ├── useTabLoading.ts # Loading 状态
|
|
521
|
+
│ └── useTabPanel.tsx # 核心引擎
|
|
522
|
+
├── model/TabModel.ts # 类型定义
|
|
523
|
+
├── components/ # UI 组件
|
|
524
|
+
├── nuxt/ # Nuxt 模块
|
|
525
|
+
└── assets/style/ # 样式
|
|
709
526
|
```
|
|
710
527
|
|
|
711
|
-
|
|
528
|
+
详细架构设计请参阅 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。
|
|
712
529
|
|
|
713
530
|
---
|
|
714
531
|
|
|
715
|
-
## License
|
|
532
|
+
## 📝 License
|
|
716
533
|
|
|
717
|
-
|
|
534
|
+
[LGPL-2.1](./LICENSE)
|