vue-stack-tabs 0.1.51 → 1.0.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.md +717 -3
- package/dist/index.d.ts +3 -0
- package/dist/src/lib/StackTabs.vue.d.ts +33 -161
- package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +6 -6
- package/dist/src/lib/components/ContextMenu/index.vue.d.ts +14 -14
- package/dist/src/lib/components/PageLoading.vue.d.ts +5 -5
- package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +24 -15
- package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +10 -8
- package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +31 -13
- package/dist/src/lib/components/TabHeader/index.vue.d.ts +172 -20
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +24 -0
- package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +1 -0
- package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -0
- package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -0
- package/dist/src/lib/hooks/tabPanel/state.d.ts +77 -0
- package/dist/src/lib/hooks/useContextMenu.d.ts +6 -1
- package/dist/src/lib/hooks/useTabActions.d.ts +68 -0
- package/dist/src/lib/hooks/{useTabMitt.d.ts → useTabEventBus.d.ts} +4 -3
- package/dist/src/lib/hooks/useTabLoading.d.ts +6 -0
- package/dist/src/lib/hooks/{useTabpanel.d.ts → useTabPanel.d.ts} +20 -14
- package/dist/src/lib/hooks/useTabPanel.spec.d.ts +1 -0
- package/dist/src/lib/hooks/useTabRouter.d.ts +14 -5
- package/dist/src/lib/i18n/index.d.ts +1 -1
- package/dist/src/lib/i18n/lang/en.d.ts +1 -0
- package/dist/src/lib/i18n/lang/zh-CN.d.ts +1 -0
- package/dist/src/lib/index.d.ts +5 -4
- package/dist/src/lib/model/TabModel.d.ts +20 -3
- package/dist/src/lib/model/TabModel.spec.d.ts +1 -0
- package/dist/src/lib/utils/iframeBridge.d.ts +31 -0
- package/dist/src/lib/utils/iframeBridge.spec.d.ts +1 -0
- package/dist/src/lib/utils/scrollUtils.d.ts +28 -0
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +19 -0
- package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +1 -0
- package/dist/src/lib/utils/urlParser.d.ts +19 -0
- package/dist/src/lib/utils/urlParser.spec.d.ts +1 -0
- package/dist/src/lib/versionLogger.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/vue-stack-tabs.css +1 -0
- package/dist/vue-stack-tabs.es.js +1795 -1679
- package/dist/vue-stack-tabs.umd.js +1 -5
- package/package.json +76 -39
- package/src/lib/StackTabs.vue +222 -47
- package/src/lib/assets/image/open.svg +1 -0
- package/src/lib/assets/style/common/var.scss +1 -1
- package/src/lib/assets/style/icon.scss +6 -0
- package/src/lib/assets/style/stackTab.scss +46 -3
- package/src/lib/assets/style/transition.scss +63 -62
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +7 -0
- package/src/lib/components/ContextMenu/index.vue +50 -38
- package/src/lib/components/PageLoading.vue +30 -10
- package/src/lib/components/TabHeader/TabHeaderButton.vue +12 -3
- package/src/lib/components/TabHeader/TabHeaderItem.vue +31 -27
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +83 -77
- package/src/lib/components/TabHeader/index.vue +45 -40
- package/src/lib/hooks/tabPanel/evict.spec.ts +85 -0
- package/src/lib/hooks/tabPanel/evict.ts +57 -0
- package/src/lib/hooks/tabPanel/scroll.ts +52 -0
- package/src/lib/hooks/tabPanel/session.ts +33 -0
- package/src/lib/hooks/tabPanel/state.ts +52 -0
- package/src/lib/hooks/useContextMenu.ts +22 -20
- package/src/lib/hooks/useTabActions.ts +148 -0
- package/src/lib/hooks/useTabEventBus.ts +31 -0
- package/src/lib/hooks/useTabLoading.ts +12 -4
- package/src/lib/hooks/useTabPanel.spec.ts +34 -0
- package/src/lib/hooks/useTabPanel.tsx +546 -0
- package/src/lib/hooks/useTabRouter.ts +114 -103
- package/src/lib/i18n/index.ts +14 -18
- package/src/lib/i18n/lang/en.ts +2 -1
- package/src/lib/i18n/lang/zh-CN.ts +2 -1
- package/src/lib/iframe.vue +6 -0
- package/src/lib/index.ts +27 -8
- package/src/lib/model/TabModel.spec.ts +40 -0
- package/src/lib/model/TabModel.ts +22 -9
- package/src/lib/nuxt/module.d.ts +19 -0
- package/src/lib/nuxt/module.ts +34 -0
- package/src/lib/nuxt/runtime/plugin.client.ts +11 -0
- package/src/lib/utils/iframeBridge.spec.ts +37 -0
- package/src/lib/utils/iframeBridge.ts +52 -0
- package/src/lib/utils/{TabScrollHelper.ts → scrollUtils.ts} +23 -29
- package/src/lib/utils/tabInfoEncoder.spec.ts +42 -0
- package/src/lib/utils/tabInfoEncoder.ts +65 -0
- package/src/lib/utils/urlParser.spec.ts +45 -0
- package/src/lib/utils/urlParser.ts +42 -0
- package/src/lib/{banner.ts → versionLogger.ts} +4 -5
- package/dist/src/lib/banner.d.ts +0 -2
- package/dist/src/lib/hooks/useStackTab.d.ts +0 -11
- package/dist/src/lib/utils/TabIdHelper.d.ts +0 -11
- package/dist/src/lib/utils/TabScrollHelper.d.ts +0 -33
- package/dist/src/lib/utils/UriHelper.d.ts +0 -4
- package/src/lib/hooks/useStackTab.ts +0 -119
- package/src/lib/hooks/useTabMitt.ts +0 -30
- package/src/lib/hooks/useTabpanel.tsx +0 -708
- package/src/lib/utils/TabIdHelper.ts +0 -70
- package/src/lib/utils/UriHelper.ts +0 -22
|
@@ -1,103 +1,114 @@
|
|
|
1
|
-
import { defu } from 'defu'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
1
|
+
import { defu } from 'defu'
|
|
2
|
+
import { getCurrentInstance } from 'vue'
|
|
3
|
+
import type { ComponentInternalInstance } from 'vue'
|
|
4
|
+
import type { RouteLocationPathRaw } from 'vue-router'
|
|
5
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
6
|
+
|
|
7
|
+
import useTabPanel from './useTabPanel'
|
|
8
|
+
import { parseUrl } from '../utils/urlParser'
|
|
9
|
+
import { useTabEmitter } from './useTabEventBus'
|
|
10
|
+
|
|
11
|
+
/** StackTabs 监听此事件切换前进动画 */
|
|
12
|
+
const ROUTER_EVENT_FORWARD = 'FORWARD'
|
|
13
|
+
/** StackTabs 监听此事件切换后退动画 */
|
|
14
|
+
const ROUTER_EVENT_BACKWARD = 'BACKWARD'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* useTabRouter - 标签内路由 Hook
|
|
18
|
+
*
|
|
19
|
+
* 职责:在单个标签内实现「前进/后退」栈式导航,以及滚动位置的记录与恢复
|
|
20
|
+
* 使用范围:仅在标签内的页面组件中使用,依赖 router-view 传入的 tId、pId
|
|
21
|
+
*
|
|
22
|
+
* 主要 API:
|
|
23
|
+
* - forward(to) 前进:push 新页面到该标签的 pages 栈
|
|
24
|
+
* - backward(to, backQuery?) 后退:pop 栈顶、markCacheForEviction,跳转到新栈顶
|
|
25
|
+
* - addScrollTarget(...selectors) 注册需要记录滚动位置的选择器
|
|
26
|
+
*/
|
|
27
|
+
export default function useTabRouter() {
|
|
28
|
+
const instance = getCurrentInstance() as ComponentInternalInstance
|
|
29
|
+
const { attrs, props } = instance
|
|
30
|
+
/** 当前标签 id,由 cache 组件的 dynamic-component 传入 */
|
|
31
|
+
const tabId = (attrs.tId ?? props.tId) as string
|
|
32
|
+
/** 当前页面缓存 id */
|
|
33
|
+
const pageId = (attrs.pId ?? props.pId) as string
|
|
34
|
+
|
|
35
|
+
const route = useRoute()
|
|
36
|
+
const router = useRouter()
|
|
37
|
+
const emitter = useTabEmitter()
|
|
38
|
+
const { getTab, markCacheForEviction, getComponent, addPageScroller } = useTabPanel()
|
|
39
|
+
|
|
40
|
+
/** 当前标签的编码信息,用于 push 时保持 __tab */
|
|
41
|
+
const tabInfo = route.query.__tab as string
|
|
42
|
+
|
|
43
|
+
/** 注册需记录滚动位置的容器选择器 */
|
|
44
|
+
const addScrollTarget = (...selectors: string[]) => {
|
|
45
|
+
addPageScroller(pageId, ...selectors)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 前进:进栈,跳转到新页面 */
|
|
49
|
+
const forward = (to: RouteLocationPathRaw) => {
|
|
50
|
+
const query = defu({ __tab: tabInfo }, to.query)
|
|
51
|
+
emitter.emit(ROUTER_EVENT_FORWARD)
|
|
52
|
+
router.push({ path: to.path, query })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 后退:从该标签的 pages 栈中 pop,并跳转到新栈顶
|
|
57
|
+
* @param to 数字=退几步,字符串=回退到首个路径匹配的页面
|
|
58
|
+
* @param backQuery 回退后传给目标页面的 props 数据(目标页通过 props 接收)
|
|
59
|
+
*/
|
|
60
|
+
const backward = (to: number | string, backQuery?: object) => {
|
|
61
|
+
const tab = getTab(tabId)
|
|
62
|
+
if (!tab) return
|
|
63
|
+
|
|
64
|
+
const stack = tab.pages
|
|
65
|
+
let steps = 0
|
|
66
|
+
|
|
67
|
+
if (typeof to === 'number') {
|
|
68
|
+
steps = Math.abs(to)
|
|
69
|
+
} else {
|
|
70
|
+
const parsed = parseUrl(to)
|
|
71
|
+
backQuery = defu(parsed.query, backQuery)
|
|
72
|
+
const pages = stack.list()
|
|
73
|
+
let found = false
|
|
74
|
+
for (let i = stack.size() - 2; i >= 0; i--) {
|
|
75
|
+
steps++
|
|
76
|
+
if (pages[i]?.path === parsed.path) {
|
|
77
|
+
found = true
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!found) return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (steps <= 0 || stack.size() <= steps) return
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < steps; i++) {
|
|
87
|
+
const popped = stack.pop()
|
|
88
|
+
if (popped) markCacheForEviction(popped.id)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const target = stack.peek()
|
|
92
|
+
if (!target) return
|
|
93
|
+
|
|
94
|
+
const component = getComponent(target.id)
|
|
95
|
+
if (component && backQuery) {
|
|
96
|
+
const comp = component as {
|
|
97
|
+
components?: { DynamicComponent?: { props?: object } }
|
|
98
|
+
}
|
|
99
|
+
const dynamic = comp.components?.DynamicComponent
|
|
100
|
+
if (dynamic?.props) {
|
|
101
|
+
dynamic.props = defu(backQuery, dynamic.props)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emitter.emit(ROUTER_EVENT_BACKWARD)
|
|
106
|
+
router.push({ path: target.path, query: target.query })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
forward,
|
|
111
|
+
backward,
|
|
112
|
+
addScrollTarget
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/lib/i18n/index.ts
CHANGED
|
@@ -9,20 +9,16 @@ export default () => {
|
|
|
9
9
|
* 获取所有语言文件
|
|
10
10
|
* @param {Object} mList
|
|
11
11
|
*/
|
|
12
|
-
function getLangFiles(
|
|
12
|
+
function getLangFiles(
|
|
13
|
+
mList: Record<string, { default?: Record<string, unknown> }>,
|
|
14
|
+
msg: Record<string, Record<string, unknown>>
|
|
15
|
+
) {
|
|
13
16
|
for (const path in mList) {
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
const mod = mList[path] as { default?: Record<string, unknown> }
|
|
18
|
+
if (mod?.default) {
|
|
16
19
|
const pathName = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.'))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
msg[pathName] = {
|
|
20
|
-
...mList[pathName],
|
|
21
|
-
...mList[path].default
|
|
22
|
-
}
|
|
23
|
-
} else {
|
|
24
|
-
msg[pathName] = mList[path].default
|
|
25
|
-
}
|
|
20
|
+
const existing = msg[pathName] as Record<string, unknown> | undefined
|
|
21
|
+
msg[pathName] = existing ? { ...existing, ...mod.default } : mod.default
|
|
26
22
|
}
|
|
27
23
|
}
|
|
28
24
|
}
|
|
@@ -52,16 +48,16 @@ export default () => {
|
|
|
52
48
|
// 那么你就可以 使用 :lower:(小写) :upper:(大写) 来引入文件
|
|
53
49
|
// const viewModules = import.meta.globEager('../views/**/locales/[[:lower:]][[:lower:]]-[[:upper:]][[:upper:]].ts')
|
|
54
50
|
|
|
55
|
-
const allLangs
|
|
56
|
-
const message:
|
|
57
|
-
getLangFiles(modules, message)
|
|
51
|
+
const allLangs = (): Record<string, Record<string, unknown>> => {
|
|
52
|
+
const message: Record<string, Record<string, unknown>> = {}
|
|
53
|
+
getLangFiles(modules as Record<string, { default?: Record<string, unknown> }>, message)
|
|
58
54
|
return message
|
|
59
55
|
}
|
|
60
56
|
// const localeI18n = inject('locales') as { locale: string; messages: object }
|
|
61
|
-
const getI18n = (localeI18n?: { locale: string; messages:
|
|
57
|
+
const getI18n = (localeI18n?: { locale: string; messages: Record<string, unknown> }[]) => {
|
|
62
58
|
const combinateMessage = { ...allLangs() }
|
|
63
59
|
if (localeI18n) {
|
|
64
|
-
for (const l of localeI18n
|
|
60
|
+
for (const l of localeI18n) {
|
|
65
61
|
combinateMessage[l.locale] = l.messages
|
|
66
62
|
}
|
|
67
63
|
// combinateMessage = { ...allLangs(), ...localeI18n.messages }
|
|
@@ -70,7 +66,7 @@ export default () => {
|
|
|
70
66
|
return createI18n({
|
|
71
67
|
locale: 'zh-CN',
|
|
72
68
|
fallbackLocale: 'en',
|
|
73
|
-
messages: combinateMessage
|
|
69
|
+
messages: combinateMessage as Record<string, Record<string, string>>
|
|
74
70
|
})
|
|
75
71
|
// const combinateMessage = { ...allLangs(), ...localeI18n.messages }
|
|
76
72
|
// return useI18n({
|
package/src/lib/i18n/lang/en.ts
CHANGED
package/src/lib/iframe.vue
CHANGED
package/src/lib/index.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vue-stack-tabs - 入口与导出
|
|
3
|
+
*
|
|
4
|
+
* 导出:StackTab(组件)、useTabActions、useTabRouter、useTabLoading、IFrame、TabHeaderButton
|
|
5
|
+
* 安装:Vue.use(plugin) 注册全局组件 VueStackTabs、事件总线、i18n
|
|
6
|
+
*/
|
|
1
7
|
import type { App } from 'vue'
|
|
2
|
-
// import { createI18n } from 'vue-i18n'
|
|
3
8
|
import i18n from './i18n'
|
|
4
9
|
import StackTab from './StackTabs.vue'
|
|
5
|
-
import
|
|
10
|
+
import logVersion from './versionLogger'
|
|
6
11
|
import IFrame from './iframe.vue'
|
|
7
12
|
import useTabLoading from './hooks/useTabLoading'
|
|
8
13
|
import useTabRouter from './hooks/useTabRouter'
|
|
9
|
-
import
|
|
14
|
+
import useTabActions from './hooks/useTabActions'
|
|
10
15
|
import TabHeaderButton from '@/lib/components/TabHeader/TabHeaderButton.vue'
|
|
11
|
-
import
|
|
16
|
+
import useTabEventBus from '@/lib/hooks/useTabEventBus'
|
|
12
17
|
import './assets/style/index.scss'
|
|
13
18
|
//
|
|
14
19
|
// const i18n = createI18n({
|
|
@@ -25,10 +30,24 @@ declare module '@vue/runtime-core' {
|
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
export {
|
|
33
|
+
export {
|
|
34
|
+
IFrame,
|
|
35
|
+
useTabActions,
|
|
36
|
+
useTabLoading,
|
|
37
|
+
useTabRouter,
|
|
38
|
+
TabHeaderButton,
|
|
39
|
+
StackTab
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
postOpenTab,
|
|
43
|
+
onRefreshRequest,
|
|
44
|
+
MSG_REFRESH,
|
|
45
|
+
MSG_OPEN_TAB
|
|
46
|
+
} from './utils/iframeBridge'
|
|
47
|
+
export type { IframeOpenTabPayload } from './utils/iframeBridge'
|
|
29
48
|
export default {
|
|
30
|
-
install(Vue: App, options?: { locale: string; messages:
|
|
31
|
-
|
|
32
|
-
Vue.component('VueStackTabs', StackTab).use(
|
|
49
|
+
install(Vue: App, options?: { locale: string; messages: Record<string, unknown> }[]): void {
|
|
50
|
+
logVersion(import.meta.env.PACKAGE_VERSION)
|
|
51
|
+
Vue.component('VueStackTabs', StackTab).use(useTabEventBus).use(i18n().getI18n(options))
|
|
33
52
|
}
|
|
34
53
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { Stack } from './TabModel'
|
|
3
|
+
|
|
4
|
+
describe('Stack', () => {
|
|
5
|
+
it('push 与 peek', () => {
|
|
6
|
+
const s = new Stack<number>()
|
|
7
|
+
s.push(1)
|
|
8
|
+
expect(s.peek()).toBe(1)
|
|
9
|
+
s.push(2)
|
|
10
|
+
expect(s.peek()).toBe(2)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('pop 返回栈顶并移除', () => {
|
|
14
|
+
const s = new Stack<string>(['a', 'b'])
|
|
15
|
+
expect(s.pop()).toBe('b')
|
|
16
|
+
expect(s.pop()).toBe('a')
|
|
17
|
+
expect(s.pop()).toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('size 与 isEmpty', () => {
|
|
21
|
+
const s = new Stack<number>()
|
|
22
|
+
expect(s.isEmpty()).toBe(true)
|
|
23
|
+
expect(s.size()).toBe(0)
|
|
24
|
+
s.push(1)
|
|
25
|
+
expect(s.isEmpty()).toBe(false)
|
|
26
|
+
expect(s.size()).toBe(1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('list 返回元素数组', () => {
|
|
30
|
+
const s = new Stack<number>([1, 2, 3])
|
|
31
|
+
expect(s.list()).toEqual([1, 2, 3])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('clear 清空栈', () => {
|
|
35
|
+
const s = new Stack<number>([1, 2])
|
|
36
|
+
s.clear()
|
|
37
|
+
expect(s.isEmpty()).toBe(true)
|
|
38
|
+
expect(s.peek()).toBeUndefined()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* TabModel - 标签页数据模型
|
|
3
|
+
*
|
|
4
|
+
* 职责:定义标签、页面、栈结构及右键菜单的数据结构
|
|
5
|
+
* 使用:useTabPanel、useTabActions、ContextMenu 等
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 标签栏滚动方式:滚轮、按钮、或两者 */
|
|
2
9
|
export enum TabScrollMode {
|
|
3
10
|
// 使用滚轮
|
|
4
11
|
WHEEL = 'wheel',
|
|
@@ -19,56 +26,63 @@ export enum TabScrollMode {
|
|
|
19
26
|
// TAB_REFRESH = 'TAB_REFRESH',
|
|
20
27
|
// TAB_REFRESH_ALL = 'TAB_REFRESH_ALL'
|
|
21
28
|
// }
|
|
22
|
-
|
|
29
|
+
/** 标签栏滚动条拖拽时的临时状态 */
|
|
23
30
|
export interface DragData {
|
|
24
31
|
thumbLeft: number
|
|
25
32
|
startScrollLeft: number
|
|
26
33
|
startThumbLeft: number
|
|
27
34
|
startPageX: number
|
|
28
35
|
}
|
|
29
|
-
|
|
36
|
+
/** 滚动条滑块计算所需的数据 */
|
|
30
37
|
export interface ScrollData {
|
|
31
38
|
clientWidth: number
|
|
32
39
|
scrollWidth: number
|
|
33
40
|
scrollLeft: number
|
|
34
41
|
}
|
|
35
|
-
// 滚动条事件类型
|
|
36
42
|
export type ScrollEvents = {
|
|
37
43
|
ScrollUpdate: void
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
/**
|
|
41
|
-
* Tab data for open
|
|
42
|
-
*/
|
|
46
|
+
/** 打开标签时传入的完整数据(含 path、query) */
|
|
43
47
|
export interface ITabData extends ITabBase {
|
|
44
48
|
// tab name
|
|
45
49
|
title: string
|
|
46
50
|
path: string
|
|
47
51
|
query?: Record<string, string>
|
|
48
52
|
}
|
|
53
|
+
/** iframe 刷新方式:postMessage 由 iframe 内页自行刷新(不重建 DOM,动画正常);reload 强制重载 iframe */
|
|
54
|
+
export type IframeRefreshMode = 'postMessage' | 'reload'
|
|
55
|
+
|
|
56
|
+
/** 标签基础信息(id、title、是否可关闭/刷新、是否 iframe) */
|
|
49
57
|
export interface ITabBase {
|
|
50
58
|
id?: string
|
|
51
59
|
title: string
|
|
52
60
|
closable?: boolean
|
|
53
61
|
refreshable?: boolean
|
|
54
62
|
iframe?: boolean
|
|
63
|
+
/** iframe 刷新方式,默认 postMessage */
|
|
64
|
+
iframeRefreshMode?: IframeRefreshMode
|
|
55
65
|
}
|
|
66
|
+
/** 内部使用的标签项,含 active 状态和 pages 栈 */
|
|
56
67
|
export interface ITabItem extends ITabBase {
|
|
57
68
|
id: string
|
|
58
69
|
closable: boolean
|
|
59
70
|
refreshable: boolean
|
|
60
71
|
iframe: boolean
|
|
72
|
+
iframeRefreshMode?: IframeRefreshMode
|
|
61
73
|
url?: string
|
|
62
74
|
active: boolean
|
|
63
75
|
pages: Stack<ITabPage>
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
/** 标签内的单个页面(对应 keep-alive 的一个缓存实例) */
|
|
66
79
|
export interface ITabPage {
|
|
67
80
|
id: string
|
|
68
81
|
tabId: string
|
|
69
82
|
path: string
|
|
70
83
|
query?: Record<string, string>
|
|
71
84
|
}
|
|
85
|
+
/** 右键菜单项:图标、标题、回调、禁用条件 */
|
|
72
86
|
export interface IContextMenu {
|
|
73
87
|
icon?: string
|
|
74
88
|
title: string
|
|
@@ -84,11 +98,10 @@ export interface IContextMenu {
|
|
|
84
98
|
// path: string
|
|
85
99
|
// query: any
|
|
86
100
|
// }
|
|
101
|
+
/** 使用 Map 实现的栈结构,用于标签内的 pages 页面栈 */
|
|
87
102
|
export class Stack<T> {
|
|
88
|
-
// 存储的Map
|
|
89
103
|
private items: Map<number, T>
|
|
90
104
|
|
|
91
|
-
//
|
|
92
105
|
constructor(items?: T[]) {
|
|
93
106
|
this.items = new Map()
|
|
94
107
|
if (items) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ModuleOptions } from './module'
|
|
2
|
+
|
|
3
|
+
declare module 'nuxt/schema' {
|
|
4
|
+
interface PublicRuntimeConfig {
|
|
5
|
+
vueStackTabs: {
|
|
6
|
+
locale: string
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module '@nuxt/schema' {
|
|
12
|
+
interface PublicRuntimeConfig {
|
|
13
|
+
vueStackTabs: {
|
|
14
|
+
locale: string
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type { ModuleOptions }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { addPlugin, addImports, createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
2
|
+
|
|
3
|
+
export interface ModuleOptions {
|
|
4
|
+
/** 默认语言 */
|
|
5
|
+
locale?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default defineNuxtModule<ModuleOptions>({
|
|
9
|
+
meta: {
|
|
10
|
+
name: 'vue-stack-tabs',
|
|
11
|
+
configKey: 'vueStackTabs'
|
|
12
|
+
},
|
|
13
|
+
defaults: {
|
|
14
|
+
locale: 'zh-CN'
|
|
15
|
+
},
|
|
16
|
+
setup(options, nuxt) {
|
|
17
|
+
const { resolve } = createResolver(import.meta.url)
|
|
18
|
+
|
|
19
|
+
nuxt.options.runtimeConfig.public.vueStackTabs = {
|
|
20
|
+
locale: options.locale
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
addPlugin({
|
|
24
|
+
src: resolve('./runtime/plugin.client'),
|
|
25
|
+
mode: 'client'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
addImports([
|
|
29
|
+
{ name: 'useTabActions', from: 'vue-stack-tabs' },
|
|
30
|
+
{ name: 'useTabRouter', from: 'vue-stack-tabs' },
|
|
31
|
+
{ name: 'useTabLoading', from: 'vue-stack-tabs' }
|
|
32
|
+
])
|
|
33
|
+
}
|
|
34
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import VueStackTabs from 'vue-stack-tabs'
|
|
2
|
+
|
|
3
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
4
|
+
const config = useRuntimeConfig().public.vueStackTabs as {
|
|
5
|
+
locale?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
nuxtApp.vueApp.use(VueStackTabs, {
|
|
9
|
+
locale: config?.locale ?? 'zh-CN'
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
5
|
+
import { MSG_REFRESH, MSG_OPEN_TAB, postOpenTab, onRefreshRequest } from './iframeBridge'
|
|
6
|
+
|
|
7
|
+
describe('iframeBridge', () => {
|
|
8
|
+
it('MSG_REFRESH 和 MSG_OPEN_TAB 为预期常量', () => {
|
|
9
|
+
expect(MSG_REFRESH).toBe('vue-stack-tabs:refresh')
|
|
10
|
+
expect(MSG_OPEN_TAB).toBe('vue-stack-tabs:openTab')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('postOpenTab', () => {
|
|
14
|
+
it('有 window.parent 时调用 postMessage', () => {
|
|
15
|
+
const spy = vi.spyOn(window.parent, 'postMessage')
|
|
16
|
+
postOpenTab({ title: 't', path: '/p' })
|
|
17
|
+
expect(spy).toHaveBeenCalledWith(
|
|
18
|
+
{ type: MSG_OPEN_TAB, payload: { title: 't', path: '/p' } },
|
|
19
|
+
'*'
|
|
20
|
+
)
|
|
21
|
+
spy.mockRestore()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('onRefreshRequest', () => {
|
|
26
|
+
it('注册监听并返回取消函数', () => {
|
|
27
|
+
const cb = vi.fn()
|
|
28
|
+
const off = onRefreshRequest(cb)
|
|
29
|
+
expect(typeof off).toBe('function')
|
|
30
|
+
window.dispatchEvent(new MessageEvent('message', { data: { type: MSG_REFRESH } }))
|
|
31
|
+
expect(cb).toHaveBeenCalled()
|
|
32
|
+
off()
|
|
33
|
+
window.dispatchEvent(new MessageEvent('message', { data: { type: MSG_REFRESH } }))
|
|
34
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iframe 与父窗口的 postMessage 桥接
|
|
3
|
+
*
|
|
4
|
+
* 在 iframe 内的页面中引入,用于:
|
|
5
|
+
* - 接收父窗口的刷新指令并执行
|
|
6
|
+
* - 向父窗口发送打开新标签的请求
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 刷新消息:父 -> iframe,iframe 内页面需监听并执行 location.reload() 或自身刷新逻辑 */
|
|
10
|
+
export const MSG_REFRESH = 'vue-stack-tabs:refresh'
|
|
11
|
+
|
|
12
|
+
/** 打开标签消息:iframe -> 父,payload 为 ITabData */
|
|
13
|
+
export const MSG_OPEN_TAB = 'vue-stack-tabs:openTab'
|
|
14
|
+
|
|
15
|
+
export interface IframeOpenTabPayload {
|
|
16
|
+
id?: string
|
|
17
|
+
title: string
|
|
18
|
+
path: string
|
|
19
|
+
query?: Record<string, string>
|
|
20
|
+
closable?: boolean
|
|
21
|
+
refreshable?: boolean
|
|
22
|
+
iframe?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 在 iframe 内调用:向父窗口请求打开新标签
|
|
27
|
+
* @param payload 标签数据,至少需 title、path
|
|
28
|
+
*/
|
|
29
|
+
export function postOpenTab(payload: IframeOpenTabPayload) {
|
|
30
|
+
if (typeof window === 'undefined' || !window.parent) return
|
|
31
|
+
window.parent.postMessage(
|
|
32
|
+
{ type: MSG_OPEN_TAB, payload },
|
|
33
|
+
'*' // 父窗口可校验 origin,此处用 * 便于跨子域
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 在 iframe 内调用:注册刷新监听,收到父窗口刷新指令时执行 callback
|
|
39
|
+
* @param callback 默认 location.reload(),可自定义
|
|
40
|
+
* @returns 取消监听的函数
|
|
41
|
+
*/
|
|
42
|
+
export function onRefreshRequest(callback: () => void = () => window.location.reload()) {
|
|
43
|
+
if (typeof window === 'undefined') return () => {}
|
|
44
|
+
|
|
45
|
+
const handler = (ev: MessageEvent) => {
|
|
46
|
+
if (ev.data?.type === MSG_REFRESH) {
|
|
47
|
+
callback()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
window.addEventListener('message', handler)
|
|
51
|
+
return () => window.removeEventListener('message', handler)
|
|
52
|
+
}
|