chore: init project
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export { default as LayoutContent } from './layout-content.vue';
|
||||
export { default as LayoutFooter } from './layout-footer.vue';
|
||||
export { default as LayoutHeader } from './layout-header.vue';
|
||||
export { default as LayoutSide } from './layout-side.vue';
|
||||
export { default as LayoutTabs } from './layout-tabs.vue';
|
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContentCompactType } from '@vben-core/typings';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 内容区域定宽
|
||||
* @default 'wide'
|
||||
*/
|
||||
contentCompact?: ContentCompactType;
|
||||
/**
|
||||
* 定宽布局宽度
|
||||
* @default 1200
|
||||
*/
|
||||
contentCompactWidth?: number;
|
||||
/**
|
||||
* padding
|
||||
* @default 16
|
||||
*/
|
||||
padding?: number;
|
||||
/**
|
||||
* paddingBottom
|
||||
* @default 16
|
||||
*/
|
||||
paddingBottom?: number;
|
||||
/**
|
||||
* paddingLeft
|
||||
* @default 16
|
||||
*/
|
||||
paddingLeft?: number;
|
||||
/**
|
||||
* paddingRight
|
||||
* @default 16
|
||||
*/
|
||||
paddingRight?: number;
|
||||
/**
|
||||
* paddingTop
|
||||
* @default 16
|
||||
*/
|
||||
paddingTop?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentCompact: 'wide',
|
||||
contentCompactWidth: 1200,
|
||||
padding: 16,
|
||||
paddingBottom: 16,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 16,
|
||||
});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const {
|
||||
contentCompact,
|
||||
padding,
|
||||
paddingBottom,
|
||||
paddingLeft,
|
||||
paddingRight,
|
||||
paddingTop,
|
||||
} = props;
|
||||
|
||||
const compactStyle: CSSProperties =
|
||||
contentCompact === 'compact' ? { margin: '0 auto', width: `1200px` } : {};
|
||||
return {
|
||||
...compactStyle,
|
||||
flex: 1,
|
||||
padding: `${padding}px`,
|
||||
paddingBottom: `${paddingBottom}px`,
|
||||
paddingLeft: `${paddingLeft}px`,
|
||||
paddingRight: `${paddingRight}px`,
|
||||
paddingTop: `${paddingTop}px`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main :style="style">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* 是否固定在顶部
|
||||
* @default true
|
||||
*/
|
||||
fixed?: boolean;
|
||||
/**
|
||||
* 高度
|
||||
* @default 32
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* 高度
|
||||
* @default 100%
|
||||
*/
|
||||
width?: string;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 0
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutFooter' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--color-background))',
|
||||
fixed: true,
|
||||
height: 32,
|
||||
show: true,
|
||||
width: '100%',
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const { b } = useNamespace('footer');
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { backgroundColor, fixed, height, show, width, zIndex } = props;
|
||||
return {
|
||||
backgroundColor,
|
||||
height: `${height}px`,
|
||||
marginBottom: show ? '0' : `-${height}px`,
|
||||
position: fixed ? 'fixed' : 'static',
|
||||
width,
|
||||
zIndex,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer
|
||||
:class="b()"
|
||||
class="bottom-0 w-full transition-all duration-200"
|
||||
:style="style"
|
||||
>
|
||||
<slot></slot>
|
||||
</footer>
|
||||
</template>
|
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import { IcRoundMenu } from '@vben-core/iconify';
|
||||
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import { computed, useSlots } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
|
||||
/**
|
||||
* 横屏
|
||||
* @default false
|
||||
*/
|
||||
fullWidth?: boolean;
|
||||
/**
|
||||
* 高度
|
||||
* @default 60
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* 是否混合导航
|
||||
* @default false
|
||||
*/
|
||||
isMixedNav?: boolean;
|
||||
/**
|
||||
* 是否移动端
|
||||
* @default false
|
||||
*/
|
||||
isMobile?: boolean;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* 是否显示关闭菜单按钮
|
||||
* @default true
|
||||
*/
|
||||
showToggleBtn?: boolean;
|
||||
/**
|
||||
* 侧边是否显示
|
||||
*/
|
||||
sideHidden?: boolean;
|
||||
/**
|
||||
* 侧边菜单宽度
|
||||
* @default 0
|
||||
*/
|
||||
sideWidth?: number;
|
||||
/**
|
||||
* 宽度
|
||||
* @default 100%
|
||||
*/
|
||||
width?: string;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 0
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutHeader' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--color-background))',
|
||||
// fixed: true,
|
||||
height: 60,
|
||||
isMixedNav: false,
|
||||
show: true,
|
||||
showToggleBtn: false,
|
||||
sideWidth: 0,
|
||||
width: '100%',
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ openMenu: []; toggleMenu: [] }>();
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const { b, e } = useNamespace('header');
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { backgroundColor, fullWidth, height, show } = props;
|
||||
const right = !show || !fullWidth ? undefined : 0;
|
||||
|
||||
return {
|
||||
// ...(props.isMixedNav ? { left: 0, position: `fixed` } : {}),
|
||||
backgroundColor,
|
||||
height: `${height}px`,
|
||||
marginTop: show ? 0 : `-${height}px`,
|
||||
right,
|
||||
};
|
||||
});
|
||||
|
||||
const logoStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
minWidth: `${props.isMobile ? 40 : props.sideWidth}px`,
|
||||
};
|
||||
});
|
||||
|
||||
function handleToggleMenu() {
|
||||
emit('toggleMenu');
|
||||
}
|
||||
|
||||
function handleOpenMenu() {
|
||||
emit('openMenu');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header :class="b()" :style="style">
|
||||
<div v-if="slots.logo" :style="logoStyle">
|
||||
<slot name="logo"></slot>
|
||||
</div>
|
||||
<VbenIconButton
|
||||
v-if="showToggleBtn"
|
||||
:class="e('toggle-btn')"
|
||||
@click="handleToggleMenu"
|
||||
>
|
||||
<IcRoundMenu class="size-5" />
|
||||
</VbenIconButton>
|
||||
|
||||
<VbenIconButton
|
||||
v-if="isMobile"
|
||||
:class="e('toggle-btn')"
|
||||
@click="handleOpenMenu"
|
||||
>
|
||||
<IcRoundMenu class="size-5" />
|
||||
</VbenIconButton>
|
||||
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('header') {
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
|
||||
@include e('toggle-btn') {
|
||||
margin: 0 4px 0 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,388 @@
|
||||
<script setup lang="ts">
|
||||
import { ScrollArea } from '@vben-core/shadcn-ui';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
// import { onClickOutside } from '@vueuse/core';
|
||||
import { computed, ref, shallowRef, useSlots, watchEffect } from 'vue';
|
||||
|
||||
import { SideCollapseButton, SidePinButton } from './widgets';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor: string;
|
||||
/**
|
||||
* 折叠区域高度
|
||||
* @default 32
|
||||
*/
|
||||
collapseHeight?: number;
|
||||
/**
|
||||
* 折叠宽度
|
||||
* @default 48
|
||||
*/
|
||||
collapseWidth?: number;
|
||||
/**
|
||||
* 隐藏的dom是否可见
|
||||
* @default true
|
||||
*/
|
||||
domVisible?: boolean;
|
||||
/**
|
||||
* 扩展区域背景颜色
|
||||
*/
|
||||
extraBackgroundColor: string;
|
||||
/**
|
||||
* 扩展区域宽度
|
||||
* @default 180
|
||||
*/
|
||||
extraWidth?: number;
|
||||
/**
|
||||
* 固定扩展区域
|
||||
* @default false
|
||||
*/
|
||||
fixedExtra?: boolean;
|
||||
/**
|
||||
* 头部高度
|
||||
*/
|
||||
headerHeight: number;
|
||||
/**
|
||||
* 是否侧边混合模式
|
||||
* @default false
|
||||
*/
|
||||
isSideMixed?: boolean;
|
||||
/**
|
||||
* 混合菜单宽度
|
||||
* @default 80
|
||||
*/
|
||||
mixedWidth?: number;
|
||||
/**
|
||||
* 顶部padding
|
||||
* @default 60
|
||||
*/
|
||||
paddingTop?: number;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* 显示折叠按钮
|
||||
* @default false
|
||||
*/
|
||||
showCollapseButton?: boolean;
|
||||
/**
|
||||
* 主题
|
||||
*/
|
||||
theme?: string;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
* @default 180
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 0
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutSide' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapseHeight: 42,
|
||||
collapseWidth: 48,
|
||||
domVisible: true,
|
||||
extraWidth: 180,
|
||||
fixedExtra: false,
|
||||
isSideMixed: false,
|
||||
mixedWidth: 80,
|
||||
paddingTop: 60,
|
||||
show: true,
|
||||
showCollapseButton: true,
|
||||
theme: 'dark',
|
||||
width: 180,
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ leave: [] }>();
|
||||
const collapse = defineModel<boolean>('collapse');
|
||||
const extraCollapse = defineModel<boolean>('extraCollapse');
|
||||
const expandOnHovering = defineModel<boolean>('expandOnHovering');
|
||||
const expandOnHover = defineModel<boolean>('expandOnHover');
|
||||
const extraVisible = defineModel<boolean>('extraVisible');
|
||||
|
||||
const { b, e, is } = useNamespace('side');
|
||||
const slots = useSlots();
|
||||
|
||||
const asideRef = shallowRef<HTMLDivElement | null>();
|
||||
const scrolled = ref(false);
|
||||
|
||||
const hiddenSideStyle = computed((): CSSProperties => {
|
||||
return calcMenuWidthStyle(true);
|
||||
});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { isSideMixed, paddingTop, zIndex } = props;
|
||||
|
||||
return {
|
||||
...calcMenuWidthStyle(false),
|
||||
paddingTop: `${paddingTop}px`,
|
||||
zIndex,
|
||||
...(isSideMixed && extraVisible.value ? { transition: 'none' } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
const extraStyle = computed((): CSSProperties => {
|
||||
const { extraBackgroundColor, extraWidth, width, zIndex } = props;
|
||||
return {
|
||||
backgroundColor: extraBackgroundColor,
|
||||
left: `${width}px`,
|
||||
width: extraVisible.value ? `${extraWidth}px` : 0,
|
||||
zIndex,
|
||||
};
|
||||
});
|
||||
|
||||
const extraTitleStyle = computed((): CSSProperties => {
|
||||
const { headerHeight } = props;
|
||||
|
||||
return {
|
||||
height: `${headerHeight - 1}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const contentWidthStyle = computed((): CSSProperties => {
|
||||
const { collapseWidth, fixedExtra, isSideMixed, mixedWidth } = props;
|
||||
if (isSideMixed && fixedExtra) {
|
||||
// if (!extraVisible.value) {
|
||||
// return {};
|
||||
// }
|
||||
return { width: `${collapse.value ? collapseWidth : mixedWidth}px` };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const contentStyle = computed((): CSSProperties => {
|
||||
const { collapseHeight, headerHeight } = props;
|
||||
|
||||
return {
|
||||
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
||||
paddingTop: '8px',
|
||||
...contentWidthStyle.value,
|
||||
};
|
||||
});
|
||||
|
||||
const headerStyle = computed((): CSSProperties => {
|
||||
const { headerHeight, isSideMixed } = props;
|
||||
|
||||
return {
|
||||
...(isSideMixed ? { display: 'flex', justifyContent: 'center' } : {}),
|
||||
height: `${headerHeight}px`,
|
||||
...contentWidthStyle.value,
|
||||
};
|
||||
});
|
||||
|
||||
const extraContentStyle = computed((): CSSProperties => {
|
||||
const { collapseHeight, headerHeight } = props;
|
||||
return {
|
||||
color: 'red',
|
||||
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
const collapseStyle = computed((): CSSProperties => {
|
||||
const { collapseHeight } = props;
|
||||
|
||||
return {
|
||||
height: `${collapseHeight}px`,
|
||||
};
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
extraVisible.value = props.fixedExtra ? true : extraVisible.value;
|
||||
});
|
||||
|
||||
// onClickOutside(asideRef, (event) => {
|
||||
// const { fixedExtra, width } = props;
|
||||
// // 防止点击 aside 区域关闭
|
||||
// if (!fixedExtra && event.clientX >= width && extraVisible.value) {
|
||||
// extraVisible.value = false;
|
||||
// }
|
||||
// });
|
||||
|
||||
function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
|
||||
const { backgroundColor, extraWidth, fixedExtra, isSideMixed, show, width } =
|
||||
props;
|
||||
|
||||
let widthValue = `${width + (isSideMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
|
||||
|
||||
const { collapseWidth } = props;
|
||||
|
||||
if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
|
||||
widthValue = `${collapseWidth}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
...(widthValue === '0px' ? { overflow: 'hidden' } : {}),
|
||||
backgroundColor,
|
||||
flex: `0 0 ${widthValue}`,
|
||||
marginLeft: show ? 0 : `-${widthValue}`,
|
||||
maxWidth: widthValue,
|
||||
minWidth: widthValue,
|
||||
width: widthValue,
|
||||
};
|
||||
}
|
||||
|
||||
function handleMouseenter() {
|
||||
// 未开启和未折叠状态不生效
|
||||
if (expandOnHover.value) {
|
||||
return;
|
||||
}
|
||||
if (!expandOnHovering.value) {
|
||||
collapse.value = false;
|
||||
}
|
||||
expandOnHovering.value = true;
|
||||
}
|
||||
|
||||
function handleMouseleave() {
|
||||
emit('leave');
|
||||
|
||||
if (expandOnHover.value) {
|
||||
return;
|
||||
}
|
||||
expandOnHovering.value = false;
|
||||
collapse.value = true;
|
||||
extraVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
scrolled.value = (target?.scrollTop ?? 0) > 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="domVisible" :class="e('hide')" :style="hiddenSideStyle"></div>
|
||||
<aside
|
||||
:class="[b(), is(theme, true)]"
|
||||
:style="style"
|
||||
@mouseenter="handleMouseenter"
|
||||
@mouseleave="handleMouseleave"
|
||||
>
|
||||
<SidePinButton
|
||||
v-if="!collapse && !isSideMixed"
|
||||
v-model:expand-on-hover="expandOnHover"
|
||||
:theme="theme"
|
||||
/>
|
||||
<div v-if="slots.logo" :style="headerStyle">
|
||||
<slot name="logo"></slot>
|
||||
</div>
|
||||
<ScrollArea :style="contentStyle" :on-scroll="handleScroll">
|
||||
<div :class="[e('shadow'), { scrolled }]"></div>
|
||||
<slot></slot>
|
||||
</ScrollArea>
|
||||
|
||||
<div :style="collapseStyle"></div>
|
||||
<SideCollapseButton
|
||||
v-if="showCollapseButton && !isSideMixed"
|
||||
v-model:collapse="collapse"
|
||||
:theme="theme"
|
||||
/>
|
||||
<div
|
||||
v-if="isSideMixed"
|
||||
ref="asideRef"
|
||||
:class="e('extra')"
|
||||
:style="extraStyle"
|
||||
>
|
||||
<SideCollapseButton
|
||||
v-if="isSideMixed && expandOnHover"
|
||||
v-model:collapse="extraCollapse"
|
||||
:theme="theme"
|
||||
/>
|
||||
|
||||
<SidePinButton
|
||||
v-if="!extraCollapse"
|
||||
v-model:expand-on-hover="expandOnHover"
|
||||
:theme="theme"
|
||||
/>
|
||||
<div v-if="!extraCollapse" :style="extraTitleStyle">
|
||||
<slot name="extra-title"></slot>
|
||||
</div>
|
||||
<ScrollArea
|
||||
:style="extraContentStyle"
|
||||
:class="e('extra-content')"
|
||||
:on-scroll="handleScroll"
|
||||
>
|
||||
<div :class="[e('shadow'), { scrolled }]"></div>
|
||||
<slot name="extra"></slot>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('side') {
|
||||
--color-surface: var(--color-menu);
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
transition: all 0.2s ease 0s;
|
||||
|
||||
@include is('dark') {
|
||||
--color-surface: var(--color-menu-dark);
|
||||
}
|
||||
|
||||
@include e('shadow') {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
inline-size: 100%;
|
||||
block-size: 40px;
|
||||
height: 50px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--color-surface)),
|
||||
transparent
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
will-change: opacity;
|
||||
|
||||
&.scrolled {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include is('dark') {
|
||||
.#{$namespace}-side__extra {
|
||||
&-content {
|
||||
border-color: hsl(var(--color-dark-border)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include e('hide') {
|
||||
height: 100%;
|
||||
transition: all 0.2s ease 0s;
|
||||
}
|
||||
|
||||
@include e('extra') {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease 0s;
|
||||
|
||||
&-content {
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* 高度
|
||||
* @default 30
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutTabs' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--color-background))',
|
||||
fixed: true,
|
||||
height: 30,
|
||||
});
|
||||
|
||||
const { b, e } = useNamespace('tabs');
|
||||
|
||||
const hiddenStyle = computed((): CSSProperties => {
|
||||
const { height } = props;
|
||||
return {
|
||||
height: `${height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { backgroundColor } = props;
|
||||
return {
|
||||
...hiddenStyle.value,
|
||||
backgroundColor,
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :class="b()" :style="style">
|
||||
<slot></slot>
|
||||
<div :class="e('toolbar')">
|
||||
<slot name="toolbar"></slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('tabs') {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
// transition: all 0.2s;
|
||||
|
||||
@include e('toolbar') {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,2 @@
|
||||
export { default as SideCollapseButton } from './side-collapse-button.vue';
|
||||
export { default as SidePinButton } from './side-pin-button.vue';
|
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiMenuClose, MdiMenuOpen } from '@vben-core/iconify';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
interface Props {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'SideCollapseButton' });
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const collapse = defineModel<boolean>('collapse');
|
||||
|
||||
const { b, is } = useNamespace('side-collapse');
|
||||
|
||||
function handleCollapse() {
|
||||
collapse.value = !collapse.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[b(), is(theme, true)]" @click.stop="handleCollapse">
|
||||
<MdiMenuClose v-if="collapse" />
|
||||
<MdiMenuOpen v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('side-collapse') {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
color: hsl(var(--color-foreground) / 60%);
|
||||
cursor: pointer;
|
||||
background: hsl(var(--color-accent)) !important;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@include is('dark') {
|
||||
color: hsl(var(--color-dark-foreground) / 60%) !important;
|
||||
background: hsl(var(--color-dark-accent)) !important;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-dark-foreground)) !important;
|
||||
background: hsl(var(--color-dark-accent-hover)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
background: hsl(var(--color-accent-hover));
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiPin, MdiPinOff } from '@vben-core/iconify';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
interface Props {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'SidePinButton' });
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const expandOnHover = defineModel<boolean>('expandOnHover');
|
||||
|
||||
const { b, is } = useNamespace('side-pin');
|
||||
|
||||
function togglePined() {
|
||||
expandOnHover.value = !expandOnHover.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[b(), is(theme, true)]" @click="togglePined">
|
||||
<MdiPinOff v-if="!expandOnHover" />
|
||||
<MdiPin v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('side-pin') {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 6px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
color: hsl(var(--color-foreground) / 60%);
|
||||
cursor: pointer;
|
||||
background: hsl(var(--color-accent)) !important;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@include is('dark') {
|
||||
color: hsl(var(--color-dark-foreground) / 60%) !important;
|
||||
background: unset;
|
||||
background: hsl(var(--color-dark-accent)) !important;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-dark-foreground)) !important;
|
||||
background: hsl(var(--color-dark-accent-hover)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
background: hsl(var(--color-accent-hover));
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user