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 CHANGED
@@ -1,62 +1,42 @@
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
+ > Vue 3 多标签页管理库,基于 Vue Router。用 Vue 作用域实现类似 iframe TabPanel 的效果——**每个 Tab 间组件作用域互不干扰**。
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
+ ## ✨ 特性
7
6
 
8
- 基于 Vue 3 和 Vue Router 的多标签页管理库。每个标签拥有独立的组件栈,作用域互不干扰,适合后台管理、多页签工作台等场景。
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
- ## Table of Contents / 目录
13
-
14
- - [Features / 特性](#features--特性)
15
- - [Installation / 安装](#installation--安装)
16
- - [Quick Start / 快速开始](#quick-start--快速开始)
17
- - [Props / 属性](#props--属性)
18
- - [API](#api)
19
- - [Nuxt](#nuxt)
20
- - [iframe & PostMessage](#iframe--postmessage)
21
- - [i18n / 国际化](#i18n--国际化)
22
- - [Sub-routes / 子路由](#sub-routes-experimental--子路由试验性)
23
- - [完整中文说明](#完整中文说明)
24
- - [License / 许可证](#license--许可证)
25
-
26
- ---
27
-
28
- ## Features / 特性
29
-
30
- - **Route-based & iframe tabs** — Tab pages from Vue Router routes or embedded iframes
31
- - **Tab operations** — Add, close, refresh, batch close (all / left / right / others)
32
- - **Stack navigation** — Forward / backward within a tab
33
- - **Scroll position** — Remember and restore scroll per page
34
- - **Session recovery** — Restore last active tab on refresh
35
- - **Nuxt 3/4 module** — Official Nuxt integration
36
-
37
- ---
38
-
39
- ## Installation / 安装
19
+ ## 📦 安装
40
20
 
41
21
  ```bash
42
- pnpm add vue-stack-tabs
43
- # or
22
+ # npm
44
23
  npm install vue-stack-tabs
45
- ```
46
24
 
47
- **Peer dependencies:** Vue 3, Vue Router 5.x
25
+ # pnpm
26
+ pnpm add vue-stack-tabs
27
+ ```
48
28
 
49
29
  ---
50
30
 
51
- ## Quick Start / 快速开始
31
+ ## 🚀 快速接入(Vue 3)
52
32
 
53
- ### Vue
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/style.css'
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
- ```vue
70
- <!-- App.vue or layout -->
71
- <template>
72
- <vue-stack-tabs iframe-path="/iframe" />
73
- </template>
74
- ```
75
-
76
- Configure the iframe route to match `iframe-path` (see [Route config](#route-config--路由配置)).
77
-
78
- ### Route Config / 路由配置
49
+ ### 2. 配置路由
79
50
 
80
- The parent route must wrap `<vue-stack-tabs>`, and the iframe path route must be registered.
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: Frame, // contains <vue-stack-tabs>
62
+ component: () => import('./Layout.vue'),
94
63
  children: [
95
- { path: '', component: () => import('@/views/home.vue') },
96
- { path: 'iframe', component: IFrame }, // must match iframe-path
97
- { path: ':pathMatch(.*)', redirect: '/404' }
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
- ```ts
131
- import { useTabActions } from 'vue-stack-tabs'
132
-
133
- const {
134
- openTab,
135
- closeTab,
136
- closeAllTabs,
137
- refreshTab,
138
- refreshAllTabs,
139
- activeTab,
140
- reset,
141
- tabs,
142
- openInNewWindow
143
- } = useTabActions()
74
+ export default router
144
75
  ```
145
76
 
146
- | Method | Description |
147
- |--------|-------------|
148
- | `openTab(tab, renew?)` | Open tab. `renew=true` clears stack and reopens |
149
- | `closeTab(id)` | Close tab |
150
- | `closeAllTabs()` | Close all tabs |
151
- | `refreshTab(id)` | Refresh current page of tab |
152
- | `refreshAllTabs()` | Refresh all tabs |
153
- | `activeTab(id, isRoute?)` | Activate tab |
154
- | `reset()` | Reset all tabs |
155
- | `openInNewWindow(id)` | Open iframe tab in new window (fallback) |
156
-
157
- **openTab(tab: ITabData)**
158
-
159
- | Field | Type | Required | Description |
160
- |-------|------|----------|-------------|
161
- | `id` | `string` | No | Tab id (auto-generated if omitted) |
162
- | `title` | `string` | Yes | Tab title |
163
- | `path` | `string` | Yes | Route path or iframe URL |
164
- | `query` | `Record<string, string>` | No | Query params |
165
- | `iframe` | `boolean` | No | Open as iframe |
166
- | `iframeRefreshMode` | `'postMessage' \| 'reload'` | No | iframe refresh mode |
167
- | `closable` | `boolean` | No | Default `true` |
168
- | `refreshable` | `boolean` | No | Default `true` |
169
-
170
- ### useTabRouter
171
-
172
- Used **inside tab pages** for in-tab navigation.
173
-
174
- ```ts
175
- import { useTabRouter } from 'vue-stack-tabs'
77
+ ### 3. 使用组件
176
78
 
177
- const { forward, backward, addScrollTarget } = useTabRouter()
178
-
179
- // Forward: push new page
180
- forward({ path: '/detail', query: { id: '1' } })
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
- // 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
87
+ <script setup lang="ts">
88
+ import type { ITabData } from 'vue-stack-tabs'
186
89
 
187
- // Register scroll containers for position restore
188
- addScrollTarget('.scroll-area', '#panel')
90
+ const defaultTabs: ITabData[] = [
91
+ {
92
+ title: '首页',
93
+ path: '/',
94
+ closable: false,
95
+ refreshable: true
96
+ }
97
+ ]
98
+ </script>
189
99
  ```
190
100
 
191
- ### useTabLoading
101
+ ### 4. 打开标签
192
102
 
193
103
  ```ts
194
- import { useTabLoading } from 'vue-stack-tabs'
104
+ import { useTabActions } from 'vue-stack-tabs'
195
105
 
196
- const { openTabLoading, closeTabLoading } = useTabLoading()
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
- ### Initial Tabs / 初始化页签
116
+ // 关闭标签
117
+ closeTab('about')
200
118
 
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
- />
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
- ### Module (recommended)
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
- <vue-stack-tabs
270
- iframe-path="/iframe"
271
- :iframe-allowed-origins="['https://iframe-origin.com']"
272
- />
273
- ```
274
-
275
- ### Receive refresh in iframe
276
-
277
- ```ts
278
- import { onRefreshRequest } from 'vue-stack-tabs'
279
- import { onUnmounted } from 'vue'
280
-
281
- const unlisten = onRefreshRequest() // defaults to location.reload()
282
- onUnmounted(unlisten)
283
- ```
284
-
285
- ### Cross-origin iframe (reload mode)
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
- When the iframe is cross-origin and you cannot modify its code:
150
+ <script setup lang="ts">
151
+ import type { ITabData } from 'vue-stack-tabs'
288
152
 
289
- ```ts
290
- openTab({
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
- <vue-stack-tabs iframe-path="/iframe" i18n="zh-CN" />
306
- ```
307
-
308
- ### Custom messages
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
- ```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
- }])
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
- ## Slots
335
-
336
- | Slot | Description |
337
- |------|-------------|
338
- | `leftButton` | Left side of tab bar |
339
- | `rightButton` | Right side of tab bar |
340
-
341
- ---
342
-
343
- ## Events
344
-
345
- | Event | Payload | Description |
346
- |-------|---------|-------------|
347
- | `onActive` | `id: string` | Fired when a tab is activated |
348
- | `onPageLoaded` | — | Fired when page component is loaded |
176
+ ## 📖 API 参考
349
177
 
350
- ---
178
+ ### useTabActions
351
179
 
352
- ## Exports
180
+ 标签级操作封装。
353
181
 
354
182
  ```ts
355
- import VueStackTabs, {
356
- useTabActions,
357
- useTabRouter,
358
- useTabLoading,
359
- TabHeaderButton,
360
- IFrame,
361
- postOpenTab,
362
- onRefreshRequest,
363
- MSG_REFRESH,
364
- MSG_OPEN_TAB
365
- } from 'vue-stack-tabs'
366
- import type { ITabData, ITabBase, IframeOpenTabPayload } from 'vue-stack-tabs'
367
- ```
368
-
369
- ---
370
-
371
- ## Sub-routes (Experimental / 子路由试验性)
372
-
373
- Nested routes with `children` and `forward`/`backward` are supported. See [SUBROUTE_EXPERIMENTAL.md](./docs/SUBROUTE_EXPERIMENTAL.md) for details. This feature is experimental.
374
-
375
- ---
376
-
377
- ## Development / 开发
183
+ import { useTabActions } from 'vue-stack-tabs'
378
184
 
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
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
- See [CONTRIBUTING.md](./CONTRIBUTING.md). [AGENTS.md](./AGENTS.md) for AI assistants.
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
- // 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
- ### 快速开始
214
+ import { useTabRouter } from 'vue-stack-tabs'
413
215
 
414
- ```vue
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
- 父路由需包裹 `<vue-stack-tabs>`,并注册与 `iframe-path` 一致的 iframe 路由。
219
+ | 方法 | 说明 |
220
+ | --------------------------- | --------------------------------- |
221
+ | `forward(to)` | 在当前标签内前进到新页面 |
222
+ | `backward(to, backQuery?)` | 在当前标签内后退 |
223
+ | `addScrollTarget(selector)` | 注册需要记忆滚动位置的 DOM 选择器 |
422
224
 
423
- ### 路由配置
225
+ #### forward
424
226
 
425
227
  ```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'
228
+ // 前进到 /detail 页面
229
+ forward({ path: '/detail', query: { id: '123' } })
430
230
 
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
- })
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
- import { useTabActions } from 'vue-stack-tabs'
238
+ // 后退 1 步(默认)
239
+ backward(1)
471
240
 
472
- const {
473
- openTab,
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
- ```ts
514
- import { useTabRouter } from 'vue-stack-tabs'
247
+ // 带参回退(目标页通过 props._back 接收)
248
+ backward('/list', { result: 'success', data: { id: 1 } })
249
+ ```
515
250
 
516
- const { forward, backward, addScrollTarget } = useTabRouter()
251
+ **目标页接收参数:**
517
252
 
518
- // 前进:入栈新页面
519
- forward({ path: '/detail', query: { id: '1' } })
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
- #### useTabLoading
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
- ```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
- />
274
+ // 异步操作完成后关闭
275
+ fetchData().finally(() => closeTabLoading())
548
276
  ```
549
277
 
550
- > 第一个页签建议设为不可关闭(`closable: false`)。
278
+ > 组件卸载时自动关闭 Loading,无需手动清理。
551
279
 
552
- ### Nuxt
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
- ```ts
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
- 模块会自动导入 `useTabActions`、`useTabRouter`、`useTabLoading`。
305
+ ## 🎯 Events
567
306
 
568
- #### Nuxt 中配置 iframe 路由
307
+ | 事件 | 参数 | 说明 |
308
+ | -------------- | -------------- | ---------------------- |
309
+ | `onActive` | `(id: string)` | 标签被激活时触发 |
310
+ | `onPageLoaded` | — | 页面组件加载完成时触发 |
311
+
312
+ ---
313
+
314
+ ## 🖼️ iframe 标签页
315
+
316
+ ### 打开 iframe 标签
569
317
 
570
318
  ```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
- }
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
- #### iframe 内打开新标签
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: '123' },
596
- closable: true,
597
- refreshable: true
337
+ query: { id: '1' }
598
338
  })
599
339
  ```
600
340
 
601
- 跨域时父应用需配置 `iframeAllowedOrigins`:
341
+ ### iframe 刷新
602
342
 
603
- ```vue
604
- <vue-stack-tabs
605
- iframe-path="/iframe"
606
- :iframe-allowed-origins="['https://iframe来源域名']"
607
- />
608
- ```
343
+ iframe 刷新有两种模式:
609
344
 
610
- #### iframe 内接收刷新指令
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
- const unlisten = onRefreshRequest() // 默认执行 location.reload()
617
- onUnmounted(unlisten)
618
- ```
619
-
620
- #### 跨域 iframe(reload 模式)
621
-
622
- 若 iframe 为跨域且无法修改其代码,可指定 `iframeRefreshMode: 'reload'`,刷新时直接重载 iframe:
623
-
624
- ```ts
625
- openTab({
626
- title: '外部页面',
627
- path: 'https://example.com',
628
- iframe: true,
629
- iframeRefreshMode: 'reload'
352
+ onRefreshRequest(() => {
353
+ // 执行刷新逻辑
354
+ location.reload()
630
355
  })
631
356
  ```
632
357
 
633
- ### 国际化
358
+ ---
359
+
360
+ ## 🌍 国际化
634
361
 
635
- 内置 `zh-CN`、`en`。
362
+ 内置语言:`zh-CN`(中文)、`en-US`(英文)。
636
363
 
637
- ```vue
638
- <vue-stack-tabs iframe-path="/iframe" i18n="zh-CN" />
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
- 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: '新窗口打开'
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
- 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'
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
- ```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 # 文档预览
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
- 参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。[AGENTS.md](./AGENTS.md) 供 AI 助手参考。
428
+ 详细架构设计请参阅 [`ARCHITECTURE.md`](./ARCHITECTURE.md)。
712
429
 
713
430
  ---
714
431
 
715
- ## License / 许可证
432
+ ## 📝 License
716
433
 
717
- Apache License 2.0
434
+ [LGPL-2.1](./LICENSE)