✨(tab): 添加滚动
This commit is contained in:
parent
d553e38ab4
commit
833a40058e
@ -3,7 +3,7 @@
|
|||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab[overflow] > .layui-tab-title {
|
.layui-tab[overflow] > .layui-tab-head > .layui-tab-title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
.layui-tab-head {
|
.layui-tab-head {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-card .layui-tab-head {
|
.layui-tab-card .layui-tab-head {
|
||||||
@ -52,6 +53,7 @@
|
|||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-title li a {
|
.layui-tab-title li a {
|
||||||
@ -65,6 +67,7 @@
|
|||||||
.layui-tab-title.is-top,
|
.layui-tab-title.is-top,
|
||||||
.layui-tab-title.is-bottom {
|
.layui-tab-title.is-bottom {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-title.is-right,
|
.layui-tab-title.is-right,
|
||||||
@ -84,6 +87,13 @@
|
|||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layui-tab-title.is-top li,
|
||||||
|
.layui-tab-title.is-bottom li {
|
||||||
|
border-bottom: 1px solid #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.layui-tab-title.is-right {
|
.layui-tab-title.is-right {
|
||||||
border-left: 1px solid var(--global-neutral-color-3);
|
border-left: 1px solid var(--global-neutral-color-3);
|
||||||
}
|
}
|
||||||
@ -94,7 +104,7 @@
|
|||||||
|
|
||||||
.layui-tab-title .layui-this {
|
.layui-tab-title .layui-this {
|
||||||
color: #000;
|
color: #000;
|
||||||
background-color: #fff
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-title .layui-this:after {
|
.layui-tab-title .layui-this:after {
|
||||||
@ -167,6 +177,7 @@
|
|||||||
|
|
||||||
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-top {
|
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-top {
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-right,
|
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-right,
|
||||||
@ -182,9 +193,11 @@
|
|||||||
.layui-tab-card>.layui-tab-head>.layui-tab-title .layui-this:after{
|
.layui-tab-card>.layui-tab-head>.layui-tab-title .layui-this:after{
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-bottom{
|
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-bottom{
|
||||||
border-top: 1px solid var(--global-neutral-color-3);
|
border-top: 1px solid var(--global-neutral-color-3);
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-left li,
|
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-left li,
|
||||||
@ -213,8 +226,7 @@
|
|||||||
border-left-color: #fff;
|
border-left-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-tab-card>.layui-tab-head>.layui-tab-title .layui-tab-bar {
|
.layui-tab-card>.layui-tab-head .layui-tab-bar {
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@ -246,6 +258,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layui-tab-bar.prev{
|
||||||
|
left: 0;
|
||||||
|
right:auto;
|
||||||
|
border-right: 1px solid var(--global-neutral-color-3) !important;
|
||||||
|
border-left: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.layui-tab-bar .layui-icon {
|
.layui-tab-bar .layui-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -319,9 +338,8 @@
|
|||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 1.5px;
|
height: 1.5px;
|
||||||
background-color: #409eff;
|
background-color: var(--global-checked-color);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transition: transform .3s;
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -6,6 +6,7 @@ export default {
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
import { LayIcon } from "@layui/icons-vue";
|
||||||
import tabItem from "../tabItem/index.vue";
|
import tabItem from "../tabItem/index.vue";
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@ -16,12 +17,12 @@ import {
|
|||||||
Ref,
|
Ref,
|
||||||
ref,
|
ref,
|
||||||
watch,
|
watch,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
onMounted,
|
onMounted,
|
||||||
nextTick,
|
nextTick,
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { useResizeObserver } from '@vueuse/core'
|
import { useResizeObserver } from "@vueuse/core";
|
||||||
|
|
||||||
export type tabPositionType = "top" | "bottom" | "left" | "right";
|
export type tabPositionType = "top" | "bottom" | "left" | "right";
|
||||||
|
|
||||||
@ -86,34 +87,153 @@ const close = function (index: number, id: any) {
|
|||||||
emit("close", id);
|
emit("close", id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeBarRef = shallowRef<HTMLElement>();
|
const activeBarRef = shallowRef<HTMLElement | undefined>(undefined);
|
||||||
const activeEl = shallowRef<HTMLElement | undefined>();
|
const activeEl = shallowRef<HTMLElement | undefined>(undefined);
|
||||||
const tabBarStyle = ref<CSSProperties>()
|
const tabBarStyle = ref<CSSProperties>();
|
||||||
const getBarStyle = () => {
|
const getBarStyle = () => {
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let tabSize = 0;
|
let tabSize = 0;
|
||||||
const sizeName = props.tabPosition === "top" || props.tabPosition === "bottom" ? "width" : "height";
|
const sizeName =
|
||||||
|
props.tabPosition === "top" || props.tabPosition === "bottom"
|
||||||
|
? "width"
|
||||||
|
: "height";
|
||||||
const axis = sizeName === "width" ? "X" : "Y";
|
const axis = sizeName === "width" ? "X" : "Y";
|
||||||
const position = axis === 'X' ? 'left' : 'top'
|
const position = axis === "X" ? "left" : "top";
|
||||||
const el = activeEl.value;
|
const el = activeEl.value;
|
||||||
if(!el || !el.parentElement) return;
|
const activeElParentElement = navRef.value;
|
||||||
const rect = el.getBoundingClientRect();
|
if (!el || !activeElParentElement) return;
|
||||||
const parentRect = el.parentElement?.getBoundingClientRect();
|
const rect = el?.getBoundingClientRect();
|
||||||
|
const parentRect = activeElParentElement?.getBoundingClientRect();
|
||||||
offset = rect[position] - parentRect[position];
|
offset = rect[position] - parentRect[position];
|
||||||
tabSize = el.getBoundingClientRect()[sizeName];
|
tabSize = el.getBoundingClientRect()[sizeName];
|
||||||
return {
|
return {
|
||||||
[sizeName]: `${tabSize}px`,
|
[sizeName]: `${tabSize}px`,
|
||||||
transform: `translate${axis}(${offset}px)`,
|
transform: `translate${axis}(${offset}px)`,
|
||||||
}
|
// transition: `transform .3s`, // activeBar 动画
|
||||||
}
|
};
|
||||||
const update = () => {
|
};
|
||||||
const parentEL = activeBarRef.value?.parentNode;
|
|
||||||
activeEl.value = parentEL?.querySelector(" .layui-this") as HTMLElement;
|
|
||||||
tabBarStyle.value = getBarStyle();
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerSize = "";
|
const navRef = shallowRef<HTMLElement | undefined>(undefined);
|
||||||
const navSize = "";
|
const scrollable = ref(false);
|
||||||
|
const navOffset = ref<number>(0);
|
||||||
|
const navStyle = computed<CSSProperties>(() => {
|
||||||
|
const axis =
|
||||||
|
props.tabPosition === "top" || props.tabPosition === "bottom" ? "X" : "Y";
|
||||||
|
const position = axis === "X" ? "left" : "top";
|
||||||
|
const scrollPrevSize = scrollPrevRef.value?.[`offset${sizeName.value}`] ?? 0;
|
||||||
|
return {
|
||||||
|
transform: `translate${axis}(-${navOffset.value}px)`,
|
||||||
|
[position]: scrollable.value ? `${scrollPrevSize}px` : 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const sizeName = computed(() => {
|
||||||
|
return props.tabPosition === "top" || props.tabPosition === "bottom"
|
||||||
|
? "Width"
|
||||||
|
: "Height";
|
||||||
|
});
|
||||||
|
|
||||||
|
const getNavSize = function () {
|
||||||
|
let size = 0;
|
||||||
|
const nodeList = navRef.value?.querySelectorAll("li");
|
||||||
|
nodeList?.forEach((item) => {
|
||||||
|
size += item[`offset${sizeName.value}`];
|
||||||
|
});
|
||||||
|
return size;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollPrev = function () {
|
||||||
|
if (!navRef.value) return;
|
||||||
|
const containerSize = navRef.value[`offset${sizeName.value}`];
|
||||||
|
const currentOffset = navOffset.value;
|
||||||
|
if (!currentOffset) return;
|
||||||
|
let newOffset =
|
||||||
|
currentOffset > containerSize ? currentOffset - containerSize : 0;
|
||||||
|
navOffset.value = newOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollNextRef = shallowRef<HTMLElement | undefined>(undefined);
|
||||||
|
const scrollPrevRef = shallowRef<HTMLElement | undefined>(undefined);
|
||||||
|
const scrollNext = function () {
|
||||||
|
if (!navRef.value) return;
|
||||||
|
const navSize = getNavSize();
|
||||||
|
const containerSize = navRef.value[`offset${sizeName.value}`];
|
||||||
|
const currentOffset = navOffset.value;
|
||||||
|
const scrollNextSize = scrollNextRef.value?.[`offset${sizeName.value}`] ?? 0;
|
||||||
|
const scrollPrevSize = scrollPrevRef.value?.[`offset${sizeName.value}`] ?? 0;
|
||||||
|
if (navSize - currentOffset <= containerSize) return;
|
||||||
|
let newOffset =
|
||||||
|
navSize - currentOffset > containerSize * 2
|
||||||
|
? currentOffset + containerSize
|
||||||
|
: navSize - containerSize + scrollNextSize + scrollPrevSize;
|
||||||
|
navOffset.value = newOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
const headRef = shallowRef<HTMLDivElement | undefined>(undefined);
|
||||||
|
const scrollToActiveTab = function () {
|
||||||
|
if (!scrollable.value) return;
|
||||||
|
const activeTab = activeEl.value;
|
||||||
|
const container = headRef.value;
|
||||||
|
if (!activeTab || !container) return;
|
||||||
|
const activeTabRect = activeTab?.getBoundingClientRect();
|
||||||
|
const containerRect = container?.getBoundingClientRect();
|
||||||
|
const isHorizontal = ["top", "bottom"].includes(props.tabPosition);
|
||||||
|
const currentOffset = navOffset.value;
|
||||||
|
let newOffset = currentOffset;
|
||||||
|
const navSize = getNavSize();
|
||||||
|
const scrollNextSize = scrollNextRef.value?.[`offset${sizeName.value}`] ?? 0;
|
||||||
|
const scrollPrevSize = scrollPrevRef.value?.[`offset${sizeName.value}`] ?? 0;
|
||||||
|
const maxOffset = isHorizontal
|
||||||
|
? navSize - containerRect.width + scrollNextSize + scrollPrevSize
|
||||||
|
: navSize - containerRect.height + scrollNextSize + scrollPrevSize;
|
||||||
|
if (isHorizontal) {
|
||||||
|
if (activeTabRect.left < containerRect.left) {
|
||||||
|
newOffset = currentOffset - (containerRect.left - activeTabRect.left);
|
||||||
|
newOffset -= scrollPrevSize;
|
||||||
|
}
|
||||||
|
if (activeTabRect.right > containerRect.right) {
|
||||||
|
newOffset = currentOffset + activeTabRect.right - containerRect.right;
|
||||||
|
newOffset += scrollNextSize;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (activeTabRect.top < containerRect.top) {
|
||||||
|
newOffset = currentOffset - (containerRect.top - activeTabRect.top);
|
||||||
|
}
|
||||||
|
if (activeTabRect.bottom > containerRect.bottom) {
|
||||||
|
newOffset = currentOffset + (activeTabRect.bottom - containerRect.bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newOffset = Math.max(newOffset, 0);
|
||||||
|
navOffset.value = Math.min(newOffset, maxOffset);
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
if (!navRef.value) return;
|
||||||
|
activeEl.value = navRef.value?.querySelector(".layui-this") as HTMLElement;
|
||||||
|
tabBarStyle.value = getBarStyle();
|
||||||
|
|
||||||
|
if (props.tabPosition !== "top" && props.tabPosition !== "bottom") return; // 暂时屏蔽垂直方向
|
||||||
|
const navSize = getNavSize();
|
||||||
|
const containerSize = navRef.value[`offset${sizeName.value}`];
|
||||||
|
const currentOffset = navOffset.value;
|
||||||
|
const scrollNextSize = scrollNextRef.value?.[`offset${sizeName.value}`] ?? 0;
|
||||||
|
const scrollPrevSize = scrollPrevRef.value?.[`offset${sizeName.value}`] ?? 0;
|
||||||
|
if (containerSize < navSize) {
|
||||||
|
const currentOffset = navOffset.value;
|
||||||
|
scrollable.value = true;
|
||||||
|
if (navSize - currentOffset < containerSize) {
|
||||||
|
navOffset.value =
|
||||||
|
navSize - containerSize + scrollNextSize + scrollPrevSize;
|
||||||
|
}
|
||||||
|
scrollToActiveTab();
|
||||||
|
} else {
|
||||||
|
scrollable.value = false;
|
||||||
|
if (currentOffset > 0) {
|
||||||
|
navOffset.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useResizeObserver(navRef, update);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
slotsChange,
|
slotsChange,
|
||||||
@ -125,16 +245,22 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.modelValue, props.tabPosition, props.type],
|
() => [
|
||||||
|
props.modelValue,
|
||||||
|
props.tabPosition,
|
||||||
|
props.type,
|
||||||
|
childrens.value.length,
|
||||||
|
],
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
update();
|
update();
|
||||||
})
|
scrollToActiveTab();
|
||||||
|
});
|
||||||
|
|
||||||
provide("active", active);
|
provide("active", active);
|
||||||
provide("slotsChange", slotsChange);
|
provide("slotsChange", slotsChange);
|
||||||
@ -149,21 +275,28 @@ provide("slotsChange", slotsChange);
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
ref="headRef"
|
||||||
:class="['layui-tab-head', props.tabPosition ? `is-${tabPosition}` : '']"
|
:class="['layui-tab-head', props.tabPosition ? `is-${tabPosition}` : '']"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
|
ref="navRef"
|
||||||
:class="[
|
:class="[
|
||||||
'layui-tab-title',
|
'layui-tab-title',
|
||||||
props.tabPosition ? `is-${tabPosition}` : '',
|
props.tabPosition ? `is-${tabPosition}` : '',
|
||||||
]"
|
]"
|
||||||
|
:style="navStyle"
|
||||||
>
|
>
|
||||||
<div ref="activeBarRef" v-if="type === 'brief'" class="layui-tab-active-bar" :style="tabBarStyle"></div>
|
<div
|
||||||
|
ref="activeBarRef"
|
||||||
|
v-if="type === 'brief'"
|
||||||
|
class="layui-tab-active-bar"
|
||||||
|
:style="tabBarStyle"
|
||||||
|
></div>
|
||||||
<li
|
<li
|
||||||
v-for="(children, index) in childrens"
|
v-for="(children, index) in childrens"
|
||||||
:key="children.props?.id"
|
:key="children.props?.id"
|
||||||
:class="[children.props?.id === active ? 'layui-this' : '']"
|
:class="[children.props?.id === active ? 'layui-this' : '']"
|
||||||
@click.stop="change(children.props?.id)"
|
@click.stop="change(children.props?.id)"
|
||||||
|
|
||||||
>
|
>
|
||||||
{{ children.props?.title }}
|
{{ children.props?.title }}
|
||||||
<i
|
<i
|
||||||
@ -173,7 +306,22 @@ provide("slotsChange", slotsChange);
|
|||||||
></i>
|
></i>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<span
|
||||||
|
ref="scrollPrevRef"
|
||||||
|
v-if="scrollable"
|
||||||
|
class="layui-unselect layui-tab-bar prev"
|
||||||
|
@click="scrollPrev"
|
||||||
|
>
|
||||||
|
<LayIcon type="layui-icon-left"></LayIcon>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
ref="scrollNextRef"
|
||||||
|
v-if="scrollable"
|
||||||
|
class="layui-unselect layui-tab-bar"
|
||||||
|
@click="scrollNext"
|
||||||
|
>
|
||||||
|
<LayIcon type="layui-icon-right"></LayIcon>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-tab-content">
|
<div class="layui-tab-content">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -224,6 +224,53 @@ export default {
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
::: title 动态添加
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: demo
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<lay-button @click="addTab">添加</lay-button>
|
||||||
|
<lay-tab type="card" allow-close v-model="current8">
|
||||||
|
<lay-tab-item v-for="a in arr2" :key="a" :title="a.title" :id="a.id" :closable="a.closable">
|
||||||
|
内容{{a.id}}
|
||||||
|
</lay-tab-item>
|
||||||
|
</lay-tab>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
let index = 4;
|
||||||
|
const current8 = ref('1')
|
||||||
|
const arr2 = ref([
|
||||||
|
{id:'1', title:'选项一', closable: false},
|
||||||
|
{id:'2', title:'选项二'},
|
||||||
|
{id:'3', title:'选项三'}
|
||||||
|
])
|
||||||
|
const addTab = function(){
|
||||||
|
index++;
|
||||||
|
arr2.value.push({
|
||||||
|
id: String(index),
|
||||||
|
title:'新选项卡' + index
|
||||||
|
})
|
||||||
|
current8.value = String(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
arr2,
|
||||||
|
addTab,
|
||||||
|
current8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
::: title 位置设置
|
::: title 位置设置
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user