vue-stack-tabs 1.3.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.md +2 -0
- package/package.json +45 -45
- package/src/lib/assets/style/stackTab.scss +26 -1
- package/src/lib/components/TabHeader/TabHeaderItem.vue +30 -2
- package/src/lib/components/TabHeader/index.vue +72 -1
- package/src/lib/hooks/useTabActions.ts +2 -0
- package/src/lib/hooks/useTabPanel.tsx +24 -0
package/README.md
CHANGED
|
@@ -247,6 +247,7 @@ const {
|
|
|
247
247
|
refreshTab, // (id: string) => void
|
|
248
248
|
refreshAllTabs, // () => void
|
|
249
249
|
activeTab, // (id: string, isRoute?: boolean) => void
|
|
250
|
+
moveTab, // (id: string, targetIndex: number) => boolean
|
|
250
251
|
reset, // () => void
|
|
251
252
|
tabs // Ref<ITabItem[]>
|
|
252
253
|
} = useTabActions()
|
|
@@ -260,6 +261,7 @@ const {
|
|
|
260
261
|
| `refreshTab(id)` | 刷新指定标签(替换缓存 ID,重建组件实例) |
|
|
261
262
|
| `refreshAllTabs()` | 刷新所有标签 |
|
|
262
263
|
| `activeTab(id)` | 激活指定标签(切换 Tab) |
|
|
264
|
+
| `moveTab(id, targetIndex)` | 将标签移动到指定索引;标签栏也支持鼠标拖放排序 |
|
|
263
265
|
| `reset()` | 关闭所有标签并重置状态 |
|
|
264
266
|
|
|
265
267
|
---
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vue-stack-tabs",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"packageManager": "pnpm@11.
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -74,73 +74,73 @@
|
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"defu": "^6.1.7",
|
|
76
76
|
"mitt": "^3.0.1",
|
|
77
|
+
"nostics": "^1.2.0",
|
|
77
78
|
"throttle-debounce": "^5.0.2",
|
|
78
|
-
"vue": "^3.5.
|
|
79
|
+
"vue": "^3.5.42",
|
|
79
80
|
"vue-i18n-lite": "^1.0.2",
|
|
80
|
-
"vue-router": "^5.
|
|
81
|
+
"vue-router": "^5.3.0"
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
84
|
"@ctrl/tinycolor": "^4.2.0",
|
|
84
85
|
"@element-plus/icons-vue": "^2.3.2",
|
|
85
86
|
"@eslint/js": "^10.0.1",
|
|
86
|
-
"@floating-ui/core": "^1.
|
|
87
|
-
"@floating-ui/dom": "^1.
|
|
88
|
-
"@floating-ui/utils": "^0.2.
|
|
89
|
-
"@mdit/plugin-tasklist": "^
|
|
87
|
+
"@floating-ui/core": "^1.8.0",
|
|
88
|
+
"@floating-ui/dom": "^1.8.0",
|
|
89
|
+
"@floating-ui/utils": "^0.2.12",
|
|
90
|
+
"@mdit/plugin-tasklist": "^1.1.0",
|
|
90
91
|
"@popperjs/core": "^2.11.8",
|
|
91
92
|
"@rushstack/eslint-patch": "^1.16.1",
|
|
92
|
-
"@tsconfig/
|
|
93
|
+
"@tsconfig/node26": "^26.0.1",
|
|
93
94
|
"@types/lodash-es": "^4.17.12",
|
|
94
|
-
"@types/node": "^
|
|
95
|
+
"@types/node": "^26.4.0",
|
|
95
96
|
"@types/throttle-debounce": "^5.0.2",
|
|
96
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
97
|
-
"@vitejs/plugin-vue-jsx": "^5.1.
|
|
98
|
-
"@vitest/coverage-v8": "4.1.
|
|
99
|
-
"@vue/compiler-core": "^3.5.
|
|
100
|
-
"@vue/compiler-dom": "^3.5.
|
|
101
|
-
"@vue/devtools-api": "^8.1
|
|
102
|
-
"@vue/devtools-kit": "^8.1
|
|
103
|
-
"@vue/devtools-shared": "^8.1
|
|
97
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
98
|
+
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
|
99
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
100
|
+
"@vue/compiler-core": "^3.5.42",
|
|
101
|
+
"@vue/compiler-dom": "^3.5.42",
|
|
102
|
+
"@vue/devtools-api": "^8.2.1",
|
|
103
|
+
"@vue/devtools-kit": "^8.2.1",
|
|
104
|
+
"@vue/devtools-shared": "^8.2.1",
|
|
104
105
|
"@vue/eslint-config-prettier": "^10.2.0",
|
|
105
|
-
"@vue/eslint-config-typescript": "^14.
|
|
106
|
-
"@vue/reactivity": "^3.5.
|
|
107
|
-
"@vue/runtime-core": "^3.5.
|
|
108
|
-
"@vue/runtime-dom": "^3.5.
|
|
109
|
-
"@vue/shared": "^3.5.
|
|
110
|
-
"@vue/test-utils": "^2.
|
|
106
|
+
"@vue/eslint-config-typescript": "^14.9.0",
|
|
107
|
+
"@vue/reactivity": "^3.5.42",
|
|
108
|
+
"@vue/runtime-core": "^3.5.42",
|
|
109
|
+
"@vue/runtime-dom": "^3.5.42",
|
|
110
|
+
"@vue/shared": "^3.5.42",
|
|
111
|
+
"@vue/test-utils": "^2.5.0",
|
|
111
112
|
"@vue/tsconfig": "^0.9.1",
|
|
112
|
-
"@vueuse/core": "^14.
|
|
113
|
-
"@vueuse/shared": "^14.
|
|
113
|
+
"@vueuse/core": "^14.4.0",
|
|
114
|
+
"@vueuse/shared": "^14.4.0",
|
|
114
115
|
"async-validator": "^4.2.5",
|
|
115
|
-
"birpc": "^4.
|
|
116
|
-
"dayjs": "^1.11.
|
|
117
|
-
"element-plus": "^2.14.
|
|
118
|
-
"eslint": "^10.
|
|
116
|
+
"birpc": "^4.2.0",
|
|
117
|
+
"dayjs": "^1.11.23",
|
|
118
|
+
"element-plus": "^2.14.5",
|
|
119
|
+
"eslint": "^10.9.1",
|
|
119
120
|
"eslint-config-prettier": "^10.1.8",
|
|
120
|
-
"eslint-plugin-vue": "^10.
|
|
121
|
-
"globals": "^17.
|
|
122
|
-
"happy-dom": "^20.
|
|
121
|
+
"eslint-plugin-vue": "^10.10.0",
|
|
122
|
+
"globals": "^17.11.0",
|
|
123
|
+
"happy-dom": "^20.12.0",
|
|
123
124
|
"hookable": "^6.1.1",
|
|
124
125
|
"lodash-es": "^4.18.1",
|
|
125
126
|
"lodash-unified": "^1.0.3",
|
|
126
127
|
"memoize-one": "^6.0.0",
|
|
127
128
|
"normalize-wheel-es": "^1.2.0",
|
|
128
129
|
"normalize.css": "^8.0.1",
|
|
129
|
-
"npm-run-all2": "^9.0.
|
|
130
|
+
"npm-run-all2": "^9.0.3",
|
|
130
131
|
"perfect-debounce": "^2.1.0",
|
|
131
|
-
"prettier": "^3.
|
|
132
|
-
"sass": "^1.
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"typescript": "
|
|
136
|
-
"
|
|
137
|
-
"unplugin-auto-import": "^21.0.0",
|
|
132
|
+
"prettier": "^3.9.6",
|
|
133
|
+
"sass-embedded": "^1.103.1",
|
|
134
|
+
"terser": "^5.51.2",
|
|
135
|
+
"typescript": "~7.0.2",
|
|
136
|
+
"typescript-eslint": "^8.69.0",
|
|
137
|
+
"unplugin-auto-import": "^21.1.0",
|
|
138
138
|
"unplugin-vue-components": "^32.1.0",
|
|
139
|
-
"vite": "^8.
|
|
140
|
-
"vite-plugin-dts": "5.0.
|
|
139
|
+
"vite": "^8.2.2",
|
|
140
|
+
"vite-plugin-dts": "5.0.3",
|
|
141
141
|
"vite-plugin-package-version": "^1.1.0",
|
|
142
142
|
"vitepress": "^1.6.4",
|
|
143
|
-
"vitest": "^4.1.
|
|
144
|
-
"vue-tsc": "^3.3.
|
|
143
|
+
"vitest": "^4.1.11",
|
|
144
|
+
"vue-tsc": "^3.3.11"
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
position: relative;
|
|
165
165
|
display: inline-flex;
|
|
166
166
|
height: 100%;
|
|
167
|
-
padding: 0;
|
|
167
|
+
padding: 0 16px 0 0;
|
|
168
168
|
margin: 0;
|
|
169
169
|
list-style: none;
|
|
170
170
|
flex-wrap: nowrap;
|
|
@@ -197,6 +197,31 @@
|
|
|
197
197
|
border-left: $stack-tab-border;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
&.is-dragging {
|
|
201
|
+
opacity: 0.55;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
&.is-drop-before::before,
|
|
205
|
+
&.is-drop-after::after {
|
|
206
|
+
position: absolute;
|
|
207
|
+
z-index: 2;
|
|
208
|
+
top: 2px;
|
|
209
|
+
bottom: 2px;
|
|
210
|
+
width: 5px;
|
|
211
|
+
content: '';
|
|
212
|
+
background: $stack-tab-color-primary;
|
|
213
|
+
border-radius: 2px;
|
|
214
|
+
box-shadow: 0 0 6px rgba($stack-tab-color-primary, 0.8);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
&.is-drop-before::before {
|
|
218
|
+
left: -3px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
&.is-drop-after::after {
|
|
222
|
+
right: -3px;
|
|
223
|
+
}
|
|
224
|
+
|
|
200
225
|
/*
|
|
201
226
|
激活右键菜单的页签
|
|
202
227
|
*/
|
|
@@ -5,10 +5,18 @@
|
|
|
5
5
|
class="stack-tab__item"
|
|
6
6
|
:class="{
|
|
7
7
|
'is-active': item.active,
|
|
8
|
-
'is-icon': item.closable
|
|
8
|
+
'is-icon': item.closable,
|
|
9
|
+
'is-dragging': dragging,
|
|
10
|
+
'is-drop-before': dropPosition === 'before',
|
|
11
|
+
'is-drop-after': dropPosition === 'after'
|
|
9
12
|
}"
|
|
13
|
+
:draggable="true"
|
|
10
14
|
@click.self="handleActivate(true)"
|
|
11
15
|
@contextmenu="$emit('contextmenu', $event)"
|
|
16
|
+
@dragstart="handleDragStart"
|
|
17
|
+
@dragend="handleDragEnd"
|
|
18
|
+
@dragover.prevent="$emit('dragover', item, $event)"
|
|
19
|
+
@drop.prevent="$emit('drop', item, $event)"
|
|
12
20
|
>
|
|
13
21
|
<button
|
|
14
22
|
:id="getStackTabTabId(item.id)"
|
|
@@ -52,13 +60,23 @@ import type { ITabItem } from '../../model/TabModel'
|
|
|
52
60
|
import { TabEventType, useTabEmitter } from '../../hooks/useTabEventBus'
|
|
53
61
|
import { getStackTabPanelId, getStackTabTabId } from '../../utils/stackTabsA11y'
|
|
54
62
|
import { useI18n } from 'vue-i18n-lite'
|
|
55
|
-
const emit = defineEmits([
|
|
63
|
+
const emit = defineEmits([
|
|
64
|
+
'close',
|
|
65
|
+
'active',
|
|
66
|
+
'contextmenu',
|
|
67
|
+
'dragstart',
|
|
68
|
+
'dragover',
|
|
69
|
+
'drop',
|
|
70
|
+
'dragend'
|
|
71
|
+
])
|
|
56
72
|
const emitter = useTabEmitter()
|
|
57
73
|
const { t } = useI18n()
|
|
58
74
|
/** 当前标签 DOM 元素,用于滚动到可视区域 */
|
|
59
75
|
const tabElementRef = ref<HTMLElement>()
|
|
76
|
+
const dragging = ref(false)
|
|
60
77
|
const props = defineProps<{
|
|
61
78
|
item: ITabItem
|
|
79
|
+
dropPosition?: 'before' | 'after'
|
|
62
80
|
}>()
|
|
63
81
|
|
|
64
82
|
/** TAB_ACTIVE 事件 payload */
|
|
@@ -80,6 +98,16 @@ const title = computed<string>(() => props.item.title || t('VueStackTab.undefine
|
|
|
80
98
|
const handleClose = () => {
|
|
81
99
|
emit('close', props.item)
|
|
82
100
|
}
|
|
101
|
+
const handleDragStart = (event: DragEvent) => {
|
|
102
|
+
dragging.value = true
|
|
103
|
+
event.dataTransfer?.setData('text/plain', props.item.id)
|
|
104
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
|
|
105
|
+
emit('dragstart', props.item, event)
|
|
106
|
+
}
|
|
107
|
+
const handleDragEnd = (event: DragEvent) => {
|
|
108
|
+
dragging.value = false
|
|
109
|
+
emit('dragend', props.item, event)
|
|
110
|
+
}
|
|
83
111
|
/** 激活当前标签,向父级 emit active 并传递 DOM 引用 */
|
|
84
112
|
const handleActivate = (isRoute = true) => {
|
|
85
113
|
emit('active', props.item, tabElementRef.value, isRoute)
|
|
@@ -17,11 +17,14 @@
|
|
|
17
17
|
v-bind="tabTransitionProps"
|
|
18
18
|
appear
|
|
19
19
|
@keydown="handleTabListKeydown"
|
|
20
|
+
@dragover.prevent="handleTabListDragOver"
|
|
21
|
+
@drop.prevent="handleTabListDrop"
|
|
20
22
|
>
|
|
21
23
|
<tab-header-item
|
|
22
24
|
v-for="(item, index) in tabs"
|
|
23
25
|
:key="item.id"
|
|
24
26
|
:item="item as ITabItem"
|
|
27
|
+
:drop-position="dropIndicator?.tabId === item.id ? dropIndicator.position : undefined"
|
|
25
28
|
@contextmenu="
|
|
26
29
|
(e: MouseEvent) =>
|
|
27
30
|
handleTabContextMenu(e, item as unknown as ITabItem, index, tabs.length)
|
|
@@ -29,6 +32,10 @@
|
|
|
29
32
|
@click.middle.prevent="handleCloseTab(item as ITabItem)"
|
|
30
33
|
@close="handleCloseTab"
|
|
31
34
|
@active="handleActivateTab"
|
|
35
|
+
@dragstart="handleTabDragStart"
|
|
36
|
+
@dragover="handleTabDragOver"
|
|
37
|
+
@drop="handleTabDrop"
|
|
38
|
+
@dragend="handleTabDragEnd"
|
|
32
39
|
/>
|
|
33
40
|
</transition-group>
|
|
34
41
|
</template>
|
|
@@ -109,7 +116,10 @@ const props = withDefaults(
|
|
|
109
116
|
max: 20
|
|
110
117
|
}
|
|
111
118
|
)
|
|
112
|
-
const { closeTab, activeTab, tabs } = useTabActions()
|
|
119
|
+
const { closeTab, activeTab, moveTab, tabs } = useTabActions()
|
|
120
|
+
const draggingTabId = ref<string | null>(null)
|
|
121
|
+
type DropPosition = 'before' | 'after'
|
|
122
|
+
const dropIndicator = ref<{ tabId: string; position: DropPosition } | null>(null)
|
|
113
123
|
|
|
114
124
|
/** 右键菜单是否启用;仅 false 明确禁用,其余旧对象值保持启用以兼容历史写法 */
|
|
115
125
|
const isContextMenuEnabled = computed<boolean>(() => props.contextmenu !== false)
|
|
@@ -137,6 +147,67 @@ const handleTabContextMenu = (e: MouseEvent, item: ITabItem, index: number, max:
|
|
|
137
147
|
showContextMenu(e, item, index, max)
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
/** 原生拖放只在释放鼠标时更新顺序,避免拖动经过标签时反复变更响应式列表。 */
|
|
151
|
+
const handleTabDragStart = (item: ITabItem) => {
|
|
152
|
+
draggingTabId.value = item.id
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleTabDragOver = (item: ITabItem, event: DragEvent) => {
|
|
156
|
+
if (!draggingTabId.value || draggingTabId.value === item.id) {
|
|
157
|
+
dropIndicator.value = null
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
const tabElement = event.currentTarget instanceof HTMLElement ? event.currentTarget : null
|
|
161
|
+
if (!tabElement) return
|
|
162
|
+
|
|
163
|
+
const { left, width } = tabElement.getBoundingClientRect()
|
|
164
|
+
dropIndicator.value = {
|
|
165
|
+
tabId: item.id,
|
|
166
|
+
position: event.clientX < left + width / 2 ? 'before' : 'after'
|
|
167
|
+
}
|
|
168
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const handleTabDrop = (target: ITabItem) => {
|
|
172
|
+
const sourceId = draggingTabId.value
|
|
173
|
+
const indicator = dropIndicator.value
|
|
174
|
+
draggingTabId.value = null
|
|
175
|
+
dropIndicator.value = null
|
|
176
|
+
if (!sourceId || sourceId === target.id || indicator?.tabId !== target.id) return
|
|
177
|
+
const sourceIndex = tabs.value.findIndex((tab) => tab.id === sourceId)
|
|
178
|
+
const targetIndex = tabs.value.findIndex((tab) => tab.id === target.id)
|
|
179
|
+
if (sourceIndex < 0 || targetIndex < 0) return
|
|
180
|
+
|
|
181
|
+
let destinationIndex = targetIndex
|
|
182
|
+
if (indicator.position === 'before' && sourceIndex < targetIndex) destinationIndex--
|
|
183
|
+
if (indicator.position === 'after' && sourceIndex > targetIndex) destinationIndex++
|
|
184
|
+
moveTab(sourceId, destinationIndex)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const handleTabDragEnd = () => {
|
|
188
|
+
draggingTabId.value = null
|
|
189
|
+
dropIndicator.value = null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 标签列表末端的留白也是投放区,便于明确地将标签移动到最后。 */
|
|
193
|
+
const isTabItemDragEvent = (event: DragEvent) =>
|
|
194
|
+
event.target instanceof Element && Boolean(event.target.closest('.stack-tab__item'))
|
|
195
|
+
|
|
196
|
+
const handleTabListDragOver = (event: DragEvent) => {
|
|
197
|
+
if (!draggingTabId.value || isTabItemDragEvent(event)) return
|
|
198
|
+
const lastTab = tabs.value[tabs.value.length - 1]
|
|
199
|
+
if (!lastTab || lastTab.id === draggingTabId.value) return
|
|
200
|
+
|
|
201
|
+
dropIndicator.value = { tabId: lastTab.id, position: 'after' }
|
|
202
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const handleTabListDrop = (event: DragEvent) => {
|
|
206
|
+
if (isTabItemDragEvent(event)) return
|
|
207
|
+
const lastTab = tabs.value[tabs.value.length - 1]
|
|
208
|
+
if (lastTab) handleTabDrop(lastTab as ITabItem)
|
|
209
|
+
}
|
|
210
|
+
|
|
140
211
|
/** 标签列表 transition 的 props,支持 string 或对象 */
|
|
141
212
|
const tabTransitionProps = computed<TransitionProps>(() =>
|
|
142
213
|
typeof props.tabTransition === 'string'
|
|
@@ -38,6 +38,7 @@ export default function useTabActions() {
|
|
|
38
38
|
renewTab,
|
|
39
39
|
getTab,
|
|
40
40
|
tabs,
|
|
41
|
+
moveTab,
|
|
41
42
|
removeTab,
|
|
42
43
|
removeAllTabs,
|
|
43
44
|
refreshTab,
|
|
@@ -169,6 +170,7 @@ export default function useTabActions() {
|
|
|
169
170
|
|
|
170
171
|
// 激活与重置
|
|
171
172
|
activeTab: active,
|
|
173
|
+
moveTab,
|
|
172
174
|
reset,
|
|
173
175
|
|
|
174
176
|
// 数据与配置
|
|
@@ -189,6 +189,29 @@ export default () => {
|
|
|
189
189
|
|
|
190
190
|
const getTab = (id: string) => tabs.value.find((t) => t.id === id) ?? null
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* 将指定标签移动到目标索引。标签对象本身不变,因此其页面栈和缓存关联保持不受影响。
|
|
194
|
+
* @returns 是否实际调整了顺序
|
|
195
|
+
*/
|
|
196
|
+
const moveTab = (id: string, targetIndex: number): boolean => {
|
|
197
|
+
const sourceIndex = tabs.value.findIndex((tab) => tab.id === id)
|
|
198
|
+
if (
|
|
199
|
+
sourceIndex < 0 ||
|
|
200
|
+
targetIndex < 0 ||
|
|
201
|
+
targetIndex >= tabs.value.length ||
|
|
202
|
+
sourceIndex === targetIndex
|
|
203
|
+
) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const reorderedTabs = tabs.value.slice()
|
|
208
|
+
const [tab] = reorderedTabs.splice(sourceIndex, 1)
|
|
209
|
+
if (!tab) return false
|
|
210
|
+
reorderedTabs.splice(targetIndex, 0, tab)
|
|
211
|
+
tabs.value = reorderedTabs
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
/** 从路由解析 tabInfo,若无则创建默认信息,调用方负责把编码信息写入内部 page.query */
|
|
193
216
|
const parseTabInfoFromRoute = (route: RouteLocationNormalizedLoaded): ITabBase => {
|
|
194
217
|
if (typeof route.query.__tab === 'string') {
|
|
@@ -718,6 +741,7 @@ export default () => {
|
|
|
718
741
|
addCache,
|
|
719
742
|
removeCache,
|
|
720
743
|
getTab,
|
|
744
|
+
moveTab,
|
|
721
745
|
addTab,
|
|
722
746
|
removeTab,
|
|
723
747
|
removeAllTabs,
|