feat(projects): 创建自定义布局组件SoybeanLayout
This commit is contained in:
41
src/package/SoybeanLayout/components/LayoutContent.vue
Normal file
41
src/package/SoybeanLayout/components/LayoutContent.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<main class="soybean-layout__main" :style="style">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 顶部内边距 */
|
||||
paddingTop?: number;
|
||||
/** 底部内边距 */
|
||||
paddingBottom?: number;
|
||||
/** 左侧内边距 */
|
||||
paddingLeft?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionDuration?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionTimingFunction?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
paddingLeft: 0,
|
||||
transitionDuration: 300,
|
||||
transitionTimingFunction: 'ease-in-out'
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
const { paddingTop, paddingBottom, paddingLeft, transitionDuration, transitionTimingFunction } = props;
|
||||
return `padding-top: ${paddingTop}px;padding-bottom: ${paddingBottom}px;padding-left: ${paddingLeft}px;transition-duration: ${transitionDuration}ms;transition-timing-function: ${transitionTimingFunction};`;
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.soybean-layout__main {
|
||||
flex-grow: 1;
|
||||
transition-property: padding-left;
|
||||
}
|
||||
</style>
|
||||
51
src/package/SoybeanLayout/components/LayoutFooter.vue
Normal file
51
src/package/SoybeanLayout/components/LayoutFooter.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<header class="soybean-layout__footer" :style="style">
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 开启fixed布局 */
|
||||
fixed?: boolean;
|
||||
/** fixed布局的层级 */
|
||||
zIndex?: number;
|
||||
/** 最小宽度 */
|
||||
minWidth?: number;
|
||||
/** 高度 */
|
||||
height?: number;
|
||||
/** 左侧内边距 */
|
||||
paddingLeft?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionDuration?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionTimingFunction?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
fixed: true,
|
||||
zIndex: 999,
|
||||
minWidth: 1200,
|
||||
height: 56,
|
||||
paddingLeft: 0,
|
||||
transitionDuration: 300,
|
||||
transitionTimingFunction: 'ease-in-out'
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
const { fixed, zIndex, minWidth, height, paddingLeft, transitionDuration, transitionTimingFunction } = props;
|
||||
const position = fixed ? 'fixed' : 'static';
|
||||
return `position: ${position};z-index: ${zIndex};min-width: ${minWidth}px;height: ${height}px;padding-left: ${paddingLeft}px;transition-duration: ${transitionDuration}ms;transition-timing-function: ${transitionTimingFunction};`;
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.soybean-layout__footer {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
transition-property: padding-left;
|
||||
}
|
||||
</style>
|
||||
51
src/package/SoybeanLayout/components/LayoutHeader.vue
Normal file
51
src/package/SoybeanLayout/components/LayoutHeader.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<header class="soybean-layout__header" :style="style">
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 开启fixed布局 */
|
||||
fixed?: boolean;
|
||||
/** fixed布局的层级 */
|
||||
zIndex?: number;
|
||||
/** 最小宽度 */
|
||||
minWidth?: number;
|
||||
/** 高度 */
|
||||
height?: number;
|
||||
/** 左侧内边距 */
|
||||
paddingLeft?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionDuration?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionTimingFunction?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
fixed: true,
|
||||
zIndex: 1001,
|
||||
minWidth: 1200,
|
||||
height: 56,
|
||||
paddingLeft: 0,
|
||||
transitionDuration: 300,
|
||||
transitionTimingFunction: 'ease-in-out'
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
const { fixed, zIndex, minWidth, height, paddingLeft, transitionDuration, transitionTimingFunction } = props;
|
||||
const position = fixed ? 'fixed' : 'static';
|
||||
return `position: ${position};z-index: ${zIndex};min-width: ${minWidth}px;height: ${height}px;padding-left: ${paddingLeft}px;transition-duration: ${transitionDuration}ms;transition-timing-function: ${transitionTimingFunction};`;
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.soybean-layout__header {
|
||||
left: 0;
|
||||
top: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
transition-property: padding-left;
|
||||
}
|
||||
</style>
|
||||
44
src/package/SoybeanLayout/components/LayoutSider.vue
Normal file
44
src/package/SoybeanLayout/components/LayoutSider.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<aside class="soybean-layout__sider" :style="style">
|
||||
<slot></slot>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** fixed布局的层级 */
|
||||
zIndex?: number;
|
||||
/** 宽度 */
|
||||
width?: number;
|
||||
/** 顶部内边距 */
|
||||
paddingTop?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionDuration?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionTimingFunction?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
zIndex: 1002,
|
||||
width: 200,
|
||||
paddingTop: 0,
|
||||
transitionDuration: 300,
|
||||
transitionTimingFunction: 'ease-in-out'
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
const { zIndex, width, paddingTop, transitionDuration, transitionTimingFunction } = props;
|
||||
return `z-index: ${zIndex};width: ${width}px;padding-top: ${paddingTop}px;transition-duration: ${transitionDuration}ms;transition-timing-function: ${transitionTimingFunction};`;
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.soybean-layout__sider {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transition-property: all;
|
||||
}
|
||||
</style>
|
||||
53
src/package/SoybeanLayout/components/LayoutTab.vue
Normal file
53
src/package/SoybeanLayout/components/LayoutTab.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="soybean-layout__tab" :style="style">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 开启fixed布局 */
|
||||
fixed?: boolean;
|
||||
/** fixed布局的top距离 */
|
||||
top?: number;
|
||||
/** fixed布局的层级 */
|
||||
zIndex?: number;
|
||||
/** 最小宽度 */
|
||||
minWidth?: number;
|
||||
/** 高度 */
|
||||
height?: number;
|
||||
/** 左侧内边距 */
|
||||
paddingLeft?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionDuration?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionTimingFunction?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
fixed: true,
|
||||
top: 56,
|
||||
zIndex: 999,
|
||||
minWidth: 1200,
|
||||
height: 56,
|
||||
paddingLeft: 0,
|
||||
transitionDuration: 300,
|
||||
transitionTimingFunction: 'ease-in-out'
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
const { fixed, top, zIndex, minWidth, height, paddingLeft, transitionDuration, transitionTimingFunction } = props;
|
||||
const position = fixed ? 'fixed' : 'static';
|
||||
return `position: ${position};top: ${top}px;z-index: ${zIndex};min-width: ${minWidth}px;height: ${height}px;padding-left: ${paddingLeft}px;transition-duration: ${transitionDuration}ms;transition-timing-function: ${transitionTimingFunction};`;
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.soybean-layout__tab {
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
transition-property: padding-left;
|
||||
}
|
||||
</style>
|
||||
7
src/package/SoybeanLayout/components/index.ts
Normal file
7
src/package/SoybeanLayout/components/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import LayoutHeader from './LayoutHeader.vue';
|
||||
import LayoutTab from './LayoutTab.vue';
|
||||
import LayoutSider from './LayoutSider.vue';
|
||||
import LayoutContent from './LayoutContent.vue';
|
||||
import LayoutFooter from './LayoutFooter.vue';
|
||||
|
||||
export { LayoutSider, LayoutHeader, LayoutTab, LayoutContent, LayoutFooter };
|
||||
53
src/package/SoybeanLayout/hooks/index.ts
Normal file
53
src/package/SoybeanLayout/hooks/index.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { ref, computed, watch, onUnmounted } from 'vue';
|
||||
import type { Ref, ComputedRef } from 'vue';
|
||||
|
||||
/**
|
||||
* 使用了固定定位的布局元素添加translateX
|
||||
* @param isFixed - 是否是fixed布局
|
||||
*/
|
||||
export function useFixedTransformStyle(isFixed: Ref<boolean> | ComputedRef<boolean>) {
|
||||
const scrollLeft = ref(0);
|
||||
const transformStyle = computed(() => `transform: translateX(${-scrollLeft.value}px);`);
|
||||
|
||||
function setScrollLeft(sLeft: number) {
|
||||
scrollLeft.value = sLeft;
|
||||
}
|
||||
function scrollHandler() {
|
||||
const sLeft = document.scrollingElement?.scrollLeft || 0;
|
||||
setScrollLeft(sLeft);
|
||||
}
|
||||
function initScrollLeft() {
|
||||
scrollHandler();
|
||||
}
|
||||
|
||||
function addScrollEventListener() {
|
||||
document.addEventListener('scroll', scrollHandler);
|
||||
}
|
||||
|
||||
function removeScrollEventListener() {
|
||||
document.removeEventListener('scroll', scrollHandler);
|
||||
}
|
||||
|
||||
function init() {
|
||||
initScrollLeft();
|
||||
addScrollEventListener();
|
||||
}
|
||||
|
||||
watch(
|
||||
isFixed,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
init();
|
||||
} else {
|
||||
removeScrollEventListener();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
removeScrollEventListener();
|
||||
});
|
||||
|
||||
return transformStyle;
|
||||
}
|
||||
173
src/package/SoybeanLayout/index.vue
Normal file
173
src/package/SoybeanLayout/index.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="soybean-layout" :style="{ minWidth: minWidth + 'px' }">
|
||||
<layout-header
|
||||
v-if="headerVisible"
|
||||
v-bind="commonProps"
|
||||
:fixed="fixedHeaderAndTab"
|
||||
:z-index="headerZIndex"
|
||||
:min-width="minWidth"
|
||||
:height="headerHeight"
|
||||
:padding-left="headerPaddingLeft"
|
||||
:style="headerAndTabTransform"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
</layout-header>
|
||||
<layout-tab
|
||||
v-if="tabVisible"
|
||||
v-bind="commonProps"
|
||||
:fixed="fixedHeaderAndTab"
|
||||
:z-index="tabZIndex"
|
||||
:top="headerHeight"
|
||||
:height="tabHeight"
|
||||
:padding-left="tabPaddingLeft"
|
||||
:style="headerAndTabTransform"
|
||||
>
|
||||
<slot name="tab"></slot>
|
||||
</layout-tab>
|
||||
<layout-sider
|
||||
v-if="siderVisible"
|
||||
v-bind="commonProps"
|
||||
:z-index="siderZIndex"
|
||||
:width="siderWidth"
|
||||
:padding-top="siderPaddingTop"
|
||||
>
|
||||
<slot name="sider"></slot>
|
||||
</layout-sider>
|
||||
<layout-content
|
||||
v-bind="commonProps"
|
||||
:padding-top="contentPaddingTop"
|
||||
:padding-bottom="contentPaddingBottom"
|
||||
:padding-left="siderWidth"
|
||||
>
|
||||
<slot></slot>
|
||||
</layout-content>
|
||||
<layout-footer
|
||||
v-if="footerVisible"
|
||||
v-bind="commonProps"
|
||||
:fixed="fixedFooter"
|
||||
:z-index="footerZIndex"
|
||||
:height="footerHeight"
|
||||
:padding-left="siderWidth"
|
||||
:style="footerTransform"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</layout-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { LayoutHeader, LayoutTab, LayoutSider, LayoutContent, LayoutFooter } from './components';
|
||||
import { useFixedTransformStyle } from './hooks';
|
||||
|
||||
interface Props {
|
||||
/** 布局模式 */
|
||||
mode?: 'vertical' | 'horizontal';
|
||||
/** 最小宽度 */
|
||||
minWidth?: number;
|
||||
/** 头部可见 */
|
||||
headerVisible?: boolean;
|
||||
/** 头部高度 */
|
||||
headerHeight?: number;
|
||||
/** 标签可见 */
|
||||
tabVisible?: boolean;
|
||||
/** 标签页高度 */
|
||||
tabHeight?: number;
|
||||
/** 固定头部和标签 */
|
||||
fixedHeaderAndTab?: boolean;
|
||||
/** 底部可见 */
|
||||
footerVisible?: boolean;
|
||||
/** 底部高度 */
|
||||
footerHeight?: number;
|
||||
/** 固定底部 */
|
||||
fixedFooter?: boolean;
|
||||
/** 侧边可见 */
|
||||
siderVisible?: boolean;
|
||||
/** 侧边栏高度 */
|
||||
siderWidth?: number;
|
||||
/** 侧边栏折叠状态的高度 */
|
||||
siderCollapsedWidth?: number;
|
||||
/** 侧边栏折叠状态 */
|
||||
siderCollapse?: boolean;
|
||||
/** 动画过渡时间 */
|
||||
transitionDuration?: number;
|
||||
/** 动画过渡时间 */
|
||||
transitionTimingFunction?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'vertical',
|
||||
minWidth: 1200,
|
||||
headerVisible: true,
|
||||
headerHeight: 56,
|
||||
tabVisible: true,
|
||||
tabHeight: 44,
|
||||
fixedHeaderAndTab: true,
|
||||
footerVisible: true,
|
||||
footerHeight: 48,
|
||||
fixedFooter: true,
|
||||
siderVisible: true,
|
||||
siderWidth: 200,
|
||||
siderCollapsedWidth: 64,
|
||||
siderCollapse: false,
|
||||
transitionDuration: 300,
|
||||
transitionTimingFunction: 'ease-in-out'
|
||||
});
|
||||
|
||||
// fixed布局时,应用translateX样式(水平方向出现滚动条,拖动滚动条时,fixed元素跟着滚动)
|
||||
const hasFixedEl = computed(() => props.fixedHeaderAndTab || props.fixedFooter);
|
||||
const transformStyle = useFixedTransformStyle(hasFixedEl);
|
||||
const headerAndTabTransform = computed(() => (props.fixedHeaderAndTab ? transformStyle.value : ''));
|
||||
const footerTransform = computed(() => (props.fixedFooter ? transformStyle.value : ''));
|
||||
|
||||
/** 各个子组件的公共属性 */
|
||||
const commonProps = computed(() => {
|
||||
const { transitionDuration, transitionTimingFunction } = props;
|
||||
return {
|
||||
transitionDuration,
|
||||
transitionTimingFunction
|
||||
};
|
||||
});
|
||||
|
||||
/** 水平布局 */
|
||||
const isVertical = computed(() => props.mode === 'vertical');
|
||||
|
||||
// fixed布局时的层级
|
||||
const headerZIndex = 1001;
|
||||
const tabZIndex = 999;
|
||||
const siderZIndex = computed(() => (isVertical.value ? 1002 : 1000));
|
||||
const footerZIndex = 999;
|
||||
|
||||
/** 侧边宽度 */
|
||||
const siderWidth = computed(() => {
|
||||
const { siderCollapse, siderWidth, siderCollapsedWidth } = props;
|
||||
const width = siderCollapse ? siderCollapsedWidth : siderWidth;
|
||||
return props.siderVisible ? width : 0;
|
||||
});
|
||||
|
||||
// 各子组件的属性
|
||||
const headerPaddingLeft = computed(() => (isVertical.value ? siderWidth.value : 0));
|
||||
const tabPaddingLeft = computed(() => (isVertical.value ? siderWidth.value : 0));
|
||||
const siderPaddingTop = computed(() => (!isVertical.value && props.headerVisible ? props.headerHeight : 0));
|
||||
const contentPaddingTop = computed(() => {
|
||||
let height = 0;
|
||||
if (props.fixedHeaderAndTab) {
|
||||
if (props.headerVisible) {
|
||||
height += props.headerHeight;
|
||||
}
|
||||
if (props.tabVisible) {
|
||||
height += props.tabHeight;
|
||||
}
|
||||
}
|
||||
return height;
|
||||
});
|
||||
const contentPaddingBottom = computed(() => (props.fixedFooter && props.footerVisible ? props.footerHeight : 0));
|
||||
</script>
|
||||
<style scoped>
|
||||
.soybean-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
3
src/package/index.ts
Normal file
3
src/package/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import SoybeanLayout from './SoybeanLayout/index.vue';
|
||||
|
||||
export { SoybeanLayout };
|
||||
Reference in New Issue
Block a user