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.
Files changed (116) hide show
  1. package/README.en.md +526 -0
  2. package/README.md +337 -520
  3. package/README.ru.md +526 -0
  4. package/dist/iframe-bridge.d.ts +8 -0
  5. package/dist/iframe-bridge.html +77 -0
  6. package/dist/iframe-bridge.mjs +20 -0
  7. package/dist/iframe-child.html +88 -0
  8. package/dist/nuxt/favicon.svg +10 -0
  9. package/dist/nuxt/iframe-bridge.html +77 -0
  10. package/dist/nuxt/iframe-child.html +88 -0
  11. package/dist/nuxt/module.d.ts +6 -0
  12. package/dist/nuxt/module.mjs +28 -0
  13. package/dist/nuxt/plugin.mjs +9 -0
  14. package/dist/nuxt/runtime/plugin.d.ts +2 -0
  15. package/dist/nuxt/runtime/plugin.mjs +9 -0
  16. package/dist/src/lib/StackTabs.vue.d.ts +30 -16
  17. package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
  18. package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
  19. package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
  20. package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
  21. package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
  22. package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
  23. package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
  24. package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
  25. package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
  26. package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
  27. package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -23
  28. package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
  29. package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
  30. package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
  31. package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
  32. package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -73
  33. package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
  34. package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
  35. package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
  36. package/dist/src/lib/hooks/useTabPanel.d.ts +17 -74
  37. package/dist/src/lib/hooks/useTabRouter.d.ts +5 -5
  38. package/dist/src/lib/i18n/index.d.ts +6 -4
  39. package/dist/src/lib/i18n/lang/en.d.ts +5 -0
  40. package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
  41. package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
  42. package/dist/src/lib/iframe-bridge.d.ts +8 -0
  43. package/dist/src/lib/iframe.vue.d.ts +2 -1
  44. package/dist/src/lib/index.d.ts +4 -5
  45. package/dist/src/lib/model/TabModel.d.ts +14 -33
  46. package/dist/src/lib/nuxt/module.d.ts +6 -0
  47. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
  48. package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
  49. package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
  50. package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
  51. package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
  52. package/dist/src/lib/utils/tabInfoEncoder.d.ts +1 -16
  53. package/dist/src/lib/utils/typeGuards.d.ts +2 -0
  54. package/dist/src/lib/utils/urlParser.d.ts +24 -3
  55. package/dist/src/lib/versionLogger.d.ts +1 -0
  56. package/dist/style.css +2 -1
  57. package/dist/utils/iframeBridge.d.ts +39 -0
  58. package/dist/vue-stack-tabs.css +2 -1
  59. package/dist/vue-stack-tabs.es.js +1 -2411
  60. package/package.json +68 -47
  61. package/src/lib/StackTabs.vue +216 -192
  62. package/src/lib/assets/style/stackTab.scss +84 -12
  63. package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
  64. package/src/lib/components/ContextMenu/index.vue +94 -10
  65. package/src/lib/components/PageLoading.vue +9 -7
  66. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
  67. package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
  68. package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
  69. package/src/lib/components/TabHeader/TabHeaderItem.vue +45 -16
  70. package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
  71. package/src/lib/components/TabHeader/index.vue +109 -12
  72. package/src/lib/hooks/stackTabsContext.ts +135 -0
  73. package/src/lib/hooks/tabPanel/evict.ts +123 -41
  74. package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
  75. package/src/lib/hooks/tabPanel/refresh.ts +109 -0
  76. package/src/lib/hooks/tabPanel/scroll.ts +57 -36
  77. package/src/lib/hooks/tabPanel/session.ts +84 -22
  78. package/src/lib/hooks/tabPanel/state.ts +41 -44
  79. package/src/lib/hooks/useIframeManager.ts +253 -0
  80. package/src/lib/hooks/useTabActions.ts +62 -30
  81. package/src/lib/hooks/useTabEventBus.ts +29 -19
  82. package/src/lib/hooks/useTabLoading.ts +3 -2
  83. package/src/lib/hooks/useTabPanel.tsx +415 -213
  84. package/src/lib/hooks/useTabRouter.ts +172 -43
  85. package/src/lib/i18n/index.ts +53 -45
  86. package/src/lib/i18n/lang/en.ts +6 -1
  87. package/src/lib/i18n/lang/zh-CN.ts +6 -1
  88. package/src/lib/i18n/stackTabsLocale.ts +8 -0
  89. package/src/lib/iframe-bridge.ts +12 -0
  90. package/src/lib/index.ts +14 -19
  91. package/src/lib/model/TabModel.ts +34 -86
  92. package/src/lib/nuxt/module.d.ts +4 -0
  93. package/src/lib/nuxt/module.ts +8 -3
  94. package/src/lib/nuxt/runtime/plugin.client.ts +2 -3
  95. package/src/lib/nuxt/runtime/plugin.ts +11 -0
  96. package/src/lib/utils/iframeBridge.ts +29 -5
  97. package/src/lib/utils/scrollUtils.ts +9 -4
  98. package/src/lib/utils/stackTabsA11y.ts +12 -0
  99. package/src/lib/utils/stackTabsMessage.ts +99 -0
  100. package/src/lib/utils/tabInfoEncoder.ts +69 -42
  101. package/src/lib/utils/typeGuards.ts +3 -0
  102. package/src/lib/utils/urlParser.ts +144 -14
  103. package/src/lib/versionLogger.ts +7 -19
  104. package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
  105. package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
  106. package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
  107. package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
  108. package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
  109. package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
  110. package/dist/vue-stack-tabs.umd.js +0 -1
  111. package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
  112. package/src/lib/hooks/useTabPanel.spec.ts +0 -34
  113. package/src/lib/model/TabModel.spec.ts +0 -40
  114. package/src/lib/utils/iframeBridge.spec.ts +0 -37
  115. package/src/lib/utils/tabInfoEncoder.spec.ts +0 -42
  116. package/src/lib/utils/urlParser.spec.ts +0 -45
package/README.md CHANGED
@@ -1,56 +1,38 @@
1
- # Vue Stack Tabs
1
+ # vue-stack-tabs
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/vue-stack-tabs.svg)](https://www.npmjs.com/package/vue-stack-tabs)
4
- [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
3
+ **中文** | [English](./README.en.md) | [Русский](./README.ru.md)
5
4
 
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.
5
+ > Vue 3 多标签页管理库,基于 Vue Router。用 Vue 作用域实现类似 iframe TabPanel 的效果——**每个 Tab 间组件作用域互不干扰**。
7
6
 
8
- 基于 Vue 3 和 Vue Router 的多标签页管理库。每个标签拥有独立的组件栈,作用域互不干扰,适合后台管理、多页签工作台等场景。
7
+ ## ✨ 特性
9
8
 
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
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
- ## Installation / 安装
21
+ ## 📦 安装
40
22
 
41
23
  ```bash
42
- pnpm add vue-stack-tabs
43
- # or
24
+ # npm
44
25
  npm install vue-stack-tabs
45
- ```
46
26
 
47
- **Peer dependencies:** Vue 3, Vue Router 5.x
27
+ # pnpm
28
+ pnpm add vue-stack-tabs
29
+ ```
48
30
 
49
31
  ---
50
32
 
51
- ## Quick Start / 快速开始
33
+ ## 🚀 快速接入(Vue 3)
52
34
 
53
- ### Vue
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
- ```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--路由配置)).
51
+ > `vue-stack-tabs` 每个 Vue app 只支持一个 `<VueStackTabs>` 实例。默认导入 `VueStackTabs` 是 Vue plugin,用于 `app.use(VueStackTabs)`;如果需要本地组件导入,请使用 named export。
77
52
 
78
- ### Route Config / 路由配置
53
+ ### 2. 配置路由
79
54
 
80
- The parent route must wrap `<vue-stack-tabs>`, and the iframe path route must be registered.
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: Frame, // contains <vue-stack-tabs>
66
+ component: () => import('./Layout.vue'),
94
67
  children: [
95
- { path: '', component: () => import('@/views/home.vue') },
96
- { path: 'iframe', component: IFrame }, // must match iframe-path
97
- { path: ':pathMatch(.*)', redirect: '/404' }
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
- ## API
81
+ ### 3. 使用组件
127
82
 
128
- ### useTabActions
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
- ```ts
131
- import { useTabActions } from 'vue-stack-tabs'
91
+ <script setup lang="ts">
92
+ import type { ITabData } from 'vue-stack-tabs'
132
93
 
133
- const {
134
- openTab,
135
- closeTab,
136
- closeAllTabs,
137
- refreshTab,
138
- refreshAllTabs,
139
- activeTab,
140
- reset,
141
- tabs,
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
- | 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.
105
+ 如果需要本地组件导入,推荐使用 `VueStackTabs` named export;`StackTab` named export 会继续保留作为兼容别名:
173
106
 
174
107
  ```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')
108
+ import { VueStackTabs as VueStackTabsComponent } from 'vue-stack-tabs'
189
109
  ```
190
110
 
191
- ### useTabLoading
111
+ ### 4. 打开标签
192
112
 
193
113
  ```ts
194
- import { useTabLoading } from 'vue-stack-tabs'
114
+ import { useTabActions } from 'vue-stack-tabs'
195
115
 
196
- const { openTabLoading, closeTabLoading } = useTabLoading()
197
- ```
116
+ const { openTab, closeTab, refreshTab } = useTabActions()
198
117
 
199
- ### Initial Tabs / 初始化页签
118
+ // 打开新标签
119
+ openTab({
120
+ id: 'about', // 可选,不传则自动生成
121
+ title: '关于',
122
+ path: '/about',
123
+ query: { id: '1' } // 可选
124
+ })
200
125
 
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
- ```
126
+ // 关闭标签
127
+ closeTab('about')
210
128
 
211
- > The first tab should be non-closable (`closable: false`).
129
+ // 刷新标签
130
+ refreshTab('about')
131
+ ```
212
132
 
213
133
  ---
214
134
 
215
- ## Nuxt
135
+ ## 🚀 快速接入(Nuxt 3/4)
216
136
 
217
- ### Module (recommended)
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
- 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:
150
+ ### 2. 在 Layout 中使用
267
151
 
268
152
  ```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
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
- ```ts
278
- import { onRefreshRequest } from 'vue-stack-tabs'
279
- import { onUnmounted } from 'vue'
160
+ <script setup lang="ts">
161
+ import type { ITabData } from 'vue-stack-tabs'
280
162
 
281
- const unlisten = onRefreshRequest() // defaults to location.reload()
282
- onUnmounted(unlisten)
163
+ const defaultTabs: ITabData[] = [{ title: '首页', path: '/', closable: false, refreshable: true }]
164
+ </script>
283
165
  ```
284
166
 
285
- ### Cross-origin iframe (reload mode)
167
+ ### 3. 页面中使用
286
168
 
287
- When the iframe is cross-origin and you cannot modify its code:
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
- ```ts
290
- openTab({
291
- title: 'External',
292
- path: 'https://example.com',
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
- ## i18n / 国际化
186
+ ## ESM-only 与 iframe bridge 子入口
301
187
 
302
- Built-in: `zh-CN`, `en`.
188
+ `vue-stack-tabs` 从当前版本开始只承诺 ESM import,不再支持 `require('vue-stack-tabs')`。
303
189
 
304
- ```vue
305
- <vue-stack-tabs iframe-path="/iframe" i18n="zh-CN" />
306
- ```
307
-
308
- ### Custom messages
190
+ 主应用中继续使用 root 入口:
309
191
 
310
192
  ```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
- }])
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
- | Slot | Description |
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
- ## Events
207
+ const off = onRefreshRequest(() => window.location.reload(), {
208
+ allowedOrigins: ['https://parent.example.com']
209
+ })
210
+ ```
344
211
 
345
- | Event | Payload | Description |
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
- ## Exports
216
+ ## iframe 安全策略
353
217
 
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'
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
- ## Sub-routes (Experimental / 子路由试验性)
234
+ ## 📖 API 参考
372
235
 
373
- Nested routes with `children` and `forward`/`backward` are supported. See [SUBROUTE_EXPERIMENTAL.md](./docs/SUBROUTE_EXPERIMENTAL.md) for details. This feature is experimental.
236
+ ### useTabActions
374
237
 
375
- ---
238
+ 标签级操作封装。
376
239
 
377
- ## Development / 开发
240
+ ```ts
241
+ import { useTabActions } from 'vue-stack-tabs'
378
242
 
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
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
- See [CONTRIBUTING.md](./CONTRIBUTING.md). [AGENTS.md](./AGENTS.md) for AI assistants.
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
- // 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
- ### 快速开始
272
+ import { useTabRouter } from 'vue-stack-tabs'
413
273
 
414
- ```vue
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
- 父路由需包裹 `<vue-stack-tabs>`,并注册与 `iframe-path` 一致的 iframe 路由。
277
+ | 方法 | 说明 |
278
+ | --------------------------- | --------------------------------- |
279
+ | `forward(to)` | 在当前标签内前进到新页面 |
280
+ | `backward(to, backQuery?)` | 在当前标签内后退 |
281
+ | `addScrollTarget(selector)` | 注册需要记忆滚动位置的 DOM 选择器 |
422
282
 
423
- ### 路由配置
283
+ #### forward
424
284
 
425
285
  ```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'
286
+ // 前进到 /detail 页面
287
+ forward({ path: '/detail', query: { id: '123' } })
430
288
 
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
- })
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
- import { useTabActions } from 'vue-stack-tabs'
296
+ // 后退 1 步(默认)
297
+ backward(1)
471
298
 
472
- const {
473
- openTab,
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
- ```ts
514
- import { useTabRouter } from 'vue-stack-tabs'
305
+ // 带参回退(目标页通过 props._back 接收)
306
+ backward('/list', { result: 'success', data: { id: 1 } })
307
+ ```
515
308
 
516
- const { forward, backward, addScrollTarget } = useTabRouter()
309
+ **目标页接收参数:**
517
310
 
518
- // 前进:入栈新页面
519
- forward({ path: '/detail', query: { id: '1' } })
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
- #### useTabLoading
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
- ```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
- />
332
+ // 异步操作完成后关闭
333
+ fetchData().finally(() => closeTabLoading())
548
334
  ```
549
335
 
550
- > 第一个页签建议设为不可关闭(`closable: false`)。
336
+ > 组件卸载时自动关闭 Loading,无需手动清理。
551
337
 
552
- ### Nuxt
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
- ```ts
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
- 模块会自动导入 `useTabActions`、`useTabRouter`、`useTabLoading`。
367
+ ## 🎯 Events
567
368
 
568
- #### Nuxt 中配置 iframe 路由
369
+ | 事件 | 参数 | 说明 |
370
+ | -------------- | -------------- | ---------------------- |
371
+ | `onActive` | `(id: string)` | 标签被激活时触发 |
372
+ | `onPageLoaded` | — | 页面组件加载完成时触发 |
569
373
 
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
- ```
374
+ ---
584
375
 
585
- ### iframe 与父窗口通信
376
+ ## 🖼️ iframe 标签页
586
377
 
587
- #### iframe 内打开新标签
378
+ ### 打开 iframe 标签
588
379
 
589
380
  ```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
381
+ openTab({
382
+ title: '外部页面',
383
+ path: 'https://example.com',
384
+ iframe: true
598
385
  })
599
386
  ```
600
387
 
601
- 跨域时父应用需配置 `iframeAllowedOrigins`:
388
+ ### iframe 页面内操作
602
389
 
603
- ```vue
604
- <vue-stack-tabs
605
- iframe-path="/iframe"
606
- :iframe-allowed-origins="['https://iframe来源域名']"
607
- />
608
- ```
390
+ 在 iframe 标签页内,可以通过 `postMessage` 与宿主容器进行通信,实现打开新标签或响应刷新操作。
391
+
392
+ #### 1. 使用 Bridge 工具(推荐)
609
393
 
610
- #### iframe 内接收刷新指令
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 unlisten = onRefreshRequest() // 默认执行 location.reload()
617
- onUnmounted(unlisten)
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
- #### 跨域 iframe(reload 模式)
421
+ #### 2. 原生对接方式
621
422
 
622
- 若 iframe 为跨域且无法修改其代码,可指定 `iframeRefreshMode: 'reload'`,刷新时直接重载 iframe:
423
+ 如果无法引入库工具,或希望保持零依赖,可以使用原生 API。生产环境必须使用明确的父页面 origin,并校验刷新消息来源。`postOpenTab` 的默认 `targetOrigin` 是 iframe 页面的 `window.location.origin`,跨域父页面必须显式传入父页面 origin:
623
424
 
624
425
  ```ts
625
- openTab({
626
- title: '外部页面',
627
- path: 'https://example.com',
628
- iframe: true,
629
- iframeRefreshMode: 'reload'
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
- 内置 `zh-CN`、`en`。
453
+ 在 `openTab` 时可通过 `iframeRefreshMode` 指定刷新行为:
636
454
 
637
- ```vue
638
- <vue-stack-tabs iframe-path="/iframe" i18n="zh-CN" />
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
- 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: '新窗口打开'
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
- 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'
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
- ```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 # 文档预览
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
- 参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。[AGENTS.md](./AGENTS.md) 供 AI 助手参考。
528
+ 详细架构设计请参阅 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。
712
529
 
713
530
  ---
714
531
 
715
- ## License / 许可证
532
+ ## 📝 License
716
533
 
717
- Apache License 2.0
534
+ [LGPL-2.1](./LICENSE)