xn-vue3-router-tabs 0.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/LICENSE +21 -0
- package/README.md +327 -0
- package/dist/components/RouterViewTab.vue.d.ts +2 -0
- package/dist/components/TabBar.vue.d.ts +16 -0
- package/dist/composables/useTabs.d.ts +3 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/keys.d.ts +5 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/resolver.d.ts +5 -0
- package/dist/store.d.ts +3 -0
- package/dist/strategy.d.ts +4 -0
- package/dist/types.d.ts +86 -0
- package/dist/xn-vue3-router-tabs.cjs +1 -0
- package/dist/xn-vue3-router-tabs.js +222 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# xn-vue3-router-tabs
|
|
2
|
+
|
|
3
|
+
为 Vue 3 + Vue Router 4 提供多页签能力的插件。
|
|
4
|
+
|
|
5
|
+
- 零额外依赖(仅 peerDep: vue + vue-router)
|
|
6
|
+
- 配置全部收口在路由 `meta` 中,不侵入业务代码
|
|
7
|
+
- 内置 KeepAlive 缓存,切换 tab 不重新加载页面
|
|
8
|
+
- 提供无样式 `<TabBar>` 组件,UI 完全由你控制
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install xn-vue3-router-tabs
|
|
16
|
+
# 或
|
|
17
|
+
pnpm add xn-vue3-router-tabs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 快速开始
|
|
23
|
+
|
|
24
|
+
### 1. 安装插件(`main.ts`)
|
|
25
|
+
|
|
26
|
+
**必须在 `app.use(router)` 之后安装。**
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createApp } from 'vue'
|
|
30
|
+
import { createTabPlugin } from 'xn-vue3-router-tabs'
|
|
31
|
+
import App from './App.vue'
|
|
32
|
+
import { router } from './router'
|
|
33
|
+
|
|
34
|
+
const app = createApp(App)
|
|
35
|
+
app.use(router)
|
|
36
|
+
app.use(createTabPlugin({
|
|
37
|
+
homePath: '/dashboard', // 关闭最后一个 tab 时的兜底页
|
|
38
|
+
maxTabs: 10, // 最大 tab 数(可选,默认不限)
|
|
39
|
+
closeStrategy: 'last-visited', // 关闭当前 tab 后的跳转策略(默认)
|
|
40
|
+
}))
|
|
41
|
+
app.mount('#app')
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. 配置路由 meta
|
|
45
|
+
|
|
46
|
+
所有路由默认纳入多页签管理,只有以下情况需要配置 `meta`:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const routes = [
|
|
50
|
+
// 不纳入管理(登录页、404 页等)
|
|
51
|
+
{ path: '/login', meta: { tab: false } },
|
|
52
|
+
|
|
53
|
+
// 自定义标题(字符串)
|
|
54
|
+
{ path: '/list', meta: { title: '采购列表' } },
|
|
55
|
+
|
|
56
|
+
// 动态标题(函数)—— path 传参,不同 id 自动产生独立 tab
|
|
57
|
+
{
|
|
58
|
+
path: '/detail/:id',
|
|
59
|
+
meta: { title: (route) => `单据 ${route.params.id}` },
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// query 传参,不同 id 产生独立 tab(依赖 fullPath 作为默认 key)
|
|
63
|
+
{
|
|
64
|
+
path: '/order',
|
|
65
|
+
meta: { title: (route) => `订单 ${route.query.id}` },
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// 两条路由共用同一个 tab(互相替换,而非新建)
|
|
69
|
+
{
|
|
70
|
+
path: '/purchase/new',
|
|
71
|
+
meta: { tabKey: '/purchase/list', title: '采购列表' },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
path: '/purchase/list',
|
|
75
|
+
meta: { title: '采购列表' },
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. 布局组件(`Layout.vue`)
|
|
81
|
+
|
|
82
|
+
用 `<RouterViewTab>` 替换 `<RouterView>`,用 `<TabBar>` 渲染标签栏:
|
|
83
|
+
|
|
84
|
+
```vue
|
|
85
|
+
<template>
|
|
86
|
+
<!-- 标签栏:通过 slot 完全自定义每个 tab 的渲染 -->
|
|
87
|
+
<div class="tab-bar">
|
|
88
|
+
<TabBar v-slot="{ tab, isActive, close, refresh }">
|
|
89
|
+
<div
|
|
90
|
+
class="tab"
|
|
91
|
+
:class="{ active: isActive }"
|
|
92
|
+
@click="router.push(tab.route.fullPath)"
|
|
93
|
+
>
|
|
94
|
+
<span>{{ tab.title }}</span>
|
|
95
|
+
<button @click.stop="refresh()">刷新</button>
|
|
96
|
+
<button @click.stop="close()">关闭</button>
|
|
97
|
+
</div>
|
|
98
|
+
</TabBar>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- 页面内容:内置 KeepAlive,切换 tab 不重新加载 -->
|
|
102
|
+
<RouterViewTab />
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<script setup lang="ts">
|
|
106
|
+
import { RouterViewTab, TabBar } from 'xn-vue3-router-tabs'
|
|
107
|
+
import { useRouter } from 'vue-router'
|
|
108
|
+
|
|
109
|
+
const router = useRouter()
|
|
110
|
+
</script>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## API
|
|
116
|
+
|
|
117
|
+
### `createTabPlugin(options?)`
|
|
118
|
+
|
|
119
|
+
创建插件实例。
|
|
120
|
+
|
|
121
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
122
|
+
|------|------|--------|------|
|
|
123
|
+
| `homePath` | `string` | `'/'` | 关闭最后一个 tab 或 `closeAll()` 后的跳转页 |
|
|
124
|
+
| `maxTabs` | `number` | 不限 | 最大 tab 数,超出时自动移除最久未访问的 |
|
|
125
|
+
| `closeStrategy` | `CloseStrategy` | `'last-visited'` | 关闭当前 tab 后的跳转策略 |
|
|
126
|
+
| `getTabKey` | `(route) => string` | — | 全局 tabKey 计算函数(meta.tabKey 优先) |
|
|
127
|
+
| `getTitle` | `(route) => string` | — | 全局 title 计算函数(meta.title 优先) |
|
|
128
|
+
| `beforeClose` | `(tab: Tab) => boolean \| Promise<boolean>` | — | 全局关闭守卫,返回 `false` 阻止关闭 |
|
|
129
|
+
|
|
130
|
+
**CloseStrategy 可选值:**
|
|
131
|
+
|
|
132
|
+
| 值 | 行为 |
|
|
133
|
+
|----|------|
|
|
134
|
+
| `'last-visited'` | 跳转到最近访问的其他 tab |
|
|
135
|
+
| `'previous'` | 跳转到列表中左侧相邻的 tab |
|
|
136
|
+
| `'next'` | 跳转到列表中右侧相邻的 tab |
|
|
137
|
+
| `'home'` | 跳转到 `homePath` |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### `useTabs()`
|
|
142
|
+
|
|
143
|
+
在组件中调用,返回标签状态和操作方法。
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const {
|
|
147
|
+
tabs, // Readonly<Tab[]> — 响应式标签列表
|
|
148
|
+
activeKey, // Readonly<Ref<string>> — 当前激活 tab 的 key
|
|
149
|
+
activeTab, // ComputedRef<Tab | undefined> — 当前激活的 tab 对象
|
|
150
|
+
close, // (key: string) => Promise<void>
|
|
151
|
+
refresh, // (key?: string) => void
|
|
152
|
+
closeOthers, // (key?: string) => Promise<void>
|
|
153
|
+
closeAll, // () => Promise<void>
|
|
154
|
+
onBeforeClose, // (guard: BeforeCloseGuard) => void
|
|
155
|
+
} = useTabs()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Tab 对象结构:**
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface Tab {
|
|
162
|
+
key: string // 唯一键
|
|
163
|
+
title: string // 显示标题
|
|
164
|
+
route: RouteLocationNormalizedLoaded // 对应路由快照
|
|
165
|
+
refreshCount: number // 内部刷新计数
|
|
166
|
+
createdAt: number // 创建时间戳
|
|
167
|
+
visitedAt: number // 最后访问时间戳
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `<RouterViewTab />`
|
|
174
|
+
|
|
175
|
+
替代 `<RouterView>` 使用。内部封装了 `<KeepAlive>`,保证:
|
|
176
|
+
|
|
177
|
+
- 切换 tab 时页面状态保留(不重新加载)
|
|
178
|
+
- 不同参数的同路由各自拥有独立缓存实例
|
|
179
|
+
- 调用 `refresh()` 时仅重建当前 tab 的组件,不影响其他 tab
|
|
180
|
+
|
|
181
|
+
```vue
|
|
182
|
+
<!-- 只需替换 RouterView,无需其他配置 -->
|
|
183
|
+
<RouterViewTab />
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### `<TabBar>` 无样式组件
|
|
189
|
+
|
|
190
|
+
通过 scoped slot 渲染每个 tab,插件不提供任何样式:
|
|
191
|
+
|
|
192
|
+
```vue
|
|
193
|
+
<TabBar v-slot="{ tab, isActive, close, refresh }">
|
|
194
|
+
<!-- tab: Tab 对象 -->
|
|
195
|
+
<!-- isActive: boolean 是否为当前激活 tab -->
|
|
196
|
+
<!-- close: () => void 关闭该 tab -->
|
|
197
|
+
<!-- refresh: () => void 刷新该 tab -->
|
|
198
|
+
<div :class="{ active: isActive }">
|
|
199
|
+
{{ tab.title }}
|
|
200
|
+
</div>
|
|
201
|
+
</TabBar>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### 路由 `meta` 配置
|
|
207
|
+
|
|
208
|
+
| 字段 | 类型 | 说明 |
|
|
209
|
+
|------|------|------|
|
|
210
|
+
| `tab` | `boolean` | `false` 时排除该路由,默认 `true`(纳入管理) |
|
|
211
|
+
| `tabKey` | `string \| (route) => string` | 自定义 tab 唯一键 |
|
|
212
|
+
| `title` | `string \| (route) => string` | 自定义 tab 标题 |
|
|
213
|
+
|
|
214
|
+
**tabKey 优先级:** `meta.tabKey` → `options.getTabKey` → `route.fullPath`
|
|
215
|
+
|
|
216
|
+
**title 优先级:** `meta.title` → `options.getTitle` → `route.path`
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 使用场景示例
|
|
221
|
+
|
|
222
|
+
### path 传参 — 不同 id 独立 tab(默认行为)
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// 路由配置
|
|
226
|
+
{ path: '/detail/:id', meta: { title: (r) => `单据 ${r.params.id}` } }
|
|
227
|
+
|
|
228
|
+
// 访问以下路由会产生 2 个独立 tab
|
|
229
|
+
router.push('/detail/1001')
|
|
230
|
+
router.push('/detail/1002')
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### query 传参 — 不同 id 独立 tab(默认行为)
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// 路由配置(无需额外配置,fullPath 天然不同)
|
|
237
|
+
{ path: '/order', meta: { title: (r) => `订单 ${r.query.id}` } }
|
|
238
|
+
|
|
239
|
+
// 访问以下路由会产生 2 个独立 tab
|
|
240
|
+
router.push('/order?id=A001')
|
|
241
|
+
router.push('/order?id=A002')
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 强制单实例 — 任意参数共用同一个 tab
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// 所有详情页共用一个 tab,后打开的替换前一个
|
|
248
|
+
{ path: '/detail/:id', meta: { tabKey: '/detail', title: (r) => `单据 ${r.params.id}` } }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 两条路由共用同一个 tab
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// /purchase/new 和 /purchase/list 共用同一个 tab
|
|
255
|
+
{ path: '/purchase/new', meta: { tabKey: '/purchase/list', title: '新建采购单' } }
|
|
256
|
+
{ path: '/purchase/list', meta: { title: '采购列表' } }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 编程式操作
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
const { close, refresh, closeOthers, closeAll, activeKey } = useTabs()
|
|
263
|
+
|
|
264
|
+
// 关闭当前 tab
|
|
265
|
+
close(activeKey.value)
|
|
266
|
+
|
|
267
|
+
// 刷新当前 tab
|
|
268
|
+
refresh()
|
|
269
|
+
|
|
270
|
+
// 关闭其他所有 tab,保留当前
|
|
271
|
+
closeOthers()
|
|
272
|
+
|
|
273
|
+
// 关闭全部并跳转 homePath
|
|
274
|
+
closeAll()
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 关闭守卫(beforeClose)
|
|
280
|
+
|
|
281
|
+
在页面有未保存修改时拦截 tab 关闭,支持组件级和全局级两种方式。
|
|
282
|
+
|
|
283
|
+
### 组件级守卫 — `onBeforeClose`
|
|
284
|
+
|
|
285
|
+
在页面组件内注册,组件销毁时自动解除:
|
|
286
|
+
|
|
287
|
+
```vue
|
|
288
|
+
<script setup>
|
|
289
|
+
import { ref } from 'vue'
|
|
290
|
+
import { useTabs } from 'xn-vue3-router-tabs'
|
|
291
|
+
|
|
292
|
+
const { onBeforeClose } = useTabs()
|
|
293
|
+
const hasUnsavedChanges = ref(false)
|
|
294
|
+
|
|
295
|
+
onBeforeClose(() => {
|
|
296
|
+
if (hasUnsavedChanges.value) {
|
|
297
|
+
return window.confirm('有未保存的修改,确定关闭?')
|
|
298
|
+
}
|
|
299
|
+
return true
|
|
300
|
+
})
|
|
301
|
+
</script>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 全局守卫 — 插件选项 `beforeClose`
|
|
305
|
+
|
|
306
|
+
在插件安装时配置,应用于所有 tab:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
app.use(createTabPlugin({
|
|
310
|
+
beforeClose: async (tab) => {
|
|
311
|
+
// 可结合后端接口判断
|
|
312
|
+
return true
|
|
313
|
+
},
|
|
314
|
+
}))
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 执行规则
|
|
318
|
+
|
|
319
|
+
- **返回值**:`true`(允许关闭)、`false`(阻止关闭),支持 `Promise`
|
|
320
|
+
- **执行顺序**:组件级守卫先执行 → 通过后 → 全局守卫再执行。任一返回 `false` 即阻止
|
|
321
|
+
- **批量关闭**(`closeOthers` / `closeAll`):逐个检查每个待关闭 tab,拒绝的保留,通过的关闭
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## TypeScript 支持
|
|
326
|
+
|
|
327
|
+
插件自动扩展 `vue-router` 的 `RouteMeta` 类型,路由配置中 `meta.tab`、`meta.tabKey`、`meta.title` 均有完整类型提示,无需手动声明。
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare function __VLS_template(): {
|
|
2
|
+
default?(_: {
|
|
3
|
+
tab: import('..').Tab;
|
|
4
|
+
isActive: boolean;
|
|
5
|
+
close: () => Promise<void>;
|
|
6
|
+
refresh: () => void;
|
|
7
|
+
}): any;
|
|
8
|
+
};
|
|
9
|
+
declare const __VLS_component: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
10
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
|
|
11
|
+
export default _default;
|
|
12
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
13
|
+
new (): {
|
|
14
|
+
$slots: S;
|
|
15
|
+
};
|
|
16
|
+
};
|
package/dist/hooks.d.ts
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createTabPlugin } from './plugin';
|
|
2
|
+
export { useTabs } from './composables/useTabs';
|
|
3
|
+
export { default as RouterViewTab } from './components/RouterViewTab.vue';
|
|
4
|
+
export { default as TabBar } from './components/TabBar.vue';
|
|
5
|
+
export type { Tab, TabPluginOptions, TabStoreContext, UseTabsReturn, CloseStrategy, BeforeCloseGuard, } from './types';
|
package/dist/keys.d.ts
ADDED
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { RouteLocationNormalized } from 'vue-router';
|
|
2
|
+
import { TabPluginOptions } from './types';
|
|
3
|
+
|
|
4
|
+
export declare function resolveTabKey(route: RouteLocationNormalized, options: TabPluginOptions): string;
|
|
5
|
+
export declare function resolveTitle(route: RouteLocationNormalized, options: TabPluginOptions): string;
|
package/dist/store.d.ts
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ComputedRef, Ref } from 'vue';
|
|
2
|
+
import { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocationRaw } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
declare module 'vue-router' {
|
|
5
|
+
interface RouteMeta {
|
|
6
|
+
/** false = 排除出多页签管理;默认 true(纳入) */
|
|
7
|
+
tab?: boolean;
|
|
8
|
+
/** 标签唯一键:字符串或根据 route 动态计算的函数 */
|
|
9
|
+
tabKey?: string | ((route: RouteLocationNormalized) => string);
|
|
10
|
+
/** 标签标题:字符串或根据 route 动态计算的函数 */
|
|
11
|
+
title?: string | ((route: RouteLocationNormalized) => string);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export type BeforeCloseGuard = () => boolean | Promise<boolean>;
|
|
15
|
+
export interface Tab {
|
|
16
|
+
/** 唯一键,由 resolveTabKey 生成 */
|
|
17
|
+
key: string;
|
|
18
|
+
/** 显示标题 */
|
|
19
|
+
title: string;
|
|
20
|
+
/** 对应路由快照(导航完成时更新) */
|
|
21
|
+
route: RouteLocationNormalizedLoaded;
|
|
22
|
+
/** 刷新计数,用于驱动 KeepAlive 重建 */
|
|
23
|
+
refreshCount: number;
|
|
24
|
+
/** 创建时间戳 */
|
|
25
|
+
createdAt: number;
|
|
26
|
+
/** 最后访问时间戳,用于 last-visited 策略 */
|
|
27
|
+
visitedAt: number;
|
|
28
|
+
}
|
|
29
|
+
export type CloseStrategy = 'last-visited' | 'previous' | 'next' | 'home';
|
|
30
|
+
export interface TabPluginOptions {
|
|
31
|
+
/**
|
|
32
|
+
* 全局 tabKey fallback。
|
|
33
|
+
* 优先级:meta.tabKey > getTabKey > route.fullPath
|
|
34
|
+
*/
|
|
35
|
+
getTabKey?: (route: RouteLocationNormalized) => string;
|
|
36
|
+
/**
|
|
37
|
+
* 全局 title fallback。
|
|
38
|
+
* 优先级:meta.title > getTitle > route.path
|
|
39
|
+
*/
|
|
40
|
+
getTitle?: (route: RouteLocationNormalized) => string;
|
|
41
|
+
/**
|
|
42
|
+
* 关闭当前 tab 后的跳转策略,默认 'last-visited'
|
|
43
|
+
*/
|
|
44
|
+
closeStrategy?: CloseStrategy;
|
|
45
|
+
/**
|
|
46
|
+
* 兜底页路径,closeStrategy='home' 时或仅剩最后一个 tab 时跳转
|
|
47
|
+
*/
|
|
48
|
+
homePath?: string;
|
|
49
|
+
/**
|
|
50
|
+
* 最大 tab 数,超出时自动移除 visitedAt 最早的 tab
|
|
51
|
+
*/
|
|
52
|
+
maxTabs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* 全局关闭守卫。返回 false 则阻止关闭。
|
|
55
|
+
*/
|
|
56
|
+
beforeClose?: (tab: Tab) => boolean | Promise<boolean>;
|
|
57
|
+
}
|
|
58
|
+
export interface TabStoreContext {
|
|
59
|
+
tabs: Tab[];
|
|
60
|
+
activeKey: Ref<string>;
|
|
61
|
+
options: TabPluginOptions;
|
|
62
|
+
guardMap: Map<string, Set<BeforeCloseGuard>>;
|
|
63
|
+
addOrActivate: (tab: Omit<Tab, 'refreshCount' | 'createdAt' | 'visitedAt'>) => void;
|
|
64
|
+
remove: (key: string) => void;
|
|
65
|
+
setActive: (key: string) => void;
|
|
66
|
+
incrementRefresh: (key?: string) => void;
|
|
67
|
+
}
|
|
68
|
+
export interface UseTabsReturn {
|
|
69
|
+
/** 响应式标签列表(只读) */
|
|
70
|
+
tabs: Readonly<Tab[]>;
|
|
71
|
+
/** 当前激活的 tab key(只读) */
|
|
72
|
+
activeKey: Readonly<Ref<string>>;
|
|
73
|
+
/** 当前激活的 tab 对象 */
|
|
74
|
+
activeTab: ComputedRef<Tab | undefined>;
|
|
75
|
+
/** 关闭指定 tab,若为当前 tab 则按策略跳转 */
|
|
76
|
+
close: (key: string) => Promise<void>;
|
|
77
|
+
/** 刷新指定 tab,默认刷新当前 tab */
|
|
78
|
+
refresh: (key?: string) => void;
|
|
79
|
+
/** 关闭除指定 key 外的其他所有 tab */
|
|
80
|
+
closeOthers: (key?: string) => Promise<void>;
|
|
81
|
+
/** 关闭所有 tab 并跳转至兜底页 */
|
|
82
|
+
closeAll: () => Promise<void>;
|
|
83
|
+
/** 注册组件级关闭守卫,组件销毁时自动解除 */
|
|
84
|
+
onBeforeClose: (guard: BeforeCloseGuard) => void;
|
|
85
|
+
}
|
|
86
|
+
export type { RouteLocationRaw };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("vue"),g=require("vue-router");function B(r,e){const n=r.meta.tabKey;return n!==void 0?typeof n=="function"?n(r):n:e.getTabKey?e.getTabKey(r):r.fullPath}function C(r,e){const n=r.meta.title;return n!==void 0?typeof n=="function"?n(r):n:e.getTitle?e.getTitle(r):r.path}function K(r,e,n){r.afterEach(o=>{if(o.meta.tab===!1){e.activeKey.value="";return}const l=B(o,n),d=C(o,n);e.addOrActivate({key:l,title:d,route:o})})}const _=Symbol("xn-vue3-router-tabs"),A=Symbol("xn-vue3-router-tabs-key");function R(r){const e=t.shallowReactive([]),n=t.ref(""),o=new Map;function l(c){const s=e.findIndex(u=>u.key===c.key);if(s>=0){e.splice(s,1,{...e[s],title:c.title,route:c.route,visitedAt:Date.now()}),n.value=c.key;return}if(e.push({...c,refreshCount:0,createdAt:Date.now(),visitedAt:Date.now()}),n.value=c.key,r.maxTabs&&e.length>r.maxTabs){let u=-1,p=1/0;for(let h=0;h<e.length-1;h++)e[h].visitedAt<p&&(p=e[h].visitedAt,u=h);u>=0&&e.splice(u,1)}}function d(c){const s=e.findIndex(u=>u.key===c);s>=0&&e.splice(s,1),o.delete(c)}function a(c){const s=e.findIndex(u=>u.key===c);s>=0&&(e.splice(s,1,{...e[s],visitedAt:Date.now()}),n.value=c)}function v(c){const s=c??n.value,u=e.findIndex(p=>p.key===s);u>=0&&e.splice(u,1,{...e[u],refreshCount:e[u].refreshCount+1})}return{tabs:e,activeKey:n,options:r,guardMap:o,addOrActivate:l,remove:d,setActive:a,incrementRefresh:v}}function P(r={}){return{install(e){const n=e.config.globalProperties.$router;if(!n){console.warn("[xn-vue3-router-tabs] vue-router is required. Make sure to install vue-router before xn-vue3-router-tabs.");return}const o=R(r);K(n,o,r),e.provide(_,o)}}}function I(r,e,n="last-visited",o="/"){const l=e.filter(a=>a.key!==r);if(l.length===0)return o;if(n==="last-visited")return[...l].sort((v,c)=>c.visitedAt-v.visitedAt)[0].route.fullPath;const d=e.findIndex(a=>a.key===r);if(n==="previous"){for(let a=d-1;a>=0;a--)if(e[a].key!==r)return e[a].route.fullPath;return l[0].route.fullPath}if(n==="next"){for(let a=d+1;a<e.length;a++)if(e[a].key!==r)return e[a].route.fullPath;return l[l.length-1].route.fullPath}return o}function T(){const r=t.inject(_);if(!r)throw new Error("[xn-vue3-router-tabs] useTabs() must be used inside a component that is a descendant of the app where createTabPlugin() was installed.");const e=r,n=g.useRouter(),{tabs:o,activeKey:l,options:d,guardMap:a}=e,v=t.computed(()=>o.find(i=>i.key===l.value)),c=t.inject(A,"");async function s(i){const y=a.get(i);if(y){for(const f of y)if(await f()===!1)return!1}if(d.beforeClose){const f=o.find(k=>k.key===i);if(f&&await d.beforeClose(f)===!1)return!1}return!0}function u(i){if(!c)return;const y=c;a.has(y)||a.set(y,new Set),a.get(y).add(i),t.getCurrentInstance()&&t.onUnmounted(()=>{const f=a.get(y);f&&(f.delete(i),f.size===0&&a.delete(y))})}async function p(i){if(!await s(i))return;if(i!==l.value){e.remove(i);return}const f=I(i,o,d.closeStrategy,d.homePath);await n.push(f)||e.remove(i)}function h(i){e.incrementRefresh(i)}async function w(i){const y=i??l.value;if(y!==l.value){const m=o.find(b=>b.key===y);if(m&&await n.push(m.route.fullPath))return}const f=o.filter(m=>m.key!==y),k=[];for(const m of f)await s(m.key)&&k.push(m.key);k.forEach(m=>e.remove(m))}async function x(){const i=[];for(const f of[...o])await s(f.key)&&i.push(f.key);i.length===0||i.includes(l.value)&&await n.push(d.homePath??"/")||i.forEach(f=>e.remove(f))}return{tabs:o,activeKey:l,activeTab:v,close:p,refresh:h,closeOthers:w,closeAll:x,onBeforeClose:u}}const S=t.defineComponent({__name:"RouterViewTab",setup(r){const e=g.useRoute(),{tabs:n,activeKey:o,activeTab:l}=T(),d=t.computed(()=>{var u;const s=((u=l.value)==null?void 0:u.refreshCount)??0;return`${o.value}@${s}`}),a=t.computed(()=>n.map(s=>`${s.key}@${s.refreshCount}`)),v=new Map;function c(s){const u=d.value;if(!v.has(u)){const h=t.shallowRef(s),w=o.value;v.set(u,{comp:t.defineComponent({name:u,setup(x,{attrs:i}){return t.provide(A,w),()=>t.h(h.value,i)}}),current:h})}const p=v.get(u);return p.current.value=s,p.comp}return t.watch(a,s=>{for(const u of v.keys())s.includes(u)||v.delete(u)}),(s,u)=>{const p=t.resolveComponent("RouterView");return t.openBlock(),t.createBlock(p,null,{default:t.withCtx(({Component:h})=>[(t.openBlock(),t.createBlock(t.KeepAlive,{include:a.value},[h&&t.unref(o)?(t.openBlock(),t.createBlock(t.resolveDynamicComponent(c(h)),{key:d.value})):t.createCommentVNode("",!0)],1032,["include"])),h&&!t.unref(o)?(t.openBlock(),t.createBlock(t.resolveDynamicComponent(h),{key:t.unref(e).fullPath})):t.createCommentVNode("",!0)]),_:1})}}}),E=t.defineComponent({__name:"TabBar",setup(r){const{tabs:e,activeKey:n,close:o,refresh:l}=T();return(d,a)=>(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(t.unref(e),v=>t.renderSlot(d.$slots,"default",{key:v.key,tab:v,isActive:v.key===t.unref(n),close:()=>t.unref(o)(v.key),refresh:()=>t.unref(l)(v.key)})),128))}});exports.RouterViewTab=S;exports.TabBar=E;exports.createTabPlugin=P;exports.useTabs=T;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { shallowReactive as E, ref as S, inject as K, computed as _, getCurrentInstance as $, onUnmounted as D, defineComponent as g, watch as M, resolveComponent as V, openBlock as k, createBlock as b, withCtx as O, KeepAlive as N, unref as w, resolveDynamicComponent as R, createCommentVNode as P, shallowRef as Y, provide as j, h as q, createElementBlock as z, Fragment as F, renderList as G, renderSlot as H } from "vue";
|
|
2
|
+
import { useRouter as L, useRoute as U } from "vue-router";
|
|
3
|
+
function W(r, e) {
|
|
4
|
+
const t = r.meta.tabKey;
|
|
5
|
+
return t !== void 0 ? typeof t == "function" ? t(r) : t : e.getTabKey ? e.getTabKey(r) : r.fullPath;
|
|
6
|
+
}
|
|
7
|
+
function J(r, e) {
|
|
8
|
+
const t = r.meta.title;
|
|
9
|
+
return t !== void 0 ? typeof t == "function" ? t(r) : t : e.getTitle ? e.getTitle(r) : r.path;
|
|
10
|
+
}
|
|
11
|
+
function Q(r, e, t) {
|
|
12
|
+
r.afterEach((n) => {
|
|
13
|
+
if (n.meta.tab === !1) {
|
|
14
|
+
e.activeKey.value = "";
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const c = W(n, t), f = J(n, t);
|
|
18
|
+
e.addOrActivate({ key: c, title: f, route: n });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const C = Symbol("xn-vue3-router-tabs"), I = Symbol("xn-vue3-router-tabs-key");
|
|
22
|
+
function X(r) {
|
|
23
|
+
const e = E([]), t = S(""), n = /* @__PURE__ */ new Map();
|
|
24
|
+
function c(u) {
|
|
25
|
+
const s = e.findIndex((o) => o.key === u.key);
|
|
26
|
+
if (s >= 0) {
|
|
27
|
+
e.splice(s, 1, {
|
|
28
|
+
...e[s],
|
|
29
|
+
title: u.title,
|
|
30
|
+
route: u.route,
|
|
31
|
+
visitedAt: Date.now()
|
|
32
|
+
}), t.value = u.key;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (e.push({
|
|
36
|
+
...u,
|
|
37
|
+
refreshCount: 0,
|
|
38
|
+
createdAt: Date.now(),
|
|
39
|
+
visitedAt: Date.now()
|
|
40
|
+
}), t.value = u.key, r.maxTabs && e.length > r.maxTabs) {
|
|
41
|
+
let o = -1, y = 1 / 0;
|
|
42
|
+
for (let v = 0; v < e.length - 1; v++)
|
|
43
|
+
e[v].visitedAt < y && (y = e[v].visitedAt, o = v);
|
|
44
|
+
o >= 0 && e.splice(o, 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function f(u) {
|
|
48
|
+
const s = e.findIndex((o) => o.key === u);
|
|
49
|
+
s >= 0 && e.splice(s, 1), n.delete(u);
|
|
50
|
+
}
|
|
51
|
+
function a(u) {
|
|
52
|
+
const s = e.findIndex((o) => o.key === u);
|
|
53
|
+
s >= 0 && (e.splice(s, 1, { ...e[s], visitedAt: Date.now() }), t.value = u);
|
|
54
|
+
}
|
|
55
|
+
function d(u) {
|
|
56
|
+
const s = u ?? t.value, o = e.findIndex((y) => y.key === s);
|
|
57
|
+
o >= 0 && e.splice(o, 1, { ...e[o], refreshCount: e[o].refreshCount + 1 });
|
|
58
|
+
}
|
|
59
|
+
return { tabs: e, activeKey: t, options: r, guardMap: n, addOrActivate: c, remove: f, setActive: a, incrementRefresh: d };
|
|
60
|
+
}
|
|
61
|
+
function re(r = {}) {
|
|
62
|
+
return {
|
|
63
|
+
install(e) {
|
|
64
|
+
const t = e.config.globalProperties.$router;
|
|
65
|
+
if (!t) {
|
|
66
|
+
console.warn("[xn-vue3-router-tabs] vue-router is required. Make sure to install vue-router before xn-vue3-router-tabs.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const n = X(r);
|
|
70
|
+
Q(t, n, r), e.provide(C, n);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function Z(r, e, t = "last-visited", n = "/") {
|
|
75
|
+
const c = e.filter((a) => a.key !== r);
|
|
76
|
+
if (c.length === 0) return n;
|
|
77
|
+
if (t === "last-visited")
|
|
78
|
+
return [...c].sort((d, u) => u.visitedAt - d.visitedAt)[0].route.fullPath;
|
|
79
|
+
const f = e.findIndex((a) => a.key === r);
|
|
80
|
+
if (t === "previous") {
|
|
81
|
+
for (let a = f - 1; a >= 0; a--)
|
|
82
|
+
if (e[a].key !== r) return e[a].route.fullPath;
|
|
83
|
+
return c[0].route.fullPath;
|
|
84
|
+
}
|
|
85
|
+
if (t === "next") {
|
|
86
|
+
for (let a = f + 1; a < e.length; a++)
|
|
87
|
+
if (e[a].key !== r) return e[a].route.fullPath;
|
|
88
|
+
return c[c.length - 1].route.fullPath;
|
|
89
|
+
}
|
|
90
|
+
return n;
|
|
91
|
+
}
|
|
92
|
+
function B() {
|
|
93
|
+
const r = K(C);
|
|
94
|
+
if (!r)
|
|
95
|
+
throw new Error("[xn-vue3-router-tabs] useTabs() must be used inside a component that is a descendant of the app where createTabPlugin() was installed.");
|
|
96
|
+
const e = r, t = L(), { tabs: n, activeKey: c, options: f, guardMap: a } = e, d = _(() => n.find((i) => i.key === c.value)), u = K(I, "");
|
|
97
|
+
async function s(i) {
|
|
98
|
+
const h = a.get(i);
|
|
99
|
+
if (h) {
|
|
100
|
+
for (const l of h)
|
|
101
|
+
if (await l() === !1) return !1;
|
|
102
|
+
}
|
|
103
|
+
if (f.beforeClose) {
|
|
104
|
+
const l = n.find((m) => m.key === i);
|
|
105
|
+
if (l && await f.beforeClose(l) === !1)
|
|
106
|
+
return !1;
|
|
107
|
+
}
|
|
108
|
+
return !0;
|
|
109
|
+
}
|
|
110
|
+
function o(i) {
|
|
111
|
+
if (!u) return;
|
|
112
|
+
const h = u;
|
|
113
|
+
a.has(h) || a.set(h, /* @__PURE__ */ new Set()), a.get(h).add(i), $() && D(() => {
|
|
114
|
+
const l = a.get(h);
|
|
115
|
+
l && (l.delete(i), l.size === 0 && a.delete(h));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function y(i) {
|
|
119
|
+
if (!await s(i)) return;
|
|
120
|
+
if (i !== c.value) {
|
|
121
|
+
e.remove(i);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const l = Z(i, n, f.closeStrategy, f.homePath);
|
|
125
|
+
await t.push(l) || e.remove(i);
|
|
126
|
+
}
|
|
127
|
+
function v(i) {
|
|
128
|
+
e.incrementRefresh(i);
|
|
129
|
+
}
|
|
130
|
+
async function T(i) {
|
|
131
|
+
const h = i ?? c.value;
|
|
132
|
+
if (h !== c.value) {
|
|
133
|
+
const p = n.find((x) => x.key === h);
|
|
134
|
+
if (p && await t.push(p.route.fullPath))
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const l = n.filter((p) => p.key !== h), m = [];
|
|
138
|
+
for (const p of l)
|
|
139
|
+
await s(p.key) && m.push(p.key);
|
|
140
|
+
m.forEach((p) => e.remove(p));
|
|
141
|
+
}
|
|
142
|
+
async function A() {
|
|
143
|
+
const i = [];
|
|
144
|
+
for (const l of [...n])
|
|
145
|
+
await s(l.key) && i.push(l.key);
|
|
146
|
+
i.length === 0 || i.includes(c.value) && await t.push(f.homePath ?? "/") || i.forEach((l) => e.remove(l));
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
tabs: n,
|
|
150
|
+
activeKey: c,
|
|
151
|
+
activeTab: d,
|
|
152
|
+
close: y,
|
|
153
|
+
refresh: v,
|
|
154
|
+
closeOthers: T,
|
|
155
|
+
closeAll: A,
|
|
156
|
+
onBeforeClose: o
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const ne = /* @__PURE__ */ g({
|
|
160
|
+
__name: "RouterViewTab",
|
|
161
|
+
setup(r) {
|
|
162
|
+
const e = U(), { tabs: t, activeKey: n, activeTab: c } = B(), f = _(() => {
|
|
163
|
+
var o;
|
|
164
|
+
const s = ((o = c.value) == null ? void 0 : o.refreshCount) ?? 0;
|
|
165
|
+
return `${n.value}@${s}`;
|
|
166
|
+
}), a = _(
|
|
167
|
+
() => t.map((s) => `${s.key}@${s.refreshCount}`)
|
|
168
|
+
), d = /* @__PURE__ */ new Map();
|
|
169
|
+
function u(s) {
|
|
170
|
+
const o = f.value;
|
|
171
|
+
if (!d.has(o)) {
|
|
172
|
+
const v = Y(s), T = n.value;
|
|
173
|
+
d.set(o, {
|
|
174
|
+
comp: g({
|
|
175
|
+
name: o,
|
|
176
|
+
setup(A, { attrs: i }) {
|
|
177
|
+
return j(I, T), () => q(v.value, i);
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
current: v
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const y = d.get(o);
|
|
184
|
+
return y.current.value = s, y.comp;
|
|
185
|
+
}
|
|
186
|
+
return M(a, (s) => {
|
|
187
|
+
for (const o of d.keys())
|
|
188
|
+
s.includes(o) || d.delete(o);
|
|
189
|
+
}), (s, o) => {
|
|
190
|
+
const y = V("RouterView");
|
|
191
|
+
return k(), b(y, null, {
|
|
192
|
+
default: O(({ Component: v }) => [
|
|
193
|
+
(k(), b(N, { include: a.value }, [
|
|
194
|
+
v && w(n) ? (k(), b(R(u(v)), { key: f.value })) : P("", !0)
|
|
195
|
+
], 1032, ["include"])),
|
|
196
|
+
v && !w(n) ? (k(), b(R(v), {
|
|
197
|
+
key: w(e).fullPath
|
|
198
|
+
})) : P("", !0)
|
|
199
|
+
]),
|
|
200
|
+
_: 1
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}), oe = /* @__PURE__ */ g({
|
|
205
|
+
__name: "TabBar",
|
|
206
|
+
setup(r) {
|
|
207
|
+
const { tabs: e, activeKey: t, close: n, refresh: c } = B();
|
|
208
|
+
return (f, a) => (k(!0), z(F, null, G(w(e), (d) => H(f.$slots, "default", {
|
|
209
|
+
key: d.key,
|
|
210
|
+
tab: d,
|
|
211
|
+
isActive: d.key === w(t),
|
|
212
|
+
close: () => w(n)(d.key),
|
|
213
|
+
refresh: () => w(c)(d.key)
|
|
214
|
+
})), 128));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
export {
|
|
218
|
+
ne as RouterViewTab,
|
|
219
|
+
oe as TabBar,
|
|
220
|
+
re as createTabPlugin,
|
|
221
|
+
B as useTabs
|
|
222
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xn-vue3-router-tabs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Multi-tab management plugin for Vue 3 + Vue Router 4",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/anthropics/xn-vue3-router-tabs.git",
|
|
9
|
+
"directory": "packages/xn-vue3-router-tabs"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"vue",
|
|
13
|
+
"vue3",
|
|
14
|
+
"vue-router",
|
|
15
|
+
"tabs",
|
|
16
|
+
"multi-tab",
|
|
17
|
+
"keep-alive",
|
|
18
|
+
"router-tab"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"main": "./dist/xn-vue3-router-tabs.cjs",
|
|
23
|
+
"module": "./dist/xn-vue3-router-tabs.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/xn-vue3-router-tabs.js",
|
|
29
|
+
"require": "./dist/xn-vue3-router-tabs.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "vite build && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
|
|
38
|
+
"dev": "vite build --watch",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"typecheck": "vue-tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"vue": "^3.0.0",
|
|
44
|
+
"vue-router": "^4.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"vite": "^5.0.0",
|
|
50
|
+
"vite-plugin-dts": "^3.0.0",
|
|
51
|
+
"vitest": "^1.0.0",
|
|
52
|
+
"vue": "^3.4.0",
|
|
53
|
+
"vue-router": "^4.3.0",
|
|
54
|
+
"vue-tsc": "^2.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|