vue-stack-tabs 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +526 -0
- package/README.md +141 -39
- 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 +19 -6
- 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 -61
- 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 -72
- 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 +14 -73
- 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 +11 -40
- 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 +0 -15
- 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 -2220
- package/package.json +71 -50
- package/src/lib/StackTabs.vue +223 -196
- package/src/lib/assets/style/stackTab.scss +110 -13
- 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 +74 -17
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
- package/src/lib/components/TabHeader/index.vue +181 -13
- package/src/lib/hooks/stackTabsContext.ts +135 -0
- package/src/lib/hooks/tabPanel/evict.ts +122 -87
- 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 -40
- 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 +64 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +290 -313
- package/src/lib/hooks/useTabRouter.ts +111 -97
- 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 +27 -82
- 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 +1 -0
- 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 +70 -21
- 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 -32
- package/src/lib/utils/urlParser.spec.ts +0 -45
package/README.ru.md
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# vue-stack-tabs
|
|
2
|
+
|
|
3
|
+
[中文](./README.md) | [English](./README.en.md) | **Русский**
|
|
4
|
+
|
|
5
|
+
> Библиотека управления вкладками для Vue 3 на основе Vue Router. Реализует поведение TabPanel, аналогичное iframe, с использованием областей видимости Vue — **каждая вкладка имеет изолированную область компонентов**.
|
|
6
|
+
|
|
7
|
+
## Возможности
|
|
8
|
+
|
|
9
|
+
- **Вкладки на уровне маршрутов** — каждая вкладка владеет независимыми компонентами и кэшем
|
|
10
|
+
- **Стековая навигация внутри вкладки** — forward / backward внутри вкладки, аналогично стеку истории браузера
|
|
11
|
+
- **Обновление вкладок** — обновление одной или всех вкладок с полной пересборкой экземпляров компонентов
|
|
12
|
+
- **Вкладки iframe** — встраивание iframe-страниц с коммуникацией через postMessage
|
|
13
|
+
- **Сохранение сессии** — восстановление последних активных вкладок после обновления браузера
|
|
14
|
+
- **Запоминание позиции прокрутки** — автоматическое восстановление позиции прокрутки при переключении вкладок
|
|
15
|
+
- **Контекстное меню** — встроенные действия закрытия/обновления
|
|
16
|
+
- **Интернационализация** — встроенные китайский и английский языки, расширяемо
|
|
17
|
+
- **Модуль Nuxt 3/4** — работает из коробки
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Установка
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# npm
|
|
25
|
+
npm install vue-stack-tabs
|
|
26
|
+
|
|
27
|
+
# pnpm
|
|
28
|
+
pnpm add vue-stack-tabs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Быстрый старт (Vue 3)
|
|
34
|
+
|
|
35
|
+
### 1. Регистрация плагина
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// main.ts
|
|
39
|
+
import { createApp } from 'vue'
|
|
40
|
+
import VueStackTabs from 'vue-stack-tabs'
|
|
41
|
+
import 'vue-stack-tabs/dist/vue-stack-tabs.css'
|
|
42
|
+
import App from './App.vue'
|
|
43
|
+
import router from './router'
|
|
44
|
+
|
|
45
|
+
const app = createApp(App)
|
|
46
|
+
app.use(router)
|
|
47
|
+
app.use(VueStackTabs)
|
|
48
|
+
app.mount('#app')
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Настройка маршрутов
|
|
52
|
+
|
|
53
|
+
Вкладки зависят от Vue Router. Необходим родительский маршрут для размещения `<VueStackTabs>`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// router.ts
|
|
57
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
58
|
+
|
|
59
|
+
const router = createRouter({
|
|
60
|
+
history: createWebHistory(),
|
|
61
|
+
routes: [
|
|
62
|
+
{
|
|
63
|
+
path: '/',
|
|
64
|
+
component: () => import('./Layout.vue'),
|
|
65
|
+
children: [
|
|
66
|
+
{ path: '', component: () => import('./pages/Home.vue') },
|
|
67
|
+
{ path: 'about', component: () => import('./pages/About.vue') },
|
|
68
|
+
{ path: 'settings', component: () => import('./pages/Settings.vue') },
|
|
69
|
+
// маршрут-заглушка для iframe (обязательно)
|
|
70
|
+
{ path: 'iframe', component: () => import('vue-stack-tabs').then((m) => m.IFrame) }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export default router
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Использование компонента
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<!-- Layout.vue -->
|
|
83
|
+
<template>
|
|
84
|
+
<div style="width: 100%; height: 100vh">
|
|
85
|
+
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" :max="20" :contextmenu="true" />
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script setup lang="ts">
|
|
90
|
+
import type { ITabData } from 'vue-stack-tabs'
|
|
91
|
+
|
|
92
|
+
const defaultTabs: ITabData[] = [
|
|
93
|
+
{
|
|
94
|
+
title: 'Главная',
|
|
95
|
+
path: '/',
|
|
96
|
+
closable: false,
|
|
97
|
+
refreshable: true
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
</script>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Открытие вкладок
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
107
|
+
|
|
108
|
+
const { openTab, closeTab, refreshTab } = useTabActions()
|
|
109
|
+
|
|
110
|
+
// Открыть новую вкладку
|
|
111
|
+
openTab({
|
|
112
|
+
id: 'about', // необязательно, генерируется автоматически
|
|
113
|
+
title: 'О приложении',
|
|
114
|
+
path: '/about',
|
|
115
|
+
query: { id: '1' } // необязательно
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Закрыть вкладку
|
|
119
|
+
closeTab('about')
|
|
120
|
+
|
|
121
|
+
// Обновить вкладку
|
|
122
|
+
refreshTab('about')
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Быстрый старт (Nuxt 3/4)
|
|
128
|
+
|
|
129
|
+
### 1. Регистрация модуля
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// nuxt.config.ts
|
|
133
|
+
export default defineNuxtConfig({
|
|
134
|
+
modules: ['vue-stack-tabs/nuxt'],
|
|
135
|
+
vueStackTabs: {
|
|
136
|
+
locale: 'en-US'
|
|
137
|
+
},
|
|
138
|
+
css: ['vue-stack-tabs/dist/vue-stack-tabs.css']
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2. Использование в Layout
|
|
143
|
+
|
|
144
|
+
```vue
|
|
145
|
+
<!-- layouts/default.vue -->
|
|
146
|
+
<template>
|
|
147
|
+
<div style="width: 100%; height: 100vh">
|
|
148
|
+
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" />
|
|
149
|
+
</div>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<script setup lang="ts">
|
|
153
|
+
import type { ITabData } from 'vue-stack-tabs'
|
|
154
|
+
|
|
155
|
+
const defaultTabs: ITabData[] = [{ title: 'Главная', path: '/', closable: false, refreshable: true }]
|
|
156
|
+
</script>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 3. Использование на страницах
|
|
160
|
+
|
|
161
|
+
```vue
|
|
162
|
+
<!-- pages/index.vue -->
|
|
163
|
+
<template>
|
|
164
|
+
<div>
|
|
165
|
+
<h1>Главная</h1>
|
|
166
|
+
<button @click="openTab({ title: 'О приложении', path: '/about' })">Открыть страницу «О приложении»</button>
|
|
167
|
+
</div>
|
|
168
|
+
</template>
|
|
169
|
+
|
|
170
|
+
<script setup>
|
|
171
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
172
|
+
const { openTab } = useTabActions()
|
|
173
|
+
</script>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## ESM-only и суб-вход iframe bridge
|
|
179
|
+
|
|
180
|
+
`vue-stack-tabs` начиная с текущей версии поддерживает только ESM import; `require('vue-stack-tabs')` больше не поддерживается.
|
|
181
|
+
|
|
182
|
+
Основное приложение продолжает использовать корневой вход:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import VueStackTabs from 'vue-stack-tabs'
|
|
186
|
+
import 'vue-stack-tabs/dist/vue-stack-tabs.css'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Для iframe-страниц используйте суб-вход без побочных эффектов стилей:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { postOpenTab, onRefreshRequest } from 'vue-stack-tabs/iframe-bridge'
|
|
193
|
+
|
|
194
|
+
postOpenTab(
|
|
195
|
+
{ title: 'Детали заказа', path: '/orders/1' },
|
|
196
|
+
{ targetOrigin: 'https://parent.example.com' }
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const off = onRefreshRequest(() => window.location.reload(), {
|
|
200
|
+
allowedOrigins: ['https://parent.example.com']
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Если `targetOrigin` не указан, по умолчанию используется `window.location.origin` страницы iframe, подходит для родительских страниц с тем же источником; для кросс-доменных родительских страниц необходимо явно передать `targetOrigin`. В продакшене рекомендуется явно передавать и `targetOrigin`, и `allowedOrigins`.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Политика безопасности iframe
|
|
209
|
+
|
|
210
|
+
`VueStackTabs` поддерживает настройку атрибутов безопасности iframe:
|
|
211
|
+
|
|
212
|
+
```vue
|
|
213
|
+
<VueStackTabs
|
|
214
|
+
iframe-path="/__iframe"
|
|
215
|
+
iframe-sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-same-origin"
|
|
216
|
+
iframe-referrer-policy="strict-origin-when-cross-origin"
|
|
217
|
+
iframe-allow="fullscreen"
|
|
218
|
+
:iframe-load-timeout="15000"
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Sandbox по умолчанию сохраняет типичные возможности бизнес-страниц и ориентирован на совместимость, не на строгую изоляцию. Для строгой изоляции удалите `allow-same-origin` и продолжайте уменьшать токены sandbox по потребностям бизнеса.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Справочник API
|
|
227
|
+
|
|
228
|
+
### useTabActions
|
|
229
|
+
|
|
230
|
+
Композабл для операций на уровне вкладок.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { useTabActions } from 'vue-stack-tabs'
|
|
234
|
+
|
|
235
|
+
const {
|
|
236
|
+
openTab, // (tab: ITabData, renew?: boolean) => Promise<string>
|
|
237
|
+
closeTab, // (id: string) => string
|
|
238
|
+
closeAllTabs, // () => void
|
|
239
|
+
refreshTab, // (id: string) => void
|
|
240
|
+
refreshAllTabs, // () => void
|
|
241
|
+
activeTab, // (id: string, isRoute?: boolean) => void
|
|
242
|
+
reset, // () => void
|
|
243
|
+
tabs // Ref<ITabItem[]>
|
|
244
|
+
} = useTabActions()
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| Метод | Описание |
|
|
248
|
+
| ---------------------- | --------------------------------------------------------------------------------- |
|
|
249
|
+
| `openTab(tab, renew?)` | Открыть новую вкладку. При `renew=true` очищает стек страниц и открывает заново |
|
|
250
|
+
| `closeTab(id)` | Закрыть указанную вкладку, возвращает ID новой активной вкладки |
|
|
251
|
+
| `closeAllTabs()` | Закрыть все закрываемые вкладки |
|
|
252
|
+
| `refreshTab(id)` | Обновить указанную вкладку (заменяет ID кэша, пересоздаёт экземпляр компонента) |
|
|
253
|
+
| `refreshAllTabs()` | Обновить все вкладки |
|
|
254
|
+
| `activeTab(id)` | Активировать указанную вкладку (переключить вкладку) |
|
|
255
|
+
| `reset()` | Закрыть все вкладки и сбросить состояние |
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### useTabRouter
|
|
260
|
+
|
|
261
|
+
Стековая навигация внутри вкладки. **Можно использовать только внутри компонентов страниц во вкладке**.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { useTabRouter } from 'vue-stack-tabs'
|
|
265
|
+
|
|
266
|
+
const { forward, backward, addScrollTarget } = useTabRouter()
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
| Метод | Описание |
|
|
270
|
+
| --------------------------- | ----------------------------------------------------------- |
|
|
271
|
+
| `forward(to)` | Перейти вперёд к новой странице внутри текущей вкладки |
|
|
272
|
+
| `backward(to, backQuery?)` | Вернуться назад внутри текущей вкладки |
|
|
273
|
+
| `addScrollTarget(selector)` | Зарегистрировать CSS-селектор DOM для запоминания прокрутки |
|
|
274
|
+
|
|
275
|
+
#### forward
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// Перейти на страницу /detail
|
|
279
|
+
forward({ path: '/detail', query: { id: '123' } })
|
|
280
|
+
|
|
281
|
+
// Добавить тот же маршрут в стек (один маршрут может быть добавлен многократно, каждый с независимым кэшем)
|
|
282
|
+
forward({ path: '/list', query: { page: '2' } })
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### backward
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// Вернуться на 1 шаг назад (по умолчанию)
|
|
289
|
+
backward(1)
|
|
290
|
+
|
|
291
|
+
// Вернуться на N шагов
|
|
292
|
+
backward(3)
|
|
293
|
+
|
|
294
|
+
// Вернуться к определённому пути (автоматический поиск в стеке)
|
|
295
|
+
backward('/list')
|
|
296
|
+
|
|
297
|
+
// Вернуться с параметрами (целевая страница получает через props._back)
|
|
298
|
+
backward('/list', { result: 'success', data: { id: 1 } })
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Получение параметров на целевой странице:**
|
|
302
|
+
|
|
303
|
+
```vue
|
|
304
|
+
<script setup>
|
|
305
|
+
// Объявить props для получения параметров обратной навигации
|
|
306
|
+
defineProps<{ _back?: { result: string; data: any } }>()
|
|
307
|
+
</script>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### useTabLoading
|
|
313
|
+
|
|
314
|
+
Управление состоянием загрузки страницы. **Можно использовать только внутри компонентов страниц во вкладке**.
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { useTabLoading } from 'vue-stack-tabs'
|
|
318
|
+
|
|
319
|
+
const { openTabLoading, closeTabLoading } = useTabLoading()
|
|
320
|
+
|
|
321
|
+
// Показать оверлей загрузки
|
|
322
|
+
openTabLoading()
|
|
323
|
+
|
|
324
|
+
// Закрыть после завершения асинхронной операции
|
|
325
|
+
fetchData().finally(() => closeTabLoading())
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
> Загрузка автоматически закрывается при размонтировании компонента — ручная очистка не требуется.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Свойства (Props)
|
|
333
|
+
|
|
334
|
+
Свойства компонента `<VueStackTabs>`:
|
|
335
|
+
|
|
336
|
+
| Свойство | Тип | По умолчанию | Описание |
|
|
337
|
+
| ----------------------- | --------------------------- | ----------------------- | ------------------------------------------------------------- |
|
|
338
|
+
| `iframePath` | `string` | **Обязательно** | Путь маршрута-заглушки для iframe |
|
|
339
|
+
| `iframeSandbox` | `string` | `allow-scripts allow-forms allow-popups allow-downloads allow-same-origin` | Политика sandbox для iframe; по умолчанию совместимость, не строгая изоляция — удалите `allow-same-origin` для строгой изоляции; пустая строка отключает sandbox (не рекомендуется) |
|
|
340
|
+
| `iframeReferrerPolicy` | `ReferrerPolicy` | `strict-origin-when-cross-origin` | Атрибут referrerpolicy для iframe |
|
|
341
|
+
| `iframeAllow` | `string` | `''` | Атрибут allow для iframe, например `fullscreen` |
|
|
342
|
+
| `iframeLoadTimeout` | `number` | `15000` | Тайм-аут загрузки iframe в мс |
|
|
343
|
+
| `defaultTabs` | `ITabData[]` | `[]` | Начальный список вкладок |
|
|
344
|
+
| `max` | `number` | `20` | Максимальное количество вкладок |
|
|
345
|
+
| `contextmenu` | `boolean \| object` | `true` | Включить контекстное меню |
|
|
346
|
+
| `pageTransition` | `string` | `'stack-tab-swap'` | Имя анимации перехода при навигации вперёд |
|
|
347
|
+
| `pageTransitionBack` | `string` | `'stack-tab-swap-back'` | Имя анимации перехода при навигации назад |
|
|
348
|
+
| `tabTransition` | `string \| TransitionProps` | `'stack-tab-zoom'` | Эффект перехода при добавлении/удалении вкладок |
|
|
349
|
+
| `tabScrollMode` | `TabScrollMode` | `'both'` | Режим прокрутки панели вкладок: `'wheel'` / `'button'` / `'both'` |
|
|
350
|
+
| `width` | `string` | `'100%'` | Ширина контейнера |
|
|
351
|
+
| `height` | `string` | `'100%'` | Высота контейнера |
|
|
352
|
+
| `i18n` | `string` | `'zh-CN'` | Локаль интернационализации |
|
|
353
|
+
| `globalScroll` | `boolean` | `false` | Включить запоминание прокрутки на уровне страницы |
|
|
354
|
+
| `sessionPrefix` | `string` | `''` | Префикс ключа sessionStorage |
|
|
355
|
+
| `iframeAllowedOrigins` | `string[]` | Тот же источник | Разрешённые источники для iframe postMessage |
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## События (Events)
|
|
360
|
+
|
|
361
|
+
| Событие | Параметры | Описание |
|
|
362
|
+
| -------------- | -------------- | --------------------------------------------- |
|
|
363
|
+
| `onActive` | `(id: string)` | Срабатывает при активации вкладки |
|
|
364
|
+
| `onPageLoaded` | — | Срабатывает при загрузке компонента страницы |
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Вкладки iframe
|
|
369
|
+
|
|
370
|
+
### Открытие вкладки iframe
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
openTab({
|
|
374
|
+
title: 'Внешняя страница',
|
|
375
|
+
path: 'https://example.com',
|
|
376
|
+
iframe: true
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Операции внутри iframe-страниц
|
|
381
|
+
|
|
382
|
+
Внутри вкладки iframe можно взаимодействовать с хост-контейнером через `postMessage` для открытия новых вкладок или обработки действий обновления.
|
|
383
|
+
|
|
384
|
+
#### 1. Использование утилит Bridge (рекомендуется)
|
|
385
|
+
|
|
386
|
+
Если iframe-страница может ссылаться на код библиотеки (или импортировать инструменты из `iframeBridge.ts`), рекомендуется этот подход:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import { postOpenTab, onRefreshRequest } from 'vue-stack-tabs/iframe-bridge'
|
|
390
|
+
|
|
391
|
+
const parentOrigin = 'https://parent.example.com'
|
|
392
|
+
|
|
393
|
+
// Открыть вкладку
|
|
394
|
+
postOpenTab(
|
|
395
|
+
{
|
|
396
|
+
title: 'Новая страница',
|
|
397
|
+
path: '/detail',
|
|
398
|
+
query: { id: '1' }
|
|
399
|
+
},
|
|
400
|
+
{ targetOrigin: parentOrigin }
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
// Слушать запросы на обновление
|
|
404
|
+
const off = onRefreshRequest(
|
|
405
|
+
() => {
|
|
406
|
+
// Пользовательская логика обновления
|
|
407
|
+
location.reload()
|
|
408
|
+
},
|
|
409
|
+
{ allowedOrigins: [parentOrigin] }
|
|
410
|
+
)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### 2. Нативная интеграция
|
|
414
|
+
|
|
415
|
+
Если невозможно импортировать утилиты библиотеки или требуется нулевая зависимость, используйте нативный API:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
const parentOrigin = 'https://parent.example.com'
|
|
419
|
+
|
|
420
|
+
// Открыть вкладку
|
|
421
|
+
window.parent.postMessage(
|
|
422
|
+
{
|
|
423
|
+
type: 'vue-stack-tabs:openTab',
|
|
424
|
+
payload: {
|
|
425
|
+
title: 'Нативная новая страница',
|
|
426
|
+
path: '/detail'
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
parentOrigin
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
// Слушать запросы на обновление
|
|
433
|
+
window.addEventListener('message', (ev) => {
|
|
434
|
+
if (ev.origin !== parentOrigin) return
|
|
435
|
+
if (ev.source !== window.parent) return
|
|
436
|
+
if (ev.data?.type === 'vue-stack-tabs:refresh') {
|
|
437
|
+
// Выполнить обновление
|
|
438
|
+
location.reload()
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Режимы обновления iframe
|
|
444
|
+
|
|
445
|
+
При вызове `openTab` можно указать поведение обновления через `iframeRefreshMode`:
|
|
446
|
+
|
|
447
|
+
- `postMessage` (по умолчанию): хост отправляет сообщение `vue-stack-tabs:refresh` в iframe; внутренняя страница сама решает, как обновляться (более плавная анимация).
|
|
448
|
+
- `reload`: хост напрямую сбрасывает `src` или `key` iframe для принудительной перезагрузки браузером (подходит для кросс-доменных или немодифицируемых страниц).
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Интернационализация
|
|
453
|
+
|
|
454
|
+
Встроенные языки: `zh-CN` (китайский), `en-US` (английский).
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
// Переключить язык
|
|
458
|
+
app.use(VueStackTabs, [{ locale: 'en-US' }])
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Пользовательский языковой пакет:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
app.use(VueStackTabs, [
|
|
465
|
+
{
|
|
466
|
+
locale: 'ru-RU',
|
|
467
|
+
messages: {
|
|
468
|
+
'VueStackTab.close': 'Закрыть',
|
|
469
|
+
'VueStackTab.refresh': 'Обновить'
|
|
470
|
+
// ...
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
])
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Определения типов
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
/** Данные, передаваемые при открытии вкладки */
|
|
482
|
+
interface ITabData {
|
|
483
|
+
id?: string // ID вкладки (автоматически генерируемый UUID, если не указан)
|
|
484
|
+
title: string // Заголовок вкладки
|
|
485
|
+
path: string // Путь маршрута или URL iframe
|
|
486
|
+
query?: Record<string, string> // Параметры маршрута
|
|
487
|
+
closable?: boolean // Можно ли закрыть (по умолчанию true)
|
|
488
|
+
refreshable?: boolean // Можно ли обновить (по умолчанию true)
|
|
489
|
+
iframe?: boolean // Является ли вкладкой iframe (по умолчанию false)
|
|
490
|
+
iframeRefreshMode?: 'postMessage' | 'reload' // Способ обновления iframe
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Режим прокрутки панели вкладок */
|
|
494
|
+
enum TabScrollMode {
|
|
495
|
+
WHEEL = 'wheel',
|
|
496
|
+
BUTTON = 'button',
|
|
497
|
+
BOTH = 'both'
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Структура проекта
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
src/lib/
|
|
507
|
+
├── StackTabs.vue # Главный компонент
|
|
508
|
+
├── index.ts # Точка входа и экспорты
|
|
509
|
+
├── hooks/ # Основная логика
|
|
510
|
+
│ ├── useTabActions.ts # Публичный API
|
|
511
|
+
│ ├── useTabRouter.ts # Навигация внутри вкладки
|
|
512
|
+
│ ├── useTabLoading.ts # Состояние загрузки
|
|
513
|
+
│ └── useTabPanel.tsx # Ядро движка
|
|
514
|
+
├── model/TabModel.ts # Определения типов
|
|
515
|
+
├── components/ # UI-компоненты
|
|
516
|
+
├── nuxt/ # Модуль Nuxt
|
|
517
|
+
└── assets/style/ # Стили
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Подробное описание архитектуры см. в [`ARCHITECTURE.md`](./ARCHITECTURE.md).
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Лицензия
|
|
525
|
+
|
|
526
|
+
[LGPL-2.1](./LICENSE)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iframe-bridge 子入口(vue-stack-tabs/iframe-bridge)。
|
|
3
|
+
*
|
|
4
|
+
* 供 iframe 内部页面引用,不包含 Vue 组件和样式等副作用,
|
|
5
|
+
* 仅导出 postMessage 通信工具。
|
|
6
|
+
*/
|
|
7
|
+
export { MSG_OPEN_TAB, MSG_REFRESH, onRefreshRequest, postOpenTab } from './utils/iframeBridge';
|
|
8
|
+
export type { IframeBridgeOptions, IframeOpenTabPayload, RefreshRequestOptions } from './utils/iframeBridge';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Iframe Bridge 测试页面</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: system-ui, sans-serif;
|
|
10
|
+
padding: 20px;
|
|
11
|
+
background: #f0f2f5;
|
|
12
|
+
}
|
|
13
|
+
.card {
|
|
14
|
+
background: white;
|
|
15
|
+
padding: 20px;
|
|
16
|
+
border-radius: 8px;
|
|
17
|
+
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
18
|
+
}
|
|
19
|
+
button {
|
|
20
|
+
padding: 10px 20px;
|
|
21
|
+
background: #67c23a;
|
|
22
|
+
color: white;
|
|
23
|
+
border: none;
|
|
24
|
+
border-radius: 4px;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
}
|
|
27
|
+
.status {
|
|
28
|
+
margin-top: 10px;
|
|
29
|
+
color: #606266;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div class="card">
|
|
35
|
+
<h2>此页面演示使用封装好的 Bridge 对接方式</h2>
|
|
36
|
+
<p>页面加载时间: <span id="time"></span></p>
|
|
37
|
+
<p>
|
|
38
|
+
使用 <code>iframeBridge.ts</code> 中提供的 <code>postOpenTab</code> 和
|
|
39
|
+
<code>onRefreshRequest</code>。
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<div style="margin: 20px 0">
|
|
43
|
+
<button id="btnOpen">使用 Bridge 方法请求打开新 Tab</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="status" id="status"></div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- 注意:在实际项目中,如果是原生 HTML 需要引入编译后的 Bridge 脚本,或者直接使用 type="module" -->
|
|
49
|
+
<script type="module">
|
|
50
|
+
// 这里的导入路径仅为本地调试示意,实际推荐将 Bridge 打包为公共库或 CDN 引用
|
|
51
|
+
import { postOpenTab, onRefreshRequest } from './src/lib/utils/iframeBridge.ts'
|
|
52
|
+
|
|
53
|
+
const timeEl = document.getElementById('time')
|
|
54
|
+
timeEl.innerText = new Date().toLocaleTimeString()
|
|
55
|
+
|
|
56
|
+
// 1. 使用 Bridge 封装的方法打开 Tab
|
|
57
|
+
document.getElementById('btnOpen').onclick = () => {
|
|
58
|
+
postOpenTab({
|
|
59
|
+
id: 'bridge_triggered_tab',
|
|
60
|
+
title: 'Bridge 弹出的新页签',
|
|
61
|
+
path: '/demo/page1',
|
|
62
|
+
closable: true
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. 使用 Bridge 封装的方法监听刷新请求
|
|
67
|
+
onRefreshRequest(() => {
|
|
68
|
+
console.log('[Bridge Child] Received refresh request from parent')
|
|
69
|
+
const statusEl = document.getElementById('status')
|
|
70
|
+
statusEl.innerText = '正在执行刷新 logic...'
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
location.reload()
|
|
73
|
+
}, 500)
|
|
74
|
+
})
|
|
75
|
+
</script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/lib/utils/iframeBridge.ts
|
|
2
|
+
var e = "vue-stack-tabs:refresh", t = "vue-stack-tabs:openTab";
|
|
3
|
+
function n() {
|
|
4
|
+
return window.location.origin;
|
|
5
|
+
}
|
|
6
|
+
function r(e, r = {}) {
|
|
7
|
+
typeof window > "u" || !window.parent || window.parent.postMessage({
|
|
8
|
+
type: t,
|
|
9
|
+
payload: e
|
|
10
|
+
}, r.targetOrigin ?? n());
|
|
11
|
+
}
|
|
12
|
+
function i(e = () => window.location.reload(), t = {}) {
|
|
13
|
+
if (typeof window > "u") return () => void 0;
|
|
14
|
+
let n = t.allowedOrigins ?? [], r = (t) => {
|
|
15
|
+
t.source === window.parent && (n.length > 0 && !n.includes(t.origin) || t.data?.type === "vue-stack-tabs:refresh" && e());
|
|
16
|
+
};
|
|
17
|
+
return window.addEventListener("message", r), () => window.removeEventListener("message", r);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { t as MSG_OPEN_TAB, e as MSG_REFRESH, i as onRefreshRequest, r as postOpenTab };
|
package/dist/iframe-child.html
CHANGED
|
@@ -53,23 +53,36 @@
|
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
55
|
<script>
|
|
56
|
-
document.getElementById('time')
|
|
56
|
+
const timeEl = document.getElementById('time')
|
|
57
|
+
const updateTime = () => {
|
|
58
|
+
timeEl.innerText = new Date().toLocaleTimeString()
|
|
59
|
+
}
|
|
60
|
+
updateTime()
|
|
57
61
|
|
|
62
|
+
// 1. 原生对接:向父窗口请求打开新标签
|
|
58
63
|
function requestNewTab() {
|
|
59
64
|
window.parent.postMessage(
|
|
60
65
|
{
|
|
61
|
-
type: 'openTab',
|
|
66
|
+
type: 'vue-stack-tabs:openTab', // 推荐使用该标准类型,StackTabs 已支持兼容 'openTab'
|
|
62
67
|
payload: {
|
|
63
|
-
id: '
|
|
64
|
-
title: '
|
|
68
|
+
id: 'native_triggered_tab',
|
|
69
|
+
title: '原生弹出的新页签',
|
|
65
70
|
path: '/demo/page2',
|
|
66
71
|
closable: true
|
|
67
|
-
}
|
|
68
|
-
refresh: true
|
|
72
|
+
}
|
|
69
73
|
},
|
|
70
74
|
'*'
|
|
71
75
|
)
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
// 2. 原生对接:监听父窗口发送的刷新请求
|
|
79
|
+
window.addEventListener('message', (ev) => {
|
|
80
|
+
if (ev.data?.type === 'vue-stack-tabs:refresh') {
|
|
81
|
+
console.log('[Native Child] Received refresh request from parent')
|
|
82
|
+
// 执行刷新逻辑(例如重新加载页面或局部数据更新)
|
|
83
|
+
location.reload()
|
|
84
|
+
}
|
|
85
|
+
})
|
|
73
86
|
</script>
|
|
74
87
|
</body>
|
|
75
88
|
</html>
|