This commit is contained in:
2022-11-14 11:56:21 +08:00
commit 0a63adba99
337 changed files with 25661 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
.layui-tab {
display: flex;
margin: 10px 0;
text-align: left !important;
}
.layui-tab[overflow] > .layui-tab-head > .layui-tab-title {
overflow: hidden;
}
.layui-tab.is-left {
flex-direction: row;
}
.layui-tab.is-right {
flex-direction: row-reverse;
justify-content: space-between
}
.layui-tab.is-top {
flex-direction: column;
}
.layui-tab.is-bottom {
flex-direction: column-reverse
}
.layui-tab-head {
display: inline-block;
overflow: hidden;
}
.layui-tab-card .layui-tab-head {
background-color: var(--global-neutral-color-1);
}
.layui-tab-title {
position: relative;
left: 0;
height: 40px;
white-space: nowrap;
font-size: 0;
border-bottom-width: 1px;
border-bottom-style: solid;
transition: all 0.2s;
-webkit-transition: all 0.2s;
}
.layui-tab-title li {
display: inline-block;
vertical-align: middle;
font-size: 14px;
transition: all 0.2s;
-webkit-transition: all 0.2s;
position: relative;
line-height: 40px;
min-width: 65px;
padding: 0 15px;
text-align: center;
cursor: pointer;
user-select: none;
}
.layui-tab-title li a {
display: block;
padding: 0 15px;
margin: 0 -15px;
}
.layui-tab-head.is-top,
.layui-tab-head.is-bottom,
.layui-tab-title.is-top,
.layui-tab-title.is-bottom {
width: 100%;
position: relative;
}
.layui-tab-title.is-right,
.layui-tab-title.is-left {
height: 100%;
min-width: 60px;
border-bottom-width: 0px;
border-bottom-style: none;
}
.layui-tab-title.is-left li {
display: list-item;
margin-right: -1px;
}
.layui-tab-title.is-right li{
display: list-item;
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 {
border-left: 1px solid var(--global-neutral-color-3);
}
.layui-tab-title.is-left {
border-right: 1px solid var(--global-neutral-color-3);
}
.layui-tab-title .layui-this {
color: #000;
background-color: #fff;
}
.layui-tab-title .layui-this:after {
position: absolute;
left: 0;
top: 0;
content: "";
width: 100%;
height: 41px;
border-width: 1px;
border-style: solid;
border-bottom-color: #fff;
border-radius: 2px 2px 0 0;
box-sizing: border-box;
pointer-events: none;
}
.layui-tab-title.is-left .layui-this:after {
border: 1px solid var(--global-neutral-color-3);
border-right-color: #FFF;
}
.layui-tab-title.is-right .layui-this:after {
border: 1px solid var(--global-neutral-color-3);
border-left-color: #FFF;
}
.layui-tab-brief>.layui-tab-head{
background-color: transparent;
}
.layui-tab-brief>.layui-tab-head>.layui-tab-title .layui-this {
color: var(--global-primary-color);
}
.layui-tab-brief>.layui-tab-head>.layui-tab-more li.layui-this:after,
.layui-tab-brief>.layui-tab-head>.layui-tab-title .layui-this:after {
border: none;
border-radius: 0;
}
.layui-tab-brief>.layui-tab-head.is-right>.layui-tab-title{
border-left: 1px solid var(--global-neutral-color-3);
}
.layui-tab-brief>.layui-tab-head.is-left>.layui-tab-title {
border-right: 1px solid var(--global-neutral-color-3);
}
.layui-tab-brief[overflow]>.layui-tab-head>.layui-tab-title .layui-this:after {
top: -1px;
}
.layui-tab-brief>.layui-tab-head.is-right>.layui-tab-title li,
.layui-tab-brief>.layui-tab-head.is-left>.layui-tab-title li{
margin-right: 0px;
}
.layui-tab-brief>.layui-tab-head.is-top>.layui-tab-title li,
.layui-tab-brief>.layui-tab-head.is-top>.layui-tab-title li {
margin-top: 0px;
margin-bottom: 0px;
}
.layui-tab-card {
border-width: 1px;
border-style: solid;
border-radius: 2px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
}
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-top {
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-left {
margin-right: -1px;
margin-left: -1px;
}
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-bottom li {
margin-top: -1px;
}
.layui-tab-card>.layui-tab-head>.layui-tab-title .layui-this:after{
border-radius: 0;
}
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-bottom{
border-top: 1px solid var(--global-neutral-color-3);
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-right li {
margin-top: -1px;
margin-bottom: -1px;
}
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-top .layui-this:after {
border: 1px solid var(--global-neutral-color-3);
border-bottom-color: #fff;
}
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-bottom .layui-this:after {
border: 1px solid var(--global-neutral-color-3);
border-top-color: #fff;
}
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-left .layui-this:after {
border: 1px solid var(--global-neutral-color-3);
border-right-color: #fff;
}
.layui-tab-card>.layui-tab-head>.layui-tab-title.is-right .layui-this:after {
border: 1px solid var(--global-neutral-color-3);
border-left-color: #fff;
}
.layui-tab-card>.layui-tab-head .layui-tab-bar {
width: 40px;
line-height: 40px;
border-radius: 0;
border-top: none;
border-right: none;
}
.layui-tab-card>.layui-tab-more .layui-this {
background: 0 0;
color: var(--global-checked-color);
}
.layui-tab-card>.layui-tab-more .layui-this:after {
border: none;
}
.layui-tab-bar {
position: absolute;
right: 0;
top: 0;
z-index: 10;
width: 30px;
height: 39px;
line-height: 39px;
border-width: 1px;
border-style: solid;
border-radius: 2px;
text-align: center;
background-color: #fff;
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 {
top: 3px;
font-size: 13.6px;
display: inline-block;
transition: all 0.3s;
-webkit-transition: all 0.3s;
}
.layui-tab-item {
display: none;
}
.layui-tab-more {
padding-right: 30px;
height: auto !important;
white-space: normal !important;
}
.layui-tab-more li.layui-this:after {
border-bottom-color: var(--global-neutral-color-3);
border-radius: 2px;
}
.layui-tab-more .layui-tab-bar .layui-icon {
top: -2px;
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.layui-tab-title li .layui-tab-close {
position: relative;
display: inline-block;
width: 18px;
height: 18px;
line-height: 20px;
margin-left: 8px;
top: 1px;
text-align: center;
font-size: 14px;
color: var(--global-neutral-color-8);
transition: all 0.2s;
-webkit-transition: all 0.2s;
}
.layui-tab-title li .layui-tab-close:hover {
border-radius: 2px;
background-color: #ff5722;
color: #fff;
}
.layui-tab-content {
padding: 15px 0;
flex: 1;
}
.layui-tab.is-right>.layui-tab-content,
.layui-tab.is-left>.layui-tab-content {
height: 100%;
padding: 0 10px;
display: inline-block;
vertical-align: top;
}
.layui-tab-active-bar{
position: absolute;
bottom: 0px;
left: 0;
height: 1.5px;
background-color: var(--global-checked-color);
z-index: 2;
list-style: none;
box-sizing: border-box;
pointer-events: none;
}
.is-top .layui-tab-active-bar{
bottom: -1px;
height: 1.5px;
}
.is-left .layui-tab-active-bar {
left: auto;
right: -1px;
top: 0;
bottom: auto;
width: 1.5px;
}
.is-right .layui-tab-active-bar {
left: -1px;
right: auto;
top: 0;
bottom: auto;
width: 1.5px;
}

View File

@@ -0,0 +1,5 @@
import { withInstall, WithInstallType } from "../../utils";
import Component from "./index.vue";
const component: WithInstallType<typeof Component> = withInstall(Component);
export default component;

392
src/component/tab/index.vue Normal file
View File

@@ -0,0 +1,392 @@
<script lang="ts">
export default {
name: "LayTab",
};
</script>
<script setup lang="ts">
import "./index.less";
import { LayIcon } from "@layui/icons-vue";
import tabItem from "../tabItem/index.vue";
import RenderFunction from "../_components/renderFunction";
import {
Component,
computed,
useSlots,
provide,
VNode,
Ref,
ref,
watch,
shallowRef,
onMounted,
nextTick,
CSSProperties,
reactive,
h,
createTextVNode,
Fragment,
} from "vue";
import { useResizeObserver } from "@vueuse/core";
import { TabData, TabInjectKey, TabPosition } from "./interface";
export interface TabProps {
type?: string;
modelValue: string;
allowClose?: boolean;
tabPosition?: TabPosition;
beforeClose?: Function;
beforeLeave?: Function;
activeBarTransition?: boolean;
}
const slot = useSlots();
const childrens: Ref<VNode[]> = ref([]);
const tabMap = reactive(new Map<number, TabData>());
const setItemInstanceBySlot = function (nodes: VNode[]) {
nodes?.map((item) => {
let component = item.type as Component;
if (item.type.toString() == "Symbol(Fragment)") {
setItemInstanceBySlot(item.children as VNode[]);
} else {
if (component.name == tabItem.name) {
childrens.value.push(item);
}
}
});
};
const props = withDefaults(defineProps<TabProps>(), {
tabPosition: "top",
});
const emit = defineEmits(["update:modelValue", "change", "close"]);
const active = computed({
get() {
return props.modelValue;
},
set(val) {
emit("update:modelValue", val);
},
});
const tabItems = computed(() => {
const tabData: TabData[] = [];
childrens.value.forEach((item) => {
const tab = tabMap.get(item.props?.id);
if (tab) tabData.push(tab);
});
return tabData;
});
const addItem = (id: number, data: any) => {
tabMap.set(id, data);
};
const removeItem = (id: number) => {
tabMap.delete(id);
};
provide(
TabInjectKey,
reactive({
active: active,
addItem,
removeItem,
})
);
const change = function (id: any) {
if (props.beforeLeave && props.beforeLeave(id) === false) {
return;
}
emit("update:modelValue", id);
emit("change", id);
};
const close = function (index: number, id: any) {
if (props.beforeClose && props.beforeClose(id) === false) {
return;
}
childrens.value.splice(index, 1);
if (active.value === id) {
const nextChildren =
childrens.value[index === childrens.value.length ? 0 : index];
change(nextChildren && nextChildren.props ? nextChildren.props.id : "");
}
emit("close", id);
};
const activeBarRef = shallowRef<HTMLElement | undefined>(undefined);
const activeEl = shallowRef<HTMLElement | undefined>(undefined);
const tabBarStyle = ref<CSSProperties>();
const getBarStyle = () => {
let offset = 0;
let tabSize = 0;
const sizeName =
props.tabPosition === "top" || props.tabPosition === "bottom"
? "width"
: "height";
const axis = sizeName === "width" ? "X" : "Y";
const position = axis === "X" ? "left" : "top";
const el = activeEl.value;
const activeElParentElement = navRef.value;
if (!el || !activeElParentElement) return;
const rect = el?.getBoundingClientRect();
const parentRect = activeElParentElement?.getBoundingClientRect();
offset = rect[position] - parentRect[position];
tabSize = el.getBoundingClientRect()[sizeName];
return {
[sizeName]: `${tabSize}px`,
transform: `translate${axis}(${offset}px)`,
transition: props.activeBarTransition ? `transform .3s` : "",
};
};
const navRef = shallowRef<HTMLElement | undefined>(undefined);
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;
}
}
};
const renderTabIcon = (attrs: Record<string, unknown>) => {
const tab = attrs.tabData as TabData;
if (typeof tab.icon === "function") {
return tab.icon();
} else if (typeof tab.icon === "string") {
return h(LayIcon, {
type: tab.icon,
style: "margin-right: 8px;",
});
}
};
const renderTabTitle = (attrs: Record<string, unknown>) => {
const tab = attrs.tabData as TabData;
if (tab.slots?.title) {
return h(Fragment, tab.slots?.title && tab.slots.title());
}
if (typeof tab.title === "function") {
return tab.title();
} else if (typeof tab.title === "string") {
return createTextVNode(tab.title as string);
}
};
useResizeObserver(navRef, update);
watch(
tabMap,
function () {
childrens.value = [];
setItemInstanceBySlot((slot.default && slot.default()) as VNode[]);
},
{ immediate: true }
);
watch(
() => [
props.modelValue,
props.tabPosition,
props.type,
childrens.value.length,
],
async () => {
await nextTick();
update();
}
);
onMounted(() => {
update();
scrollToActiveTab();
});
provide("active", active);
</script>
<template>
<div
class="layui-tab"
:class="[
type ? 'layui-tab-' + type : '',
props.tabPosition ? `is-${tabPosition}` : '',
]"
>
<div
ref="headRef"
:class="['layui-tab-head', props.tabPosition ? `is-${tabPosition}` : '']"
>
<ul
ref="navRef"
:class="[
'layui-tab-title',
props.tabPosition ? `is-${tabPosition}` : '',
]"
:style="navStyle"
>
<div
ref="activeBarRef"
v-if="type === 'brief'"
class="layui-tab-active-bar"
:style="tabBarStyle"
></div>
<li
v-for="(child, index) in tabItems"
:key="child.id"
:class="[child.id === active ? 'layui-this' : '']"
@click.stop="change(child.id)"
>
<span>
<RenderFunction
v-if="child['icon']"
:renderFunc="renderTabIcon"
:tabData="child"
/>
<RenderFunction :renderFunc="renderTabTitle" :tabData="child" />
</span>
<i
v-if="allowClose && child.closable != false"
class="layui-icon layui-icon-close layui-unselect layui-tab-close"
@click.stop="close(index, child.id)"
></i>
</li>
</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 class="layui-tab-content">
<slot></slot>
</div>
</div>
</template>

View File

@@ -0,0 +1,19 @@
import { Slots } from "vue";
export const TabInjectKey = Symbol("layuiTab");
export interface TabData {
id: string;
title?: string | Function;
icon?: string | Function;
closable?: string | boolean;
slots: Slots;
}
export interface TabsContext {
active: string;
addItem: (id: string, data: TabData) => void;
removeItem: (id: string) => void;
}
export type TabPosition = "top" | "bottom" | "left" | "right";