@gx-design-vue/pro-layout 0.1.0-alpha.12 → 0.1.0-alpha.13
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/dist/ProLayout.js +6 -1
- package/dist/components/AppPage/index.d.ts +1 -1
- package/dist/components/Breadcrumb/index.js +23 -2
- package/dist/components/Breadcrumb/interface.d.ts +5 -0
- package/dist/components/PageTransition/index.d.ts +2 -2
- package/dist/components/Tabs/InnerTabs.d.ts +8 -0
- package/dist/components/Tabs/InnerTabs.js +117 -0
- package/dist/components/Tabs/hooks/flipAnimate.d.ts +24 -0
- package/dist/components/Tabs/hooks/flipAnimate.js +40 -0
- package/dist/components/Tabs/hooks/useAutoScroll.d.ts +27 -0
- package/dist/components/Tabs/hooks/useAutoScroll.js +122 -0
- package/dist/components/Tabs/hooks/useTabDrag.d.ts +31 -0
- package/dist/components/Tabs/hooks/useTabDrag.js +252 -0
- package/dist/components/Tabs/index.d.ts +7 -1
- package/dist/components/Tabs/index.js +44 -29
- package/dist/components/Tabs/interface.d.ts +47 -6
- package/dist/components/Tabs/style/index.d.ts +34 -0
- package/dist/components/Tabs/style/index.js +140 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/defaultConfig.js +2 -1
- package/dist/hooks/useLayoutBase.d.ts +15 -13
- package/dist/hooks/useLayoutBase.js +4 -0
- package/dist/hooks/useTabs.d.ts +3 -0
- package/dist/hooks/useTabs.js +12 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/interface.d.ts +6 -2
- package/dist/pro-layout.esm.js +1349 -891
- package/dist/pro-layout.js +2 -2
- package/dist/style/tabs.d.ts +6 -0
- package/dist/style/tabs.js +9 -53
- package/dist/theme/augment.d.ts +4 -2
- package/dist/theme/interface/components.d.ts +4 -2
- package/package.json +5 -5
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { flipAnimate } from "./flipAnimate.js";
|
|
2
|
+
import { useAutoScroll } from "./useAutoScroll.js";
|
|
3
|
+
import { nextTick, ref } from "vue";
|
|
4
|
+
import { useEventListener } from "@vueuse/core";
|
|
5
|
+
//#region src/components/Tabs/hooks/useTabDrag.ts
|
|
6
|
+
const IGNORE_SELECTOR = "button, a, input, [data-drag-ignore=\"true\"]";
|
|
7
|
+
/** 移动阈值(px),超过才视为拖拽,避免误触影响点击切换 */
|
|
8
|
+
const DRAG_THRESHOLD = 3;
|
|
9
|
+
const SCROLL_RESTORE_FRAMES = 2;
|
|
10
|
+
const DISPLACEMENT_DURATION = 160;
|
|
11
|
+
const DISPLACEMENT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
|
|
12
|
+
function isIgnoredTarget(target) {
|
|
13
|
+
return target instanceof HTMLElement && Boolean(target.closest(IGNORE_SELECTOR));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 页签拖拽排序 —— 精简复刻 dnd-kit 的 useDraggable + sortable 实时让位。
|
|
17
|
+
*
|
|
18
|
+
* - 移动阈值(3px)区分点击与拖拽:未超阈值视为点击,让原生 click 走切换
|
|
19
|
+
* - 拖拽项 `transform: translateX(delta)` 跟随指针
|
|
20
|
+
* - 同组其他 tab 按虚拟插入位置实时 `translate` 让位(CSS translate 属性)
|
|
21
|
+
* - drop 锚点只在 `item.group === dragGroup` 内查找 → 天然不跨级
|
|
22
|
+
* - 松手 → onReorder 触发数据重排 → nextTick 用 flipAnimate(WAAPI FLIP)平滑归位
|
|
23
|
+
* - 拖拽结束后捕获抑制下一次 click,避免误触发 tab 切换
|
|
24
|
+
* - 边缘自动滚动串联 useAutoScroll
|
|
25
|
+
*/
|
|
26
|
+
function useTabDrag(options) {
|
|
27
|
+
const { items, scrollEl, onReorder, enabled } = options;
|
|
28
|
+
const isDragging = ref(false);
|
|
29
|
+
const draggingKey = ref("");
|
|
30
|
+
let dragGroup;
|
|
31
|
+
let dragStartClientX = 0;
|
|
32
|
+
let dragStartScrollLeft = 0;
|
|
33
|
+
let dragPointerOffsetX = 0;
|
|
34
|
+
let pointerClientX = 0;
|
|
35
|
+
let dragEl = null;
|
|
36
|
+
let groupRects = /* @__PURE__ */ new Map();
|
|
37
|
+
let pendingKey = "";
|
|
38
|
+
let pendingStartX = 0;
|
|
39
|
+
const { scroll: scrollOnDragMove, start: startAutoScroll, stop: stopAutoScroll } = useAutoScroll(scrollEl, () => pointerClientX, { onScroll: updateDragPosition });
|
|
40
|
+
function getGroupItems() {
|
|
41
|
+
return items.value.filter((item) => item.group === dragGroup);
|
|
42
|
+
}
|
|
43
|
+
function queryTabEl(key) {
|
|
44
|
+
return scrollEl.value?.querySelector(`[data-key="${key}"]`) ?? null;
|
|
45
|
+
}
|
|
46
|
+
function recordGroupRects() {
|
|
47
|
+
groupRects = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const item of getGroupItems()) {
|
|
49
|
+
const el = queryTabEl(item.key);
|
|
50
|
+
if (!el) continue;
|
|
51
|
+
const rect = el.getBoundingClientRect();
|
|
52
|
+
groupRects.set(item.key, {
|
|
53
|
+
el,
|
|
54
|
+
left: rect.left,
|
|
55
|
+
width: rect.width,
|
|
56
|
+
translate: 0
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function getCurrentScrollOffset() {
|
|
61
|
+
return (scrollEl.value?.scrollLeft ?? 0) - dragStartScrollLeft;
|
|
62
|
+
}
|
|
63
|
+
function getVisualPointerX() {
|
|
64
|
+
return pointerClientX + getCurrentScrollOffset();
|
|
65
|
+
}
|
|
66
|
+
function getDragCenterX() {
|
|
67
|
+
const dragRect = groupRects.get(draggingKey.value);
|
|
68
|
+
if (!dragRect) return getVisualPointerX();
|
|
69
|
+
return getVisualPointerX() - dragPointerOffsetX + dragRect.width / 2;
|
|
70
|
+
}
|
|
71
|
+
function getItemDisplacement(fromIdx, toIdx, dragWidth) {
|
|
72
|
+
const fromRect = groupRects.get(draggingKey.value);
|
|
73
|
+
const nextKey = fromIdx < toIdx ? getGroupItems()[fromIdx + 1]?.key : getGroupItems()[fromIdx - 1]?.key;
|
|
74
|
+
const nextRect = nextKey ? groupRects.get(nextKey) : void 0;
|
|
75
|
+
return dragWidth + (fromRect && nextRect ? Math.max(0, Math.abs(nextRect.left - fromRect.left) - (fromIdx < toIdx ? fromRect.width : nextRect.width)) : 0);
|
|
76
|
+
}
|
|
77
|
+
/** 计算拖拽项应去的目标项索引(匹配 reorderWithinGroup 的 toKey 语义) */
|
|
78
|
+
function computeTargetIdx(dragCenterX) {
|
|
79
|
+
const groupList = getGroupItems();
|
|
80
|
+
if (groupList.findIndex((item) => item.key === draggingKey.value) < 0) return -1;
|
|
81
|
+
const others = groupList.filter((item) => item.key !== draggingKey.value);
|
|
82
|
+
let targetIdx = others.length;
|
|
83
|
+
for (let i = 0; i < others.length; i++) {
|
|
84
|
+
const rect = groupRects.get(others[i].key);
|
|
85
|
+
if (!rect) continue;
|
|
86
|
+
if (dragCenterX < rect.left + rect.width / 2) {
|
|
87
|
+
targetIdx = i;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return targetIdx;
|
|
92
|
+
}
|
|
93
|
+
/** 同组其他 tab 实时让位(区间内项 translate ±dragWidth) */
|
|
94
|
+
function applyDisplacement(targetIdx) {
|
|
95
|
+
const groupList = getGroupItems();
|
|
96
|
+
const fromIdx = groupList.findIndex((item) => item.key === draggingKey.value);
|
|
97
|
+
if (fromIdx < 0) return;
|
|
98
|
+
const dragWidth = groupRects.get(draggingKey.value)?.width ?? 0;
|
|
99
|
+
for (let i = 0; i < groupList.length; i++) {
|
|
100
|
+
const key = groupList[i].key;
|
|
101
|
+
if (key === draggingKey.value) continue;
|
|
102
|
+
const rect = groupRects.get(key);
|
|
103
|
+
if (!rect) continue;
|
|
104
|
+
let translate = 0;
|
|
105
|
+
if (fromIdx < targetIdx && i > fromIdx && i <= targetIdx) translate = -getItemDisplacement(fromIdx, targetIdx, dragWidth);
|
|
106
|
+
else if (fromIdx > targetIdx && i >= targetIdx && i < fromIdx) translate = getItemDisplacement(fromIdx, targetIdx, dragWidth);
|
|
107
|
+
applyTabTranslate(rect, translate);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function applyTabTranslate(rect, nextTranslate) {
|
|
111
|
+
const prevTranslate = rect.translate;
|
|
112
|
+
if (prevTranslate === nextTranslate) return;
|
|
113
|
+
rect.animation?.cancel();
|
|
114
|
+
rect.translate = nextTranslate;
|
|
115
|
+
rect.el.style.translate = nextTranslate ? `${nextTranslate}px` : "";
|
|
116
|
+
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches || typeof rect.el.animate !== "function") return;
|
|
117
|
+
rect.animation = rect.el.animate({ translate: [`${prevTranslate}px`, `${nextTranslate}px`] }, {
|
|
118
|
+
duration: DISPLACEMENT_DURATION,
|
|
119
|
+
easing: DISPLACEMENT_EASING
|
|
120
|
+
});
|
|
121
|
+
const clearAnimation = () => {
|
|
122
|
+
rect.animation = void 0;
|
|
123
|
+
};
|
|
124
|
+
rect.animation.addEventListener("finish", clearAnimation, { once: true });
|
|
125
|
+
rect.animation.addEventListener("cancel", clearAnimation, { once: true });
|
|
126
|
+
}
|
|
127
|
+
function updateDragPosition() {
|
|
128
|
+
if (!isDragging.value) return;
|
|
129
|
+
if (dragEl) dragEl.style.transform = `translateX(${getVisualPointerX() - dragStartClientX}px)`;
|
|
130
|
+
applyDisplacement(computeTargetIdx(getDragCenterX()));
|
|
131
|
+
}
|
|
132
|
+
function clearInlineStyles(rects = groupRects, activeDragEl = dragEl) {
|
|
133
|
+
for (const rect of rects.values()) {
|
|
134
|
+
rect.animation?.cancel();
|
|
135
|
+
rect.animation = void 0;
|
|
136
|
+
rect.translate = 0;
|
|
137
|
+
const { el } = rect;
|
|
138
|
+
el.style.translate = "";
|
|
139
|
+
}
|
|
140
|
+
if (activeDragEl) activeDragEl.style.transform = "";
|
|
141
|
+
}
|
|
142
|
+
function restoreScrollLeft(scrollLeft, frames = SCROLL_RESTORE_FRAMES) {
|
|
143
|
+
const el = scrollEl.value;
|
|
144
|
+
if (!el) return;
|
|
145
|
+
el.scrollLeft = scrollLeft;
|
|
146
|
+
if (frames <= 0 || typeof requestAnimationFrame === "undefined") return;
|
|
147
|
+
requestAnimationFrame(() => restoreScrollLeft(scrollLeft, frames - 1));
|
|
148
|
+
}
|
|
149
|
+
function beginDrag(clientX) {
|
|
150
|
+
isDragging.value = true;
|
|
151
|
+
dragStartClientX = clientX;
|
|
152
|
+
dragStartScrollLeft = scrollEl.value?.scrollLeft ?? 0;
|
|
153
|
+
pointerClientX = clientX;
|
|
154
|
+
dragEl = queryTabEl(draggingKey.value);
|
|
155
|
+
recordGroupRects();
|
|
156
|
+
const dragRect = groupRects.get(draggingKey.value);
|
|
157
|
+
dragPointerOffsetX = dragRect ? clientX - dragRect.left : 0;
|
|
158
|
+
startAutoScroll();
|
|
159
|
+
}
|
|
160
|
+
/** 捕获阶段抑制拖拽结束后的下一次 click,避免误触发切换 */
|
|
161
|
+
function suppressNextClick() {
|
|
162
|
+
const target = dragEl;
|
|
163
|
+
if (!target) return;
|
|
164
|
+
const suppress = (event) => {
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
target.removeEventListener("click", suppress, true);
|
|
168
|
+
};
|
|
169
|
+
target.addEventListener("click", suppress, {
|
|
170
|
+
capture: true,
|
|
171
|
+
once: true
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function handleMousedown(event, item) {
|
|
175
|
+
if (event.button !== 0 || item.disabled) return;
|
|
176
|
+
if (isIgnoredTarget(event.target)) return;
|
|
177
|
+
if (enabled && !enabled.value) return;
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
pendingKey = item.key;
|
|
180
|
+
pendingStartX = event.clientX;
|
|
181
|
+
draggingKey.value = item.key;
|
|
182
|
+
dragGroup = item.group;
|
|
183
|
+
}
|
|
184
|
+
function handleMousemove(event) {
|
|
185
|
+
if (!isDragging.value) {
|
|
186
|
+
if (!pendingKey) return;
|
|
187
|
+
if (Math.abs(event.clientX - pendingStartX) < DRAG_THRESHOLD) return;
|
|
188
|
+
beginDrag(pendingStartX);
|
|
189
|
+
}
|
|
190
|
+
pointerClientX = event.clientX;
|
|
191
|
+
updateDragPosition();
|
|
192
|
+
scrollOnDragMove();
|
|
193
|
+
}
|
|
194
|
+
function endDrag() {
|
|
195
|
+
const targetIdx = computeTargetIdx(getDragCenterX());
|
|
196
|
+
const groupList = getGroupItems();
|
|
197
|
+
const fromIdx = groupList.findIndex((item) => item.key === draggingKey.value);
|
|
198
|
+
const prevRects = /* @__PURE__ */ new Map();
|
|
199
|
+
for (const [key, { el }] of groupRects) prevRects.set(key, el.getBoundingClientRect());
|
|
200
|
+
stopAutoScroll();
|
|
201
|
+
const dragKey = draggingKey.value;
|
|
202
|
+
const targetItem = groupList[targetIdx];
|
|
203
|
+
const hasReorder = targetIdx >= 0 && targetIdx !== fromIdx && targetItem && targetItem.key !== dragKey;
|
|
204
|
+
const currentGroupRects = groupRects;
|
|
205
|
+
const currentDragEl = dragEl;
|
|
206
|
+
const currentScrollLeft = scrollEl.value?.scrollLeft ?? 0;
|
|
207
|
+
if (hasReorder) {
|
|
208
|
+
onReorder(dragKey, targetItem.key);
|
|
209
|
+
nextTick(() => {
|
|
210
|
+
restoreScrollLeft(currentScrollLeft);
|
|
211
|
+
clearInlineStyles(currentGroupRects, currentDragEl);
|
|
212
|
+
flipAnimate(groupList.map((item) => ({
|
|
213
|
+
key: item.key,
|
|
214
|
+
el: queryTabEl(item.key)
|
|
215
|
+
})).filter((entry) => Boolean(entry.el)), prevRects);
|
|
216
|
+
restoreScrollLeft(currentScrollLeft);
|
|
217
|
+
});
|
|
218
|
+
} else clearInlineStyles();
|
|
219
|
+
suppressNextClick();
|
|
220
|
+
isDragging.value = false;
|
|
221
|
+
draggingKey.value = "";
|
|
222
|
+
dragEl = null;
|
|
223
|
+
groupRects = /* @__PURE__ */ new Map();
|
|
224
|
+
dragPointerOffsetX = 0;
|
|
225
|
+
pendingKey = "";
|
|
226
|
+
dragGroup = void 0;
|
|
227
|
+
}
|
|
228
|
+
function handleMouseup() {
|
|
229
|
+
if (isDragging.value) endDrag();
|
|
230
|
+
else if (pendingKey) {
|
|
231
|
+
pendingKey = "";
|
|
232
|
+
draggingKey.value = "";
|
|
233
|
+
dragGroup = void 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const documentTarget = typeof document === "undefined" ? void 0 : document;
|
|
237
|
+
useEventListener(documentTarget, "mousemove", handleMousemove);
|
|
238
|
+
useEventListener(documentTarget, "mouseup", handleMouseup);
|
|
239
|
+
function getItemProps(key) {
|
|
240
|
+
const item = items.value.find((entry) => entry.key === key);
|
|
241
|
+
return { onMousedown: (event) => {
|
|
242
|
+
if (item) handleMousedown(event, item);
|
|
243
|
+
} };
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
isDragging,
|
|
247
|
+
draggingKey,
|
|
248
|
+
getItemProps
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
export { useTabDrag };
|
|
@@ -3,6 +3,12 @@ import * as _$vue from "vue";
|
|
|
3
3
|
import { SlotsType } from "vue";
|
|
4
4
|
|
|
5
5
|
//#region src/components/Tabs/index.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* 标签栏默认高度(px,首帧兜底值,基于默认主题实测取整)。
|
|
8
|
+
* 真实高度由 LayoutTabs 挂载后用 useElementSize 测量并写回 context,
|
|
9
|
+
* 此常量仅用于测量回填前的占位,避免 fixed 占位首帧塌陷。
|
|
10
|
+
*/
|
|
11
|
+
declare const DEFAULT_TABS_HEIGHT = 41;
|
|
6
12
|
declare const LayoutTabs: _$vue.DefineSetupFnComponent<LayoutTabsProps, LayoutTabsEmits, SlotsType<LayoutTabsSlots>, LayoutTabsProps, _$vue.PublicProps>;
|
|
7
13
|
//#endregion
|
|
8
|
-
export { LayoutTabs as default };
|
|
14
|
+
export { DEFAULT_TABS_HEIGHT, LayoutTabs as default };
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { useLayoutBase } from "../../hooks/useLayoutBase.js";
|
|
2
2
|
import { CONTEXT_MENU_ITEMS, getDisabledState, getMenuItemLabel } from "./contextMenu.js";
|
|
3
|
-
import
|
|
3
|
+
import InnerTabs from "./InnerTabs.js";
|
|
4
|
+
import { Fragment, computed, createVNode, defineComponent, ref, watch } from "vue";
|
|
4
5
|
import { unit } from "@gx-design-vue/pro-provider";
|
|
5
6
|
import { classNames } from "@gx-design-vue/pro-utils";
|
|
6
|
-
import {
|
|
7
|
+
import { useElementSize } from "@vueuse/core";
|
|
8
|
+
import { Dropdown } from "antdv-next";
|
|
7
9
|
import { GIcon } from "@gx-design-vue/icon";
|
|
8
10
|
//#region src/components/Tabs/index.tsx
|
|
11
|
+
/**
|
|
12
|
+
* 标签栏默认高度(px,首帧兜底值,基于默认主题实测取整)。
|
|
13
|
+
* 真实高度由 LayoutTabs 挂载后用 useElementSize 测量并写回 context,
|
|
14
|
+
* 此常量仅用于测量回填前的占位,避免 fixed 占位首帧塌陷。
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_TABS_HEIGHT = 41;
|
|
9
17
|
const LayoutTabs = /* @__PURE__ */ defineComponent((props, { slots }) => {
|
|
10
|
-
const { prefixCls, proClasses, tabsState, contentFullscreen, header, isMobile } = useLayoutBase();
|
|
18
|
+
const { prefixCls, proClasses, tabsState, tabsHeight, setTabsHeight, contentFullscreen, header, isMobile } = useLayoutBase();
|
|
11
19
|
const contextMenuActiveKey = ref("");
|
|
20
|
+
const tabsContentRef = ref();
|
|
21
|
+
const { height: measuredTabsHeight } = useElementSize(tabsContentRef, void 0, { box: "border-box" });
|
|
22
|
+
watch(measuredTabsHeight, (height) => {
|
|
23
|
+
if (height > 0) setTabsHeight?.(height);
|
|
24
|
+
}, { immediate: true });
|
|
12
25
|
const dataSource = computed(() => tabsState?.dataSource?.value ?? []);
|
|
13
26
|
const activeKey = computed(() => tabsState?.activeKey?.value ?? "");
|
|
14
27
|
const checkIsFixed = computed(() => tabsState?.checkIsFixed ?? (() => false));
|
|
15
|
-
const tabsType = computed(() => {
|
|
16
|
-
if (props.config?.type === "card") return "card";
|
|
17
|
-
return "line";
|
|
18
|
-
});
|
|
19
28
|
const isFixed = computed(() => !!props.config?.fixed && !isMobile.value);
|
|
20
29
|
const tabsClassNames = computed(() => classNames(`${prefixCls.value}-tabs`, proClasses.value.tabs, {
|
|
21
30
|
[`${prefixCls.value}-tabs-${props.config?.type}`]: props.config?.type,
|
|
@@ -55,7 +64,14 @@ const LayoutTabs = /* @__PURE__ */ defineComponent((props, { slots }) => {
|
|
|
55
64
|
meta: {}
|
|
56
65
|
};
|
|
57
66
|
}
|
|
58
|
-
|
|
67
|
+
const innerItems = computed(() => dataSource.value.map((route) => ({
|
|
68
|
+
key: route.name,
|
|
69
|
+
label: route.meta?.title,
|
|
70
|
+
closable: !checkIsFixed.value(route) && dataSource.value.length > 1,
|
|
71
|
+
group: checkIsFixed.value(route) ? "fixed" : "normal"
|
|
72
|
+
})));
|
|
73
|
+
function renderTabSlot({ item }) {
|
|
74
|
+
const route = getRouteByName(item.key);
|
|
59
75
|
const routeName = route.name;
|
|
60
76
|
const isFixed = checkIsFixed.value(route);
|
|
61
77
|
const customRender = slots.tabBarItem?.({ route }) ?? props.tabBarItem?.({ route });
|
|
@@ -74,11 +90,13 @@ const LayoutTabs = /* @__PURE__ */ defineComponent((props, { slots }) => {
|
|
|
74
90
|
createVNode("span", { "class": `${prefixCls.value}-tabs-title-text` }, [route.meta?.title]),
|
|
75
91
|
isFixed && createVNode(GIcon, {
|
|
76
92
|
"type": "Pin",
|
|
77
|
-
"class": `${prefixCls.value}-tabs-title-pin
|
|
93
|
+
"class": `${prefixCls.value}-tabs-title-pin`,
|
|
94
|
+
"data-drag-ignore": "true"
|
|
78
95
|
}, null),
|
|
79
96
|
dataSource.value.length > 1 && !isFixed && createVNode(GIcon, {
|
|
80
97
|
"type": "CloseOutlined",
|
|
81
98
|
"class": `${prefixCls.value}-tabs-title-close`,
|
|
99
|
+
"data-drag-ignore": "true",
|
|
82
100
|
"onClick": (event) => {
|
|
83
101
|
event.stopPropagation();
|
|
84
102
|
event.preventDefault();
|
|
@@ -87,11 +105,6 @@ const LayoutTabs = /* @__PURE__ */ defineComponent((props, { slots }) => {
|
|
|
87
105
|
}, null)
|
|
88
106
|
])] });
|
|
89
107
|
}
|
|
90
|
-
const tabItems = computed(() => dataSource.value.map((route) => ({
|
|
91
|
-
key: route.name,
|
|
92
|
-
label: renderTabTitle(route),
|
|
93
|
-
closable: !checkIsFixed.value(route)
|
|
94
|
-
})));
|
|
95
108
|
function renderRightExtra() {
|
|
96
109
|
if (dataSource.value.length === 0) return null;
|
|
97
110
|
return createVNode("div", { "class": classNames(`${prefixCls.value}-tabs-extra`) }, [
|
|
@@ -122,29 +135,31 @@ const LayoutTabs = /* @__PURE__ */ defineComponent((props, { slots }) => {
|
|
|
122
135
|
}
|
|
123
136
|
return () => {
|
|
124
137
|
if (!tabsState || dataSource.value.length === 0) return null;
|
|
125
|
-
return createVNode(Fragment, null, [createVNode("div", { "class": tabsClassNames.value }, [isFixed.value && createVNode("div", {
|
|
138
|
+
return createVNode(Fragment, null, [createVNode("div", { "class": tabsClassNames.value }, [isFixed.value && createVNode("div", {
|
|
139
|
+
"class": `${prefixCls.value}-tabs-placeholder`,
|
|
140
|
+
"style": { height: unit(tabsHeight?.value ?? 41) }
|
|
141
|
+
}, null), createVNode("div", {
|
|
142
|
+
"ref": tabsContentRef,
|
|
126
143
|
"class": classNames(`${prefixCls.value}-tabs-content`),
|
|
127
144
|
"style": tabsContentStyle.value
|
|
128
|
-
}, [createVNode(
|
|
129
|
-
"
|
|
130
|
-
"hideAdd": true,
|
|
145
|
+
}, [createVNode(InnerTabs, {
|
|
146
|
+
"items": innerItems.value,
|
|
131
147
|
"activeKey": activeKey.value,
|
|
132
|
-
"
|
|
148
|
+
"draggable": props.config?.draggable !== false,
|
|
133
149
|
"onChange": (key) => {
|
|
134
150
|
tabsState?.handleTabClick?.(key);
|
|
151
|
+
},
|
|
152
|
+
"onReorder": (fromKey, toKey) => {
|
|
153
|
+
const fromItem = innerItems.value.find((item) => item.key === fromKey);
|
|
154
|
+
if (fromItem?.group) tabsState?.reorderWithinGroup?.(fromKey, toKey, fromItem.group);
|
|
135
155
|
}
|
|
136
|
-
}, {
|
|
156
|
+
}, {
|
|
157
|
+
tab: renderTabSlot,
|
|
158
|
+
extra: renderRightExtra
|
|
159
|
+
})])])]);
|
|
137
160
|
};
|
|
138
161
|
}, {
|
|
139
162
|
props: {
|
|
140
|
-
items: {
|
|
141
|
-
type: Array,
|
|
142
|
-
required: false
|
|
143
|
-
},
|
|
144
|
-
tabs: {
|
|
145
|
-
type: Array,
|
|
146
|
-
required: false
|
|
147
|
-
},
|
|
148
163
|
config: {
|
|
149
164
|
type: Object,
|
|
150
165
|
required: false
|
|
@@ -163,4 +178,4 @@ const LayoutTabs = /* @__PURE__ */ defineComponent((props, { slots }) => {
|
|
|
163
178
|
inheritAttrs: false
|
|
164
179
|
});
|
|
165
180
|
//#endregion
|
|
166
|
-
export { LayoutTabs as default };
|
|
181
|
+
export { DEFAULT_TABS_HEIGHT, LayoutTabs as default };
|
|
@@ -1,8 +1,53 @@
|
|
|
1
1
|
import { LayoutMenuRoute, LayoutTabsConfig } from "../../interface.js";
|
|
2
2
|
import { CustomRender } from "@gx-design-vue/pro-utils";
|
|
3
|
-
import { Tab } from "antdv-next/dist/tabs";
|
|
4
3
|
|
|
5
4
|
//#region src/components/Tabs/interface.d.ts
|
|
5
|
+
/** 页签分组(拖拽排序约束:组内可拖,不跨级) */
|
|
6
|
+
type TabGroup = 'fixed' | 'normal';
|
|
7
|
+
/** 单个页签数据 */
|
|
8
|
+
interface InnerTabItem {
|
|
9
|
+
/** 唯一标识 */
|
|
10
|
+
key: string;
|
|
11
|
+
/** 标题(string / VNode),未提供 #tab slot 时默认渲染 */
|
|
12
|
+
label?: any;
|
|
13
|
+
/** 是否显示关闭按钮,默认 true */
|
|
14
|
+
closable?: boolean;
|
|
15
|
+
/** 是否禁用 */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** 所属分组(拖拽排序时约束组内,不跨级) */
|
|
18
|
+
group?: TabGroup;
|
|
19
|
+
}
|
|
20
|
+
interface InnerTabsEmits {
|
|
21
|
+
'update:activeKey': (key: string) => any;
|
|
22
|
+
'change': (key: string) => any;
|
|
23
|
+
'close': (key: string) => any;
|
|
24
|
+
/** 拖拽排序:从 fromKey 移到 toKey 位置(同组内) */
|
|
25
|
+
'reorder': (fromKey: string, toKey: string) => any;
|
|
26
|
+
}
|
|
27
|
+
interface InnerTabsEmitsProps {
|
|
28
|
+
'onUpdate:activeKey'?: InnerTabsEmits['update:activeKey'];
|
|
29
|
+
onChange?: InnerTabsEmits['change'];
|
|
30
|
+
onClose?: InnerTabsEmits['close'];
|
|
31
|
+
onReorder?: InnerTabsEmits['reorder'];
|
|
32
|
+
}
|
|
33
|
+
interface InnerTabsProps extends InnerTabsEmitsProps {
|
|
34
|
+
prefixCls?: string;
|
|
35
|
+
/** 页签列表 */
|
|
36
|
+
items?: InnerTabItem[];
|
|
37
|
+
/** 当前激活 key(支持 v-model:activeKey) */
|
|
38
|
+
activeKey?: string;
|
|
39
|
+
/** 是否开启拖拽排序,默认 true */
|
|
40
|
+
draggable?: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface InnerTabsSlots {
|
|
43
|
+
/** 自定义单个页签内容(如上层挂右键菜单 / 图标) */
|
|
44
|
+
tab?: (props: {
|
|
45
|
+
item: InnerTabItem;
|
|
46
|
+
active: boolean;
|
|
47
|
+
}) => any;
|
|
48
|
+
/** 导航栏右侧额外区域 */
|
|
49
|
+
extra?: () => any;
|
|
50
|
+
}
|
|
6
51
|
interface LayoutTabsEmits {
|
|
7
52
|
'tabsChange': (tabs: LayoutMenuRoute[]) => void;
|
|
8
53
|
'reloadPage': () => void;
|
|
@@ -14,10 +59,6 @@ interface LayoutTabsEmitsProps {
|
|
|
14
59
|
onContentFullscreenChange?: LayoutTabsEmits['contentFullscreenChange'];
|
|
15
60
|
}
|
|
16
61
|
interface LayoutTabsProps extends LayoutTabsEmitsProps {
|
|
17
|
-
/** 已转换好的 antdv-next 标签数据 */
|
|
18
|
-
items?: Tab[];
|
|
19
|
-
/** 原始标签路由 */
|
|
20
|
-
tabs?: LayoutMenuRoute[];
|
|
21
62
|
config?: LayoutTabsConfig;
|
|
22
63
|
/** 标签项自定义(等价 #tabBarItem slot) */
|
|
23
64
|
tabBarItem?: (slotProps: {
|
|
@@ -30,4 +71,4 @@ interface LayoutTabsSlots {
|
|
|
30
71
|
}) => any;
|
|
31
72
|
}
|
|
32
73
|
//#endregion
|
|
33
|
-
export { LayoutTabsEmits, LayoutTabsEmitsProps, LayoutTabsProps, LayoutTabsSlots };
|
|
74
|
+
export { InnerTabItem, InnerTabsEmits, InnerTabsEmitsProps, InnerTabsProps, InnerTabsSlots, LayoutTabsEmits, LayoutTabsEmitsProps, LayoutTabsProps, LayoutTabsSlots, TabGroup };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as _$vue from "vue";
|
|
2
|
+
import { GetDefaultToken } from "antdv-next/dist/theme/internal";
|
|
3
|
+
|
|
4
|
+
//#region src/components/Tabs/style/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* InnerTabs 组件级 Token —— 仅承载尺寸相关字段;
|
|
7
|
+
* 卡片配色复用全局扁平 token `tabsColor*`(见 pro-provider/theme),保持与 layout 主题一致。
|
|
8
|
+
*/
|
|
9
|
+
interface ComponentToken {
|
|
10
|
+
/** 单个页签高度 */
|
|
11
|
+
itemHeight: number | string;
|
|
12
|
+
/** 页签横向内边距 */
|
|
13
|
+
itemPaddingInline: number | string;
|
|
14
|
+
/** 页签纵向内边距 */
|
|
15
|
+
itemPaddingBlock: number | string;
|
|
16
|
+
/** 页签间距 */
|
|
17
|
+
itemGap: number | string;
|
|
18
|
+
/** 页签圆角 */
|
|
19
|
+
itemBorderRadius: number | string;
|
|
20
|
+
/** 标题字号 */
|
|
21
|
+
titleFontSize: number;
|
|
22
|
+
/** 标题最大宽度(超出省略) */
|
|
23
|
+
titleMaxWidth: number | string;
|
|
24
|
+
/** 关闭按钮尺寸 */
|
|
25
|
+
closeIconSize: number | string;
|
|
26
|
+
/** 导航栏纵向内边距 */
|
|
27
|
+
navPaddingBlock: number | string;
|
|
28
|
+
/** 导航栏横向内边距 */
|
|
29
|
+
navPaddingInline: number | string;
|
|
30
|
+
}
|
|
31
|
+
declare const prepareComponentToken: GetDefaultToken<'InnerTabs'>;
|
|
32
|
+
declare const _default: (prefixCls: _$vue.Ref<string>, rootCls?: _$vue.Ref<string | undefined>) => readonly [_$vue.Ref<string, string>, _$vue.ComputedRef<string | undefined>];
|
|
33
|
+
//#endregion
|
|
34
|
+
export { ComponentToken, _default as default, prepareComponentToken };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { proGenStyleHooks } from "@gx-design-vue/pro-provider";
|
|
2
|
+
import { unit as unit$1 } from "@antdv-next/cssinjs";
|
|
3
|
+
//#region src/components/Tabs/style/index.ts
|
|
4
|
+
const prepareComponentToken = (token) => ({
|
|
5
|
+
itemHeight: token.controlHeightSM,
|
|
6
|
+
itemPaddingInline: token.paddingXS,
|
|
7
|
+
itemPaddingBlock: 2,
|
|
8
|
+
itemGap: token.marginXS,
|
|
9
|
+
itemBorderRadius: token.borderRadiusSM,
|
|
10
|
+
titleFontSize: token.fontSizeSM,
|
|
11
|
+
titleMaxWidth: 120,
|
|
12
|
+
closeIconSize: token.fontSizeSM,
|
|
13
|
+
navPaddingBlock: token.paddingXXS,
|
|
14
|
+
navPaddingInline: token.paddingSM
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* InnerTabs 样式:横向卡片式页签条(default / active 双态),无底部 ink-bar。
|
|
18
|
+
* 颜色取自扁平 token `tabsColor*`;mask-image 溢出淡出由组件内联 style 动态控制。
|
|
19
|
+
*/
|
|
20
|
+
const genInnerTabsStyle = (token) => {
|
|
21
|
+
const { componentCls } = token;
|
|
22
|
+
return { [componentCls]: {
|
|
23
|
+
display: "flex",
|
|
24
|
+
minWidth: 0,
|
|
25
|
+
[`${componentCls}-nav`]: {
|
|
26
|
+
position: "relative",
|
|
27
|
+
display: "flex",
|
|
28
|
+
flex: "1 1 auto",
|
|
29
|
+
minWidth: 0,
|
|
30
|
+
alignItems: "center",
|
|
31
|
+
paddingBlock: token.navPaddingBlock
|
|
32
|
+
},
|
|
33
|
+
[`${componentCls}-nav-wrap`]: {
|
|
34
|
+
position: "relative",
|
|
35
|
+
flex: "1 1 auto",
|
|
36
|
+
minWidth: 0,
|
|
37
|
+
marginBlock: token.calc(token.marginXXS).mul(-1).equal(),
|
|
38
|
+
paddingBlock: token.marginXXS,
|
|
39
|
+
overflowX: "auto",
|
|
40
|
+
overflowY: "hidden",
|
|
41
|
+
overflowAnchor: "none",
|
|
42
|
+
whiteSpace: "nowrap",
|
|
43
|
+
scrollbarWidth: "none",
|
|
44
|
+
msOverflowStyle: "none",
|
|
45
|
+
"&::-webkit-scrollbar": { display: "none" }
|
|
46
|
+
},
|
|
47
|
+
[`${componentCls}-nav-list`]: {
|
|
48
|
+
display: "inline-flex",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
gap: token.itemGap,
|
|
51
|
+
overflowAnchor: "none"
|
|
52
|
+
},
|
|
53
|
+
[`${componentCls}-tab`]: {
|
|
54
|
+
display: "inline-flex",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
flex: "none",
|
|
57
|
+
boxSizing: "border-box",
|
|
58
|
+
height: token.itemHeight,
|
|
59
|
+
paddingInline: token.itemPaddingInline,
|
|
60
|
+
paddingBlock: token.itemPaddingBlock,
|
|
61
|
+
fontSize: token.titleFontSize,
|
|
62
|
+
lineHeight: 1,
|
|
63
|
+
color: token.tabsColorText,
|
|
64
|
+
backgroundColor: token.tabsColorBgContainer,
|
|
65
|
+
border: `${unit$1(token.lineWidth)} ${token.lineType} ${token.tabsColorBorder}`,
|
|
66
|
+
borderRadius: token.itemBorderRadius,
|
|
67
|
+
cursor: "pointer",
|
|
68
|
+
userSelect: "none",
|
|
69
|
+
transition: [
|
|
70
|
+
`color ${token.motionDurationMid} ${token.motionEaseInOut}`,
|
|
71
|
+
`background-color ${token.motionDurationMid} ${token.motionEaseInOut}`,
|
|
72
|
+
`border-color ${token.motionDurationMid} ${token.motionEaseInOut}`,
|
|
73
|
+
`box-shadow ${token.motionDurationMid} ${token.motionEaseInOut}`
|
|
74
|
+
].join(", "),
|
|
75
|
+
"&:hover": {
|
|
76
|
+
color: token.tabsColorTextHover,
|
|
77
|
+
borderColor: token.tabsColorBorderHover,
|
|
78
|
+
backgroundColor: token.tabsColorBgContainerHover
|
|
79
|
+
},
|
|
80
|
+
[`&${componentCls}-tab-active`]: {
|
|
81
|
+
color: token.tabsColorTextActive,
|
|
82
|
+
borderColor: token.tabsColorBorderActive,
|
|
83
|
+
backgroundColor: token.tabsColorBgContainerActive,
|
|
84
|
+
"&:hover": {
|
|
85
|
+
color: token.tabsColorTextActive,
|
|
86
|
+
borderColor: token.tabsColorBorderActive,
|
|
87
|
+
backgroundColor: token.tabsColorBgContainerActive
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[`&${componentCls}-tab-disabled`]: {
|
|
91
|
+
color: token.colorTextDisabled,
|
|
92
|
+
cursor: "not-allowed"
|
|
93
|
+
},
|
|
94
|
+
[`&${componentCls}-tab-dragging`]: {
|
|
95
|
+
zIndex: 1,
|
|
96
|
+
cursor: "grabbing",
|
|
97
|
+
color: token.tabsColorText,
|
|
98
|
+
backgroundColor: token.colorBgElevated,
|
|
99
|
+
borderColor: token.tabsColorBorderHover,
|
|
100
|
+
boxShadow: token.boxShadowSecondary,
|
|
101
|
+
opacity: 1,
|
|
102
|
+
transition: "none"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[`${componentCls}-tab-btn`]: {
|
|
106
|
+
display: "inline-block",
|
|
107
|
+
maxWidth: token.titleMaxWidth,
|
|
108
|
+
overflow: "hidden",
|
|
109
|
+
whiteSpace: "nowrap",
|
|
110
|
+
textOverflow: "ellipsis",
|
|
111
|
+
verticalAlign: "middle",
|
|
112
|
+
transition: `color ${token.motionDurationMid}`
|
|
113
|
+
},
|
|
114
|
+
[`${componentCls}-tab-remove`]: {
|
|
115
|
+
display: "inline-flex",
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
justifyContent: "center",
|
|
118
|
+
marginInlineStart: token.marginXXS,
|
|
119
|
+
fontSize: token.closeIconSize,
|
|
120
|
+
lineHeight: 1,
|
|
121
|
+
color: token.tabsColorIcon,
|
|
122
|
+
cursor: "pointer",
|
|
123
|
+
transition: `all ${token.motionDurationMid}`,
|
|
124
|
+
"&:hover": { color: token.tabsColorIconHover }
|
|
125
|
+
},
|
|
126
|
+
[`${componentCls}-tab-active ${componentCls}-tab-remove`]: {
|
|
127
|
+
color: token.tabsColorIconActive,
|
|
128
|
+
"&:hover": { color: token.tabsColorIconActive }
|
|
129
|
+
},
|
|
130
|
+
[`${componentCls}-extra`]: {
|
|
131
|
+
display: "inline-flex",
|
|
132
|
+
flex: "none",
|
|
133
|
+
alignItems: "center",
|
|
134
|
+
marginInlineStart: token.marginSM
|
|
135
|
+
}
|
|
136
|
+
} };
|
|
137
|
+
};
|
|
138
|
+
var style_default = proGenStyleHooks("InnerTabs", genInnerTabsStyle, prepareComponentToken);
|
|
139
|
+
//#endregion
|
|
140
|
+
export { style_default as default, prepareComponentToken };
|
package/dist/context/index.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ interface LayoutContextProps {
|
|
|
34
34
|
cssVarCls: ComputedRef<string>;
|
|
35
35
|
rootCls: ComputedRef<string>;
|
|
36
36
|
hasFooterToolbar: Ref<boolean>;
|
|
37
|
+
tabsHeight: Ref<number>;
|
|
38
|
+
setTabsHeight: (value: number) => void;
|
|
37
39
|
/**
|
|
38
40
|
* PageContainer 级别的面包屑 slot 覆盖。
|
|
39
41
|
* 当 position='header' 时,PageContainer 的 breadcrumb slot 通过此 ref 传到 Header。
|