feat(projects): 创建自定义布局组件SoybeanLayout
This commit is contained in:
parent
006467a062
commit
0653fb144f
@ -121,6 +121,11 @@ module.exports = {
|
|||||||
group: 'internal',
|
group: 'internal',
|
||||||
position: 'before'
|
position: 'before'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'before'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: '@/interface',
|
pattern: '@/interface',
|
||||||
group: 'internal',
|
group: 'internal',
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from './custom';
|
export * from './custom';
|
||||||
export * from './svg';
|
export * from './svg';
|
||||||
|
export * from './custom';
|
||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './business';
|
export * from './business';
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col h-full">
|
|
||||||
<header
|
|
||||||
:class="{ 'fixed-lt': topFixed }"
|
|
||||||
:style="{ height: headerHeight + 'px', paddingLeft: headerPaddingLeft }"
|
|
||||||
class="z-1001 flex-shrink-0 w-full bg-white border-b border-gray-200 transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<slot name="header">
|
|
||||||
<h3>Header</h3>
|
|
||||||
</slot>
|
|
||||||
</header>
|
|
||||||
<div
|
|
||||||
:class="{ fixed: topFixed }"
|
|
||||||
:style="{ top: headerHeight + 'px', height: tabHeight + 'px', paddingLeft: siderWidth }"
|
|
||||||
class="left-0 z-999 flex-shrink-0 w-full bg-white border-b border-gray-200 transition-all duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<slot name="tab">
|
|
||||||
<div>Tab</div>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
<aside
|
|
||||||
:style="{ width: siderWidth, paddingTop: siderPaddingTop }"
|
|
||||||
:class="[isVertical ? 'z-1002' : 'z-1000']"
|
|
||||||
class="fixed-lt h-full transition-all duration-300 ease-in-out bg-white border-r border-gray-200"
|
|
||||||
>
|
|
||||||
<slot name="sider">
|
|
||||||
<n-space :vertical="true" align="center" class="pt-24px">
|
|
||||||
<n-button type="primary" @click="toggle">折叠</n-button>
|
|
||||||
<div>
|
|
||||||
<span class="pr-12px">固定头部和标签</span>
|
|
||||||
<n-switch v-model:value="fixed" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="pr-12px">vertical布局</span>
|
|
||||||
<n-switch v-model:value="isVertical" />
|
|
||||||
</div>
|
|
||||||
</n-space>
|
|
||||||
</slot>
|
|
||||||
</aside>
|
|
||||||
<main
|
|
||||||
class="flex-1 transition-all duration-300 ease-in-out"
|
|
||||||
:style="{ paddingLeft: siderWidth, paddingTop: mainPaddingTop }"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
import { NSpace, NButton, NSwitch } from 'naive-ui';
|
|
||||||
import { useBoolean } from '@/hooks';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 头部高度 */
|
|
||||||
headerHeight?: number;
|
|
||||||
/** 标签页高度 */
|
|
||||||
tabHeight?: number;
|
|
||||||
/** 固定头部和标签 */
|
|
||||||
fixdTop?: boolean;
|
|
||||||
/** 侧边栏高度 */
|
|
||||||
siderWidth?: number;
|
|
||||||
/** 侧边栏折叠状态的高度 */
|
|
||||||
siderCollapsedWidth?: number;
|
|
||||||
/** 侧边栏折叠状态 */
|
|
||||||
siderCollapse?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
headerHeight: 56,
|
|
||||||
tabHeight: 44,
|
|
||||||
fixdTop: true,
|
|
||||||
topZIndex: 1000,
|
|
||||||
siderWidth: 200,
|
|
||||||
siderZIndex: 1001,
|
|
||||||
siderCollapsedWidth: 64,
|
|
||||||
siderCollapse: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const { bool: collapse, toggle } = useBoolean();
|
|
||||||
|
|
||||||
const fixed = ref(true);
|
|
||||||
const isVertical = ref(true);
|
|
||||||
|
|
||||||
const topFixed = computed(() => fixed.value || !isVertical.value);
|
|
||||||
|
|
||||||
const siderWidth = computed(() => `${collapse.value ? props.siderCollapsedWidth : props.siderWidth}px`);
|
|
||||||
const headerPaddingLeft = computed(() => (isVertical.value ? siderWidth.value : '0px'));
|
|
||||||
const siderPaddingTop = computed(() => (isVertical.value ? '0px' : `${props.headerHeight}px`));
|
|
||||||
const mainPaddingTop = computed(() => `${fixed.value ? props.headerHeight + props.tabHeight : 0}px`);
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,3 +0,0 @@
|
|||||||
import BasicLayout from './BasicLayout.vue';
|
|
||||||
|
|
||||||
export { BasicLayout };
|
|
@ -1,11 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<basic-layout>
|
<soybean-layout :mode="mode" :fixed-header-and-tab="fixed" :fixed-footer="fixedFooter" :sider-collapse="collapse">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-end h-full bg-red-600">
|
||||||
|
<h3 class="text-white">Header</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #tab>
|
||||||
|
<div class="h-full bg-green-600"></div>
|
||||||
|
</template>
|
||||||
|
<template #sider>
|
||||||
|
<div class="w-full h-full bg-gray-200">
|
||||||
|
<n-space :vertical="true" align="center" class="pt-24px">
|
||||||
|
<n-button type="primary" @click="toggle">折叠</n-button>
|
||||||
|
<div>
|
||||||
|
<span class="pr-12px">固定头部和标签</span>
|
||||||
|
<n-switch v-model:value="fixed" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="pr-12px">固定底部</span>
|
||||||
|
<n-switch v-model:value="fixedFooter" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="pr-12px">vertical布局</span>
|
||||||
|
<n-radio-group v-model:value="mode">
|
||||||
|
<n-radio v-for="item in radios" :key="item.value" :value="item.value">
|
||||||
|
{{ item.label }}
|
||||||
|
</n-radio>
|
||||||
|
</n-radio-group>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<global-content />
|
<global-content />
|
||||||
</basic-layout>
|
<template #footer>
|
||||||
|
<div class="h-full bg-blue-400">
|
||||||
|
<h3>footer</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</soybean-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { NSpace, NButton, NSwitch, NRadioGroup, NRadio } from 'naive-ui';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
import { SoybeanLayout } from '@/package';
|
||||||
import { GlobalContent } from '../common';
|
import { GlobalContent } from '../common';
|
||||||
import { BasicLayout } from './components';
|
|
||||||
|
type LayoutMode = 'vertical' | 'horizontal';
|
||||||
|
|
||||||
|
interface ModeRadio {
|
||||||
|
value: LayoutMode;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width } = useElementSize(document.documentElement);
|
||||||
|
const { bool: collapse, toggle } = useBoolean();
|
||||||
|
|
||||||
|
const minWidthOfLayout = 1200;
|
||||||
|
|
||||||
|
const fixed = ref(true);
|
||||||
|
const fixedFooter = ref(true);
|
||||||
|
const mode = ref<LayoutMode>('vertical');
|
||||||
|
|
||||||
|
const radios: ModeRadio[] = [
|
||||||
|
{ value: 'vertical', label: 'vertical' },
|
||||||
|
{ value: 'horizontal', label: 'horizontal' }
|
||||||
|
];
|
||||||
|
|
||||||
|
watch(width, newValue => {
|
||||||
|
if (newValue < minWidthOfLayout) {
|
||||||
|
document.documentElement.style.overflowX = 'auto';
|
||||||
|
} else {
|
||||||
|
document.documentElement.style.overflowX = 'hidden';
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
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 };
|
@ -1,22 +1,20 @@
|
|||||||
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
|
import { kebabCase } from 'lodash-es';
|
||||||
import { getColorPalette, addColorAlpha } from '@/utils';
|
import { getColorPalette, addColorAlpha } from '@/utils';
|
||||||
|
|
||||||
type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error';
|
type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
||||||
type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active';
|
type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active';
|
||||||
|
|
||||||
type ColorKey = `${ColorType}Color${ColorScene}`;
|
type ColorKey = `${ColorType}Color${ColorScene}`;
|
||||||
|
|
||||||
type ThemeColor = {
|
type ThemeColor = {
|
||||||
[key in ColorKey]?: string;
|
[key in ColorKey]?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ColorAction {
|
interface ColorAction {
|
||||||
scene: ColorScene;
|
scene: ColorScene;
|
||||||
handler: (color: string) => string;
|
handler: (color: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取主题颜色的各种场景对应的颜色 */
|
/** 获取主题颜色的各种场景对应的颜色 */
|
||||||
export function getThemeColors(colors: [ColorType, string][]) {
|
function getThemeColors(colors: [ColorType, string][]) {
|
||||||
const colorActions: ColorAction[] = [
|
const colorActions: ColorAction[] = [
|
||||||
{ scene: '', handler: color => color },
|
{ scene: '', handler: color => color },
|
||||||
{ scene: 'Suppl', handler: color => color },
|
{ scene: 'Suppl', handler: color => color },
|
||||||
@ -38,23 +36,51 @@ export function getThemeColors(colors: [ColorType, string][]) {
|
|||||||
return themeColor;
|
return themeColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取naive的主题颜色 */
|
||||||
|
export function getNaiveThemeOverrides(colors: { [key in ColorType]: string }): GlobalThemeOverrides {
|
||||||
|
const { primary, info, success, warning, error } = colors;
|
||||||
|
const themeColors = getThemeColors([
|
||||||
|
['primary', primary],
|
||||||
|
['info', info],
|
||||||
|
['success', success],
|
||||||
|
['warning', warning],
|
||||||
|
['error', error]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const colorLoading = primary;
|
||||||
|
|
||||||
|
return {
|
||||||
|
common: {
|
||||||
|
...themeColors
|
||||||
|
},
|
||||||
|
LoadingBar: {
|
||||||
|
colorLoading
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeVars = Exclude<GlobalThemeOverrides['common'], undefined>;
|
||||||
|
type ThemeVarsKeys = keyof ThemeVars;
|
||||||
|
|
||||||
|
/** 添加css vars至html */
|
||||||
|
export function addThemeCssVarsToHtml(themeVars: ThemeVars) {
|
||||||
|
const keys = Object.keys(themeVars) as ThemeVarsKeys[];
|
||||||
|
const style: string[] = [];
|
||||||
|
keys.forEach(key => {
|
||||||
|
style.push(`--${kebabCase(key)}: ${themeVars[key]}`);
|
||||||
|
});
|
||||||
|
const styleStr = style.join(';');
|
||||||
|
document.documentElement.style.cssText = styleStr;
|
||||||
|
}
|
||||||
|
|
||||||
/** windicss 暗黑模式 */
|
/** windicss 暗黑模式 */
|
||||||
export function handleWindicssDarkMode() {
|
export function handleWindicssDarkMode() {
|
||||||
const DARK_CLASS = 'dark';
|
const DARK_CLASS = 'dark';
|
||||||
function getHtmlElement() {
|
|
||||||
return document.querySelector('html');
|
|
||||||
}
|
|
||||||
function addDarkClass() {
|
function addDarkClass() {
|
||||||
const html = getHtmlElement();
|
document.documentElement.classList.add(DARK_CLASS);
|
||||||
if (html) {
|
|
||||||
html.classList.add(DARK_CLASS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function removeDarkClass() {
|
function removeDarkClass() {
|
||||||
const html = getHtmlElement();
|
document.documentElement.classList.remove(DARK_CLASS);
|
||||||
if (html) {
|
|
||||||
html.classList.remove(DARK_CLASS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
addDarkClass,
|
addDarkClass,
|
||||||
|
@ -3,10 +3,9 @@ import type { Ref, ComputedRef } from 'vue';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useThemeVars, darkTheme, useOsTheme } from 'naive-ui';
|
import { useThemeVars, darkTheme, useOsTheme } from 'naive-ui';
|
||||||
import type { GlobalThemeOverrides, GlobalTheme } from 'naive-ui';
|
import type { GlobalThemeOverrides, GlobalTheme } from 'naive-ui';
|
||||||
import { kebabCase } from 'lodash-es';
|
|
||||||
import { useBoolean } from '@/hooks';
|
import { useBoolean } from '@/hooks';
|
||||||
import { getColorPalette } from '@/utils';
|
import { getColorPalette } from '@/utils';
|
||||||
import { getThemeColors, handleWindicssDarkMode } from './helpers';
|
import { getNaiveThemeOverrides, addThemeCssVarsToHtml, handleWindicssDarkMode } from './helpers';
|
||||||
|
|
||||||
interface OtherColor {
|
interface OtherColor {
|
||||||
/** 信息 */
|
/** 信息 */
|
||||||
@ -38,8 +37,6 @@ interface ThemeStore {
|
|||||||
naiveTheme: ComputedRef<BuiltInGlobalTheme | undefined>;
|
naiveTheme: ComputedRef<BuiltInGlobalTheme | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeVarsKeys = keyof Exclude<GlobalThemeOverrides['common'], undefined>;
|
|
||||||
|
|
||||||
export const useThemeStore = defineStore('theme-store', () => {
|
export const useThemeStore = defineStore('theme-store', () => {
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean();
|
const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean();
|
||||||
@ -53,27 +50,9 @@ export const useThemeStore = defineStore('theme-store', () => {
|
|||||||
error: '#f5222d'
|
error: '#f5222d'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const naiveThemeOverrides = computed<GlobalThemeOverrides>(() => {
|
const naiveThemeOverrides = computed<GlobalThemeOverrides>(() =>
|
||||||
const { info, success, warning, error } = otherColor.value;
|
getNaiveThemeOverrides({ primary: themeColor.value, ...otherColor.value })
|
||||||
const themeColors = getThemeColors([
|
);
|
||||||
['primary', themeColor.value],
|
|
||||||
['info', info],
|
|
||||||
['success', success],
|
|
||||||
['warning', warning],
|
|
||||||
['error', error]
|
|
||||||
]);
|
|
||||||
|
|
||||||
const colorLoading = themeColor.value;
|
|
||||||
|
|
||||||
return {
|
|
||||||
common: {
|
|
||||||
...themeColors
|
|
||||||
},
|
|
||||||
LoadingBar: {
|
|
||||||
colorLoading
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/** naive-ui暗黑主题 */
|
/** naive-ui暗黑主题 */
|
||||||
const naiveTheme = computed(() => (darkMode.value ? darkTheme : undefined));
|
const naiveTheme = computed(() => (darkMode.value ? darkTheme : undefined));
|
||||||
@ -81,22 +60,14 @@ export const useThemeStore = defineStore('theme-store', () => {
|
|||||||
/** 操作系统暗黑主题 */
|
/** 操作系统暗黑主题 */
|
||||||
const osTheme = useOsTheme();
|
const osTheme = useOsTheme();
|
||||||
|
|
||||||
/** 添加css vars至html */
|
/** 初始化css vars, 并添加至html */
|
||||||
function addThemeCssVarsToHtml() {
|
function initThemeCssVars() {
|
||||||
if (document.documentElement.style.cssText) return;
|
|
||||||
|
|
||||||
const updatedThemeVars = { ...themeVars.value, ...naiveThemeOverrides.value.common };
|
const updatedThemeVars = { ...themeVars.value, ...naiveThemeOverrides.value.common };
|
||||||
const keys = Object.keys(updatedThemeVars) as ThemeVarsKeys[];
|
addThemeCssVarsToHtml(updatedThemeVars);
|
||||||
const style: string[] = [];
|
|
||||||
keys.forEach(key => {
|
|
||||||
style.push(`--${kebabCase(key)}: ${updatedThemeVars[key]}`);
|
|
||||||
});
|
|
||||||
const styleStr = style.join(';');
|
|
||||||
document.documentElement.style.cssText = styleStr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
addThemeCssVarsToHtml();
|
initThemeCssVars();
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="h-full">
|
||||||
<h3>DashboardAnalysis</h3>
|
<h3>DashboardAnalysis</h3>
|
||||||
<router-link to="/about">about</router-link>
|
<router-link to="/about">about</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +40,7 @@ export default defineConfig({
|
|||||||
'fixed-center': 'fixed left-0 top-0 flex-center wh-full',
|
'fixed-center': 'fixed left-0 top-0 flex-center wh-full',
|
||||||
'nowrap-hidden': 'whitespace-nowrap overflow-hidden',
|
'nowrap-hidden': 'whitespace-nowrap overflow-hidden',
|
||||||
'ellipsis-text': 'nowrap-hidden overflow-ellipsis',
|
'ellipsis-text': 'nowrap-hidden overflow-ellipsis',
|
||||||
|
'transition-base': 'transition-all duration-300 ease-in-out',
|
||||||
'init-loading-spin': 'w-16px h-16px bg-primary rounded-8px animate-pulse'
|
'init-loading-spin': 'w-16px h-16px bg-primary rounded-8px animate-pulse'
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
|
Loading…
Reference in New Issue
Block a user