vue-stack-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 +201 -0
- package/README.md +3 -0
- package/dist/favicon.svg +10 -0
- package/dist/style.css +1 -0
- package/dist/vue-stack-tabs.es.js +6386 -0
- package/dist/vue-stack-tabs.umd.js +46 -0
- package/package.json +92 -0
- package/src/lib/StackTabs.vue +136 -0
- package/src/lib/assets/image/close.svg +6 -0
- package/src/lib/assets/image/close_all.svg +9 -0
- package/src/lib/assets/image/close_fill.svg +1 -0
- package/src/lib/assets/image/close_lefts.svg +1 -0
- package/src/lib/assets/image/close_other.svg +1 -0
- package/src/lib/assets/image/close_rights.svg +1 -0
- package/src/lib/assets/image/disable.svg +11 -0
- package/src/lib/assets/image/fullscreen.svg +1 -0
- package/src/lib/assets/image/left_arrow.svg +1 -0
- package/src/lib/assets/image/loading.svg +15 -0
- package/src/lib/assets/image/mask.svg +7 -0
- package/src/lib/assets/image/move.svg +6 -0
- package/src/lib/assets/image/reload.svg +3 -0
- package/src/lib/assets/image/reload_all.svg +6 -0
- package/src/lib/assets/image/reload_lefts.svg +15 -0
- package/src/lib/assets/image/reload_rights.svg +15 -0
- package/src/lib/assets/image/restore.svg +1 -0
- package/src/lib/assets/image/right_arrow.svg +1 -0
- package/src/lib/assets/image/scroll_pointer.svg +13 -0
- package/src/lib/assets/style/common/mixins.scss +45 -0
- package/src/lib/assets/style/common/var.scss +65 -0
- package/src/lib/assets/style/icon.scss +93 -0
- package/src/lib/assets/style/index.scss +3 -0
- package/src/lib/assets/style/stackTab.scss +395 -0
- package/src/lib/assets/style/transition.scss +40 -0
- package/src/lib/banner.ts +51 -0
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +15 -0
- package/src/lib/components/ContextMenu/index.vue +128 -0
- package/src/lib/components/TabHeader/TabHeaderButton.vue +35 -0
- package/src/lib/components/TabHeader/TabHeaderItem.vue +63 -0
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +240 -0
- package/src/lib/components/TabHeader/index.vue +134 -0
- package/src/lib/hooks/useContextMenu.ts +50 -0
- package/src/lib/hooks/useStackTab.ts +65 -0
- package/src/lib/hooks/useTabLoading.ts +48 -0
- package/src/lib/hooks/useTabRouter.ts +95 -0
- package/src/lib/hooks/useTabpanel.tsx +489 -0
- package/src/lib/i18n/index.ts +72 -0
- package/src/lib/i18n/lang/en.ts +15 -0
- package/src/lib/i18n/lang/zh-CN.ts +15 -0
- package/src/lib/iframe.vue +7 -0
- package/src/lib/index.ts +25 -0
- package/src/lib/model/TabModel.ts +182 -0
- package/src/lib/utils/TabIdHelper.ts +76 -0
- package/src/lib/utils/TabScrollHelper.ts +104 -0
- package/src/lib/utils/UriHelper.ts +22 -0
- package/types/banner.d.ts +2 -0
- package/types/hooks/useStackTab.d.ts +8 -0
- package/types/hooks/useTabLoading.d.ts +5 -0
- package/types/hooks/useTabRouter.d.ts +5 -0
- package/types/iframe.vue.d.ts +2 -0
- package/types/index.d.ts +10 -0
- package/types/model/TabModel.d.ts +114 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<li
|
|
3
|
+
ref="curtab"
|
|
4
|
+
class="stack-tab__item"
|
|
5
|
+
:class="{
|
|
6
|
+
'is-active': item.active,
|
|
7
|
+
'is-icon': item.closable
|
|
8
|
+
}"
|
|
9
|
+
@click="activeTab"
|
|
10
|
+
>
|
|
11
|
+
<!-- <div v-if="!!icon" class="stack-tab__item-icon" :class="icon"></div>-->
|
|
12
|
+
<div class="stack-tab__item-title">
|
|
13
|
+
<div class="stack-tab__item-title-content" :title="title">
|
|
14
|
+
{{ title }}
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div
|
|
18
|
+
v-if="item.closable"
|
|
19
|
+
class="stack-tab__icon-close-fill stack-tab__item-button"
|
|
20
|
+
:title="t('VueStackTab.close')"
|
|
21
|
+
@click.stop="closeTab"
|
|
22
|
+
/>
|
|
23
|
+
</li>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts" name="TabHeaderItem">
|
|
27
|
+
import { computed, ref, watch } from 'vue'
|
|
28
|
+
import type { ITabItem } from '@/lib/model/TabModel'
|
|
29
|
+
import localeI18n from '@/lib/i18n'
|
|
30
|
+
// 消息
|
|
31
|
+
const emit = defineEmits(['close', 'active'])
|
|
32
|
+
|
|
33
|
+
const { t } = localeI18n().getI18n()
|
|
34
|
+
const curtab = ref<HTMLElement>()
|
|
35
|
+
const props = defineProps<{
|
|
36
|
+
item: ITabItem
|
|
37
|
+
}>()
|
|
38
|
+
/**
|
|
39
|
+
* 如果Active为true,就看看
|
|
40
|
+
*/
|
|
41
|
+
watch(
|
|
42
|
+
() => props.item.active,
|
|
43
|
+
(val) => {
|
|
44
|
+
if (val) {
|
|
45
|
+
activeTab()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
const title = computed<string>(() => {
|
|
50
|
+
return props.item.title || t('undefined')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const closeTab = () => {
|
|
54
|
+
emit('close', props.item)
|
|
55
|
+
}
|
|
56
|
+
const activeTab = () => {
|
|
57
|
+
// if (!props.active) {
|
|
58
|
+
emit('active', props.item, curtab)
|
|
59
|
+
// }
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style lang="scss" scoped></style>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tab-header-button
|
|
3
|
+
v-if="isButtonVisibled"
|
|
4
|
+
icon-class="stack-tab__icon-left-arrow"
|
|
5
|
+
:disabled="isDisabledLeftButton"
|
|
6
|
+
@click="!isDisabledLeftButton && onButton(-1 * space)"
|
|
7
|
+
/>
|
|
8
|
+
<div ref="headerScroll" class="stack-tab__scroll" @wheel.prevent="onWheel">
|
|
9
|
+
<div
|
|
10
|
+
ref="container"
|
|
11
|
+
class="stack-tab__scroll-container"
|
|
12
|
+
:class="{
|
|
13
|
+
'stack-tab__shadow-inset-left': !isDisabledLeftButton && isDisabledRightButton,
|
|
14
|
+
'stack-tab__shadow-inset-right': !isDisabledRightButton && isDisabledLeftButton,
|
|
15
|
+
'stack-tab__shadow-inset-all': !isDisabledLeftButton && !isDisabledRightButton
|
|
16
|
+
}"
|
|
17
|
+
@scroll.passive="update"
|
|
18
|
+
>
|
|
19
|
+
<slot />
|
|
20
|
+
</div>
|
|
21
|
+
<div
|
|
22
|
+
v-show="hasScroller"
|
|
23
|
+
ref="bar"
|
|
24
|
+
class="stack-tab__scrollbar"
|
|
25
|
+
:class="{ 'is-dragging': scrollDragging }"
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
ref="thumb"
|
|
29
|
+
class="stack-tab__scrollbar-thumb"
|
|
30
|
+
:class="{ 'is-dragging': scrollDragging }"
|
|
31
|
+
:style="{
|
|
32
|
+
width: `${thumbWidth}px`,
|
|
33
|
+
transform: `translateX(${thumbLeft}px)`
|
|
34
|
+
}"
|
|
35
|
+
@mousedown.prevent="onDragStart"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<tab-header-button
|
|
40
|
+
v-if="isButtonVisibled"
|
|
41
|
+
icon-class="stack-tab__icon-right-arrow"
|
|
42
|
+
:disabled="isDisabledRightButton"
|
|
43
|
+
@click="!isDisabledRightButton && onButton(space!)"
|
|
44
|
+
/>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script lang="ts" setup>
|
|
48
|
+
import { computed, ref, reactive, onMounted, watch, onUnmounted } from 'vue'
|
|
49
|
+
import { ResizeObserver } from '@juggle/resize-observer'
|
|
50
|
+
import type { ScrollData, DragData } from '@/lib/model/TabModel'
|
|
51
|
+
import { _scrollTo, _scrollIntoView } from '@/lib/utils/TabScrollHelper'
|
|
52
|
+
import TabHeaderButton from '@/lib/components/TabHeader/TabHeaderButton.vue'
|
|
53
|
+
|
|
54
|
+
// ref
|
|
55
|
+
const container = ref<HTMLElement | null>(null)
|
|
56
|
+
const bar = ref<HTMLElement | null>(null)
|
|
57
|
+
const thumb = ref<HTMLElement | null>(null)
|
|
58
|
+
const scrollDragging = ref<boolean>(false)
|
|
59
|
+
const isDisabledLeftButton = ref<boolean>(false)
|
|
60
|
+
const isDisabledRightButton = ref<boolean>(false)
|
|
61
|
+
const isButtonVisibled = ref<boolean>(true)
|
|
62
|
+
const headerScroll = ref<HTMLElement>()
|
|
63
|
+
const props = withDefaults(
|
|
64
|
+
defineProps<{
|
|
65
|
+
// 每次移动距离
|
|
66
|
+
space?: number
|
|
67
|
+
isScrollWheel?: boolean
|
|
68
|
+
isScrollButton?: boolean
|
|
69
|
+
}>(),
|
|
70
|
+
{
|
|
71
|
+
space: 300,
|
|
72
|
+
isScrollWheel: true,
|
|
73
|
+
isScrollButton: true
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
/**
|
|
77
|
+
* 滚动条数据
|
|
78
|
+
*/
|
|
79
|
+
const scrollData = reactive<ScrollData>({
|
|
80
|
+
clientWidth: 0,
|
|
81
|
+
scrollWidth: 0,
|
|
82
|
+
scrollLeft: 0
|
|
83
|
+
})
|
|
84
|
+
const dragData = reactive<DragData>({
|
|
85
|
+
thumbLeft: 0,
|
|
86
|
+
startScrollLeft: 0,
|
|
87
|
+
startThumbLeft: 0,
|
|
88
|
+
startPageX: 0
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// 监控滚动条数据用于启用/禁用按钮
|
|
92
|
+
watch(scrollData, (newValue) => {
|
|
93
|
+
setScrollButton(newValue)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const setScrollButton = (scrollData: ScrollData) => {
|
|
97
|
+
isDisabledLeftButton.value = scrollData.scrollLeft <= 10
|
|
98
|
+
isDisabledRightButton.value =
|
|
99
|
+
scrollData.clientWidth + scrollData.scrollLeft - scrollData.scrollWidth >= -10
|
|
100
|
+
isButtonVisibled.value = scrollData.scrollWidth !== scrollData.clientWidth
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 计算属性
|
|
104
|
+
// 是否拥有滚动条
|
|
105
|
+
const hasScroller = computed<boolean>(() => {
|
|
106
|
+
return scrollData.scrollWidth > scrollData.clientWidth
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// 滑块宽度
|
|
110
|
+
const thumbWidth = computed<number>(() => {
|
|
111
|
+
if (!hasScroller.value) return 0
|
|
112
|
+
|
|
113
|
+
const { clientWidth, scrollWidth } = scrollData
|
|
114
|
+
return (clientWidth / scrollWidth) * clientWidth
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// 滑块 left
|
|
118
|
+
const thumbLeft = computed(() => {
|
|
119
|
+
if (!hasScroller.value) return 0
|
|
120
|
+
|
|
121
|
+
const { clientWidth, scrollWidth, scrollLeft } = scrollData
|
|
122
|
+
|
|
123
|
+
return (clientWidth - thumbWidth.value) * (scrollLeft / (scrollWidth - clientWidth))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// 方法
|
|
127
|
+
// 更新滚动数据
|
|
128
|
+
const update = () => {
|
|
129
|
+
// 判断 container是否为空,主空直接返回
|
|
130
|
+
if (container.value === null || container.value === undefined) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
const { clientWidth, scrollWidth, scrollLeft } = container.value
|
|
134
|
+
|
|
135
|
+
Object.assign(scrollData, { clientWidth, scrollWidth, scrollLeft })
|
|
136
|
+
}
|
|
137
|
+
const resizeUpdate = new ResizeObserver(() => {
|
|
138
|
+
update()
|
|
139
|
+
const cur = container.value?.querySelector('.stack-tab__item.is-active')
|
|
140
|
+
if (cur) {
|
|
141
|
+
if (!isInView(cur as HTMLElement)) {
|
|
142
|
+
scrollIntoView(cur as HTMLElement)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
const tabsUpdate = new ResizeObserver(() => {
|
|
147
|
+
if (!hasScroller.value) {
|
|
148
|
+
const cur = container.value?.querySelector('.stack-tab__item.is-active')
|
|
149
|
+
if (cur) {
|
|
150
|
+
if (!isInView(cur as HTMLElement)) {
|
|
151
|
+
scrollIntoView(cur as HTMLElement)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
update()
|
|
156
|
+
})
|
|
157
|
+
// 滚动到指定位置
|
|
158
|
+
const scrollTo = (left: number, smooth = true) => {
|
|
159
|
+
_scrollTo({ wrap: container.value, left, smooth })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const onButton = (space: number) => {
|
|
163
|
+
scrollTo(container.value!.scrollLeft + space)
|
|
164
|
+
}
|
|
165
|
+
// 页签鼠标滚动
|
|
166
|
+
const onWheel = (e: WheelEvent) => {
|
|
167
|
+
const isWheelUp = e.deltaY < 0
|
|
168
|
+
scrollTo(container.value!.scrollLeft + (isWheelUp ? -props.space : props.space))
|
|
169
|
+
}
|
|
170
|
+
// 拖拽
|
|
171
|
+
const onDragStart = (e: MouseEvent) => {
|
|
172
|
+
Object.assign(dragData, {
|
|
173
|
+
startPageX: e.pageX,
|
|
174
|
+
startScrollLeft: container.value?.scrollLeft,
|
|
175
|
+
startThumbLeft: thumbLeft.value,
|
|
176
|
+
thumbLeft: thumbLeft.value
|
|
177
|
+
})
|
|
178
|
+
scrollDragging.value = true
|
|
179
|
+
document.addEventListener('mousemove', onDragMove)
|
|
180
|
+
document.addEventListener('mouseup', onDragEnd)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 拖拽移动
|
|
184
|
+
const onDragMove = (e: MouseEvent) => {
|
|
185
|
+
const { clientWidth, scrollWidth } = scrollData
|
|
186
|
+
let thumbLeft = dragData.startThumbLeft + e.pageX - dragData.startPageX
|
|
187
|
+
const maxThumbLeft = clientWidth - thumbWidth.value
|
|
188
|
+
|
|
189
|
+
if (thumbLeft < 0) {
|
|
190
|
+
thumbLeft = 0
|
|
191
|
+
} else if (thumbLeft > maxThumbLeft) {
|
|
192
|
+
thumbLeft = maxThumbLeft
|
|
193
|
+
}
|
|
194
|
+
// 更新滑块定位
|
|
195
|
+
dragData.thumbLeft = thumbLeft
|
|
196
|
+
// 滚动
|
|
197
|
+
scrollTo((thumbLeft * (scrollWidth - clientWidth)) / (clientWidth - thumbWidth.value), false)
|
|
198
|
+
e.preventDefault()
|
|
199
|
+
}
|
|
200
|
+
// 拖拽结束
|
|
201
|
+
const onDragEnd = (e: MouseEvent) => {
|
|
202
|
+
scrollDragging.value = false
|
|
203
|
+
document.removeEventListener('mousemove', onDragMove)
|
|
204
|
+
document.removeEventListener('mouseup', onDragEnd)
|
|
205
|
+
|
|
206
|
+
e.preventDefault()
|
|
207
|
+
}
|
|
208
|
+
onMounted(() => {
|
|
209
|
+
resizeUpdate.observe(headerScroll.value as Element)
|
|
210
|
+
const tabNav = container.value?.querySelector('.stack-tab__nav')
|
|
211
|
+
tabsUpdate.observe(tabNav!)
|
|
212
|
+
})
|
|
213
|
+
onUnmounted(() => {
|
|
214
|
+
resizeUpdate.disconnect()
|
|
215
|
+
tabsUpdate.disconnect()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// 滚动到元素
|
|
219
|
+
const scrollIntoView = (el: HTMLElement) => {
|
|
220
|
+
_scrollIntoView({ el, wrap: container, block: 'end', inline: 'center' })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 判断子节点是否完全在可视区域
|
|
224
|
+
const isInView = (el: HTMLElement) => {
|
|
225
|
+
if (!hasScroller.value) {
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
const offsetLeft = el.offsetLeft
|
|
229
|
+
const scrollLeft = container.value!.scrollLeft
|
|
230
|
+
|
|
231
|
+
return !(
|
|
232
|
+
offsetLeft < scrollLeft ||
|
|
233
|
+
offsetLeft + el.clientWidth > scrollLeft + container.value!.clientWidth
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
defineExpose({ scrollIntoView, isInView })
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<style scoped></style>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="stack-tab__header">
|
|
3
|
+
<slot name="leftButton" />
|
|
4
|
+
<tab-header-scroll
|
|
5
|
+
ref="scroll"
|
|
6
|
+
:space="space"
|
|
7
|
+
:is-scroll-wheel="isScrollWheel"
|
|
8
|
+
:is-scroll-button="isScrollButton"
|
|
9
|
+
>
|
|
10
|
+
<template #default>
|
|
11
|
+
<transition-group
|
|
12
|
+
key="tabItemTrans"
|
|
13
|
+
tag="ul"
|
|
14
|
+
class="stack-tab__nav"
|
|
15
|
+
v-bind="tabTrans"
|
|
16
|
+
appear
|
|
17
|
+
>
|
|
18
|
+
<tab-header-item
|
|
19
|
+
v-for="(item, index) in tabs"
|
|
20
|
+
:key="item.id"
|
|
21
|
+
:contextmenu="contextmenu"
|
|
22
|
+
:item="item"
|
|
23
|
+
@contextmenu.prevent="
|
|
24
|
+
(e: MouseEvent) => showContextMenu(e, item as ITabItem, index, tabs.length)
|
|
25
|
+
"
|
|
26
|
+
@click.middle.prevent="closeTab(item as ITabItem)"
|
|
27
|
+
@close="closeTab"
|
|
28
|
+
@active="activeTab"
|
|
29
|
+
/>
|
|
30
|
+
</transition-group>
|
|
31
|
+
</template>
|
|
32
|
+
</tab-header-scroll>
|
|
33
|
+
<slot name="rightButton" />
|
|
34
|
+
<tab-header-button
|
|
35
|
+
:icon-class="maximum ? 'stack-tab__icon-restore' : 'stack-tab__icon-fullscreen'"
|
|
36
|
+
:title="t('VueStackTab.maximum')"
|
|
37
|
+
@click="maximum = !maximum"
|
|
38
|
+
/>
|
|
39
|
+
<transition name="stack-tab-zoom" appear>
|
|
40
|
+
<context-menu
|
|
41
|
+
v-if="contextMenuShown"
|
|
42
|
+
key="tabMenuTrans"
|
|
43
|
+
:left="contextMenuData.left"
|
|
44
|
+
:top="contextMenuData.top"
|
|
45
|
+
:tab-item="contextMenuData.item as ITabItem"
|
|
46
|
+
:max="contextMenuData.max"
|
|
47
|
+
:index="contextMenuData.index"
|
|
48
|
+
/>
|
|
49
|
+
</transition>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script lang="ts" setup>
|
|
54
|
+
import { inject, computed, ref } from 'vue'
|
|
55
|
+
import type { TransitionProps, Ref } from 'vue'
|
|
56
|
+
import { TabScrollMode } from '@/lib/model/TabModel'
|
|
57
|
+
import type { ITabItem } from '@/lib/model/TabModel'
|
|
58
|
+
import ContextMenu from '@/lib/components/ContextMenu/index.vue'
|
|
59
|
+
import useContextMenu from '@/lib/hooks/useContextMenu'
|
|
60
|
+
import localeI18n from '@/lib/i18n'
|
|
61
|
+
import useTabpanel from '@/lib/hooks/useTabpanel'
|
|
62
|
+
import TabHeaderItem from '@/lib/components/TabHeader/TabHeaderItem.vue'
|
|
63
|
+
import TabHeaderScroll from '@/lib/components/TabHeader/TabHeaderScroll.vue'
|
|
64
|
+
import TabHeaderButton from '@/lib/components/TabHeader/TabHeaderButton.vue'
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
const { t } = localeI18n().getI18n()
|
|
67
|
+
const maximum = inject<boolean>('maximum')
|
|
68
|
+
const emit = defineEmits(['active'])
|
|
69
|
+
const scroll = ref()
|
|
70
|
+
const { shown: contextMenuShown, contextMenuData, showContextMenu } = useContextMenu()
|
|
71
|
+
|
|
72
|
+
const props = withDefaults(
|
|
73
|
+
defineProps<{
|
|
74
|
+
space: number
|
|
75
|
+
// tab的滚动方式
|
|
76
|
+
tabScrollMode?: TabScrollMode
|
|
77
|
+
// tab动画
|
|
78
|
+
tabTransition?: string | TransitionProps
|
|
79
|
+
contextmenu?: boolean | object
|
|
80
|
+
max?: number
|
|
81
|
+
}>(),
|
|
82
|
+
{
|
|
83
|
+
tabTransition: () => {
|
|
84
|
+
return {
|
|
85
|
+
name: 'stack-tab-zoom'
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
tabScrollMode: TabScrollMode.BOTH,
|
|
89
|
+
contextmenu: true,
|
|
90
|
+
max: 20
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
const { removeTab, active, tabs } = useTabpanel()
|
|
94
|
+
|
|
95
|
+
// 转换tab动画
|
|
96
|
+
const tabTrans = computed<TransitionProps>(() => {
|
|
97
|
+
if (typeof props.tabTransition === 'string') {
|
|
98
|
+
return {
|
|
99
|
+
name: props.tabTransition
|
|
100
|
+
} as TransitionProps
|
|
101
|
+
}
|
|
102
|
+
return props.tabTransition as TransitionProps
|
|
103
|
+
})
|
|
104
|
+
// 判断是否有滚动按钮
|
|
105
|
+
const isScrollButton = computed<boolean>(() => {
|
|
106
|
+
return props.tabScrollMode === TabScrollMode.BOTH || props.tabScrollMode === TabScrollMode.BUTTON
|
|
107
|
+
})
|
|
108
|
+
// 判断是否可以使用滚轮
|
|
109
|
+
const isScrollWheel =
|
|
110
|
+
props.tabScrollMode === TabScrollMode.BOTH || props.tabScrollMode === TabScrollMode.WHEEL
|
|
111
|
+
|
|
112
|
+
// 关闭前,先判断是不最先中状态
|
|
113
|
+
const closeTab = (item: ITabItem) => {
|
|
114
|
+
// emit('close', item)
|
|
115
|
+
if (item.closable) {
|
|
116
|
+
const activeId = removeTab(item.id)
|
|
117
|
+
emit('active', activeId)
|
|
118
|
+
}
|
|
119
|
+
contextMenuShown.value = false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const activeTab = (item: ITabItem, curActiveTab: Ref<HTMLElement>) => {
|
|
123
|
+
if (scroll.value) {
|
|
124
|
+
if (!scroll.value!.isInView(curActiveTab.value)) {
|
|
125
|
+
scroll.value!.scrollIntoView(curActiveTab.value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
active(item.id)
|
|
129
|
+
emit('active', item.id)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<style scoped></style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { onUnmounted, onMounted, ref, reactive, nextTick } from 'vue'
|
|
2
|
+
import type { ITabItem } from '@/lib/model/TabModel'
|
|
3
|
+
export default () => {
|
|
4
|
+
const contextMenuData = reactive({
|
|
5
|
+
item: {},
|
|
6
|
+
index: -1,
|
|
7
|
+
left: 0,
|
|
8
|
+
top: 0,
|
|
9
|
+
max: 0
|
|
10
|
+
})
|
|
11
|
+
const shown = ref<boolean>(false)
|
|
12
|
+
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
document.addEventListener('click', clickOutSide)
|
|
15
|
+
})
|
|
16
|
+
onUnmounted(() => {
|
|
17
|
+
document.removeEventListener('click', clickOutSide)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const showContextMenu = async (e: MouseEvent, item: ITabItem, index: number, max: number) => {
|
|
21
|
+
// 关闭已打开的菜单
|
|
22
|
+
shown.value = false
|
|
23
|
+
|
|
24
|
+
await nextTick(() => {
|
|
25
|
+
const { clientY: top, clientX: left } = e
|
|
26
|
+
shown.value = true
|
|
27
|
+
Object.assign(contextMenuData, { item, index, top, left, max })
|
|
28
|
+
|
|
29
|
+
console.log('showContextMenu in callback')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// .then(() => {
|
|
33
|
+
|
|
34
|
+
// })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const clickOutSide = (ev: MouseEvent) => {
|
|
38
|
+
const classList = (ev.target as Element).classList
|
|
39
|
+
if (classList.contains('stack-tab__contextmenu')) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
shown.value = false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
shown,
|
|
47
|
+
contextMenuData,
|
|
48
|
+
showContextMenu
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useRouter } from 'vue-router'
|
|
2
|
+
import useTabpanel from '@/lib/hooks/useTabpanel'
|
|
3
|
+
import { throttle } from 'lodash-es'
|
|
4
|
+
import type { ITabData } from '@/lib/model/TabModel'
|
|
5
|
+
import { defu } from 'defu'
|
|
6
|
+
import { encodeTabInfo } from '@/lib/utils/TabIdHelper'
|
|
7
|
+
import { uriDecode } from '@/lib/utils/UriHelper'
|
|
8
|
+
import { ulid } from 'ulidx'
|
|
9
|
+
let iframePath: string
|
|
10
|
+
export default () => {
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
const { active, hasTab, pageShown, reset } = useTabpanel()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 打开新的TAB页面
|
|
16
|
+
* @param tab
|
|
17
|
+
* @param to
|
|
18
|
+
*/
|
|
19
|
+
const openNewTab = throttle(async (tab: ITabData) => {
|
|
20
|
+
if (!pageShown.value) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const tabInfo = defu(tab, { refreshable: true, closable: true, iframe: false })
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* In the function of openNewTab, if the tab already 'ACTIVE' then do nothing.
|
|
27
|
+
*/
|
|
28
|
+
if (hasTab(tabInfo.id!)) {
|
|
29
|
+
active(tabInfo.id!)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if (!tabInfo.id) {
|
|
33
|
+
tabInfo.id = ulid()
|
|
34
|
+
}
|
|
35
|
+
const __tab = await encodeTabInfo(tabInfo)
|
|
36
|
+
let query = defu(
|
|
37
|
+
{
|
|
38
|
+
__tab
|
|
39
|
+
},
|
|
40
|
+
tab.query
|
|
41
|
+
) as Record<string, string>
|
|
42
|
+
let path
|
|
43
|
+
if (!tab.iframe) {
|
|
44
|
+
const url = uriDecode(tab.path)
|
|
45
|
+
path = url.path
|
|
46
|
+
query = defu(url.query, query)
|
|
47
|
+
} else {
|
|
48
|
+
const __src = encodeURIComponent(tab.path as string)
|
|
49
|
+
path = iframePath
|
|
50
|
+
query['__src'] = __src
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
router.push({
|
|
54
|
+
path,
|
|
55
|
+
query
|
|
56
|
+
})
|
|
57
|
+
return tabInfo.id
|
|
58
|
+
}, 500)
|
|
59
|
+
|
|
60
|
+
const setIFramePath = (path: string) => {
|
|
61
|
+
iframePath = path
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { openNewTab, setIFramePath, active, reset }
|
|
65
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useLoading } from 'vue-loading-overlay'
|
|
2
|
+
import type { ActiveLoader } from 'vue-loading-overlay'
|
|
3
|
+
import { type ComponentInternalInstance, getCurrentInstance, onUnmounted } from 'vue'
|
|
4
|
+
|
|
5
|
+
export default () => {
|
|
6
|
+
const $loading = useLoading()
|
|
7
|
+
const { attrs } = getCurrentInstance() as ComponentInternalInstance
|
|
8
|
+
let loadingInstance: ActiveLoader | null = null
|
|
9
|
+
onUnmounted(() => {
|
|
10
|
+
closeTabLoading()
|
|
11
|
+
})
|
|
12
|
+
const openTabLoading = () => {
|
|
13
|
+
const container = document.querySelector(`#${attrs.tId}`)
|
|
14
|
+
// const wrappers = document.getElementsByClassName('cache-page-wrapper')
|
|
15
|
+
// for(const wrapper of wrappers){
|
|
16
|
+
// const childs = wrapper.children
|
|
17
|
+
// for(const child of childs){
|
|
18
|
+
// if(child.getAttribute("tId")===attrs.tId){
|
|
19
|
+
// container=wrapper
|
|
20
|
+
// break;
|
|
21
|
+
// }
|
|
22
|
+
// }
|
|
23
|
+
// if(container!==null){
|
|
24
|
+
// break;
|
|
25
|
+
// }
|
|
26
|
+
// }
|
|
27
|
+
if (loadingInstance === null) {
|
|
28
|
+
loadingInstance = $loading.show({
|
|
29
|
+
canCancel: false,
|
|
30
|
+
isFullPage: false,
|
|
31
|
+
height: 85,
|
|
32
|
+
width: 85,
|
|
33
|
+
loader: 'spinner',
|
|
34
|
+
color: '#007bff',
|
|
35
|
+
|
|
36
|
+
container: container!
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const closeTabLoading = () => {
|
|
42
|
+
if (loadingInstance !== null) {
|
|
43
|
+
loadingInstance.hide()
|
|
44
|
+
loadingInstance = null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { openTabLoading, closeTabLoading }
|
|
48
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defu } from 'defu'
|
|
2
|
+
|
|
3
|
+
import { getCurrentInstance } from 'vue'
|
|
4
|
+
import type { ComponentInternalInstance } from 'vue'
|
|
5
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
6
|
+
|
|
7
|
+
import type { TabRouteLocationRaw } from '@/lib/model/TabModel'
|
|
8
|
+
import useTabpanel from '@/lib/hooks/useTabpanel'
|
|
9
|
+
import { uriDecode } from '@/lib/utils/UriHelper'
|
|
10
|
+
export default () => {
|
|
11
|
+
const { attrs } = getCurrentInstance() as ComponentInternalInstance
|
|
12
|
+
const { pageShown, getTab, markDeletableCache, removeComponent, getComponent } = useTabpanel()
|
|
13
|
+
const route = useRoute()
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
let currentId = ''
|
|
16
|
+
// const currentPageId = ''
|
|
17
|
+
let currentTab = ''
|
|
18
|
+
|
|
19
|
+
currentId = attrs.tId as string
|
|
20
|
+
// currentPageId = attrs.pId as string
|
|
21
|
+
currentTab = route.query.__tab as string
|
|
22
|
+
|
|
23
|
+
const forward = (to: TabRouteLocationRaw) => {
|
|
24
|
+
pageShown.value = false
|
|
25
|
+
const query = defu(
|
|
26
|
+
{
|
|
27
|
+
__tab: currentTab
|
|
28
|
+
},
|
|
29
|
+
to.query
|
|
30
|
+
)
|
|
31
|
+
router.push({
|
|
32
|
+
path: to.path,
|
|
33
|
+
query
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 返回
|
|
38
|
+
* @param to url or step
|
|
39
|
+
* @param query 返回时要加截的数据
|
|
40
|
+
*/
|
|
41
|
+
const backward = (to: number | string, query?: Object) => {
|
|
42
|
+
pageShown.value = false
|
|
43
|
+
|
|
44
|
+
const tab = getTab(currentId)
|
|
45
|
+
if (!tab) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
const tabStack = tab.pages
|
|
49
|
+
let absTo = 0
|
|
50
|
+
if (typeof to === 'number') {
|
|
51
|
+
absTo = Math.abs(to)
|
|
52
|
+
} else {
|
|
53
|
+
let index = 1
|
|
54
|
+
let hasPath = false
|
|
55
|
+
const pageItems = tabStack.list()
|
|
56
|
+
const url = uriDecode(to)
|
|
57
|
+
query = defu(url.query, query)
|
|
58
|
+
for (let i = tabStack.size() - 2; i >= 0; i--) {
|
|
59
|
+
if (pageItems[i].path === url.path) {
|
|
60
|
+
hasPath = true
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
index += 1
|
|
64
|
+
}
|
|
65
|
+
if (hasPath) {
|
|
66
|
+
absTo = index
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (absTo === 0 || tabStack!.size() <= absTo) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
// const removeCaches = new Set()
|
|
74
|
+
for (let i = 0; i < absTo; i++) {
|
|
75
|
+
const popRoute = tabStack.pop()
|
|
76
|
+
removeComponent(popRoute!.id)
|
|
77
|
+
markDeletableCache(popRoute!.id)
|
|
78
|
+
}
|
|
79
|
+
const pageRoute = tabStack?.peek()
|
|
80
|
+
const component = getComponent(pageRoute!.id)
|
|
81
|
+
if (component !== null && component !== undefined) {
|
|
82
|
+
const sourceComponent = component.components.DynamicComponent
|
|
83
|
+
sourceComponent.props = defu(query, sourceComponent.props)
|
|
84
|
+
}
|
|
85
|
+
router.push({
|
|
86
|
+
path: pageRoute!.path,
|
|
87
|
+
query: pageRoute!.query
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
forward,
|
|
93
|
+
backward
|
|
94
|
+
}
|
|
95
|
+
}
|