效果展示
使用方法
<ResizeBox
leftTitle="左侧标题"
rightTitle="右侧标题"
leftWidth={300}
leftButton={() => {
return (
<el-button
type="primary"
onClick={() => {
b.value = b.value + 1
console.log(b.value)
}}
>
左侧按钮
</el-button>
)
}}
v-slots={{
left: () => {
return (
<div>
{arr.map(() => {
return (
<span>
文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
)
})}
</div>
)
},
center: () => {
return (
<ResizeBox
leftTitle="左侧标题2"
leftWidth={300}
v-slots={{
left: () => {
return (
<div>
{arr.map(() => {
return (
<span>
文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
)
})}
</div>
)
},
center: () => {
return (
<card title="中间">
<Mytable />
</card>
)
}
}}
/>
)
},
right: () => {
return (
<div>
{arr.map(() => {
return (
<span>
文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
)
})}
</div>
)
}
}}
/>
代码
import { defineComponent, ref, computed } from 'vue'
import './index.scss'
export default defineComponent({
name: '',
props: {
leftWidth: {
type: Number,
default: 300
},
rightWidth: {
type: Number,
default: 300
},
leftTitle: {
type: String,
default: ''
},
rightTitle: {
type: String,
default: ''
},
leftButton: {
type: Function,
default: () => {}
},
rightButton: {
type: Function,
default: () => {}
}
},
setup(props, { slots }) {
const resizeRef = ref()
// 左边宽度
const leftWidth = ref(props.leftWidth)
// 右边宽度
const rightWidth = ref(props.rightWidth)
// 折叠后宽度
const foldWidth = 64
// 是否折叠
const fold = ref(true)
// 右边是否折叠
const rightFold = ref(true)
/**
* @description: 折叠侧边栏
* @Date: 2024-01-11 09:06:33
* @Author:
* @param {boolean} val
*/
const toggleFold = (val: boolean, direction: string) => {
const targetWidth = val ? props.leftWidth : foldWidth
const step = val ? 5 : -5
const timer = setInterval(
() => {
const widthToAdjust =
direction === 'right' ? rightWidth.value : leftWidth.value
if (
(val && widthToAdjust < targetWidth) ||
(!val && widthToAdjust > targetWidth)
) {
if (direction === 'right') {
rightWidth.value += step
} else {
leftWidth.value += step
}
} else {
clearInterval(timer)
if (!val) {
direction === 'right'
? (rightFold.value = false)
: (fold.value = false)
}
}
},
val ? 2 : 4
)
if (val) {
direction === 'right' ? (rightFold.value = true) : (fold.value = true)
}
}
/**
* @description: 拖拽侧边栏
* @Date: 2023-12-28 17:48:35
* @Author:
* @param {any} event
* @param {string} dom
*/
const lineMousedown = (event: any, dom: string) => {
const startClientX = event.clientX
const domWidth = resizeRef.value.querySelector(`.${dom}`).clientWidth
document.onmousemove = (e: any): boolean => {
const endClientX = e.clientX
if (dom === 'resize-box-left') {
leftWidth.value = endClientX - startClientX + domWidth
if (leftWidth.value < foldWidth + 20) {
leftWidth.value = foldWidth
fold.value = false
return false
} else {
fold.value = true
return false
}
}
if (dom === 'resize-box-right') {
rightWidth.value = startClientX - endClientX + domWidth
if (rightWidth.value < foldWidth + 20) {
rightWidth.value = foldWidth
rightFold.value = false
return false
} else {
rightFold.value = true
return false
}
}
return false
}
document.onmouseup = (): boolean => {
document.onmousemove = null
document.onmouseup = null
return false
}
return
}
/**
* @description: 设置渐变 折叠后被折叠内容逐渐变白色
* @Date: 2023-12-28 17:48:13
* @Author:
*/
const getPercentageInRange = (widthRef) => {
const min = foldWidth
const max = props.leftWidth
const range = max - min
const relativePosition = widthRef.value - min
return relativePosition / range
}
const percentageInRange = computed(() => getPercentageInRange(leftWidth))
const percentageInRangeRight = computed(() =>
getPercentageInRange(rightWidth)
)
return () => {
return (
<div ref={resizeRef} class="resize-box">
<div
class="resize-box-left"
style={[
`width: ${leftWidth.value}px;flex: 0 0 ${leftWidth.value}px;`
]}
>
<card
style="width:100%"
title={props.leftTitle}
fold={fold.value} // 是否折叠
foldWidth={foldWidth} // 折叠后宽度
isFold // 是否是折叠卡片
onToggleFold={toggleFold}
v-slots={{
buttons: props.leftButton
}}
>
<div
style={[
'width: 100%;height: 100%',
`opacity:${percentageInRange.value}`
]}
>
{slots.left && slots.left()}
</div>
</card>
</div>
<div
class="drag-line"
onMousedown={(e: any) => lineMousedown(e, 'resize-box-left')}
></div>
<div class="resize-box-center">{slots.center && slots.center()}</div>
{slots.right && (
<div
class="drag-line"
onMousedown={(e: any) => lineMousedown(e, 'resize-box-right')}
></div>
)}
{slots.right && (
<div
class="resize-box-right"
style={[
`width: ${rightWidth.value}px;flex: 0 0 ${rightWidth.value}px;`
]}
>
<card
style="width:100%"
title={props.rightTitle}
fold={rightFold.value}
foldWidth={foldWidth}
rightFold
isFold
onToggleFold={(val: boolean) => {
toggleFold(val, 'right')
}}
v-slots={{
buttons: props.rightButton
}}
>
<div
style={[
'width: 100%;height: 100%',
`opacity:${percentageInRangeRight.value}`
]}
>
{slots.right && slots.right()}
</div>
</card>
</div>
)}
</div>
)
}
}
})
css
.resize-box {
display: flex;
width: 100%;
height: 100%;
// gap: 10px;
.resize-box-left,
.resize-box-right {
height: 100%;
// width: 400px;
// flex: 0 0 400px;
}
.drag-line {
width: 20px;
flex: 0 0 20px;
cursor: col-resize;
}
.resize-box-center {
flex: 1;
width: 0;
}
}
卡片封装的代码可以参考
<script lang="ts" name="Card" setup>
import { useSlots, provide, ref, Ref, computed, onMounted, nextTick } from 'vue'
const props = defineProps({
height: {
type: String,
default: '100%'
},
title: String,
foldWidth: {
type: Number,
default: 64
},
message: String,
isFold: Boolean, // 是否需要折叠
fold: Boolean, // 是否折叠
rightFold: Boolean, // 右边的折叠
modelValue: String || Number
})
const emits = defineEmits(['toggle-fold', 'tab-click', 'update:modelValue'])
const clickFold = () => {
emits('toggle-fold', !props.fold)
}
const slot = useSlots()
const clickNav = (value: any) => {
emits('tab-click', value)
emits('update:modelValue', value)
}
const nav: Ref<Array<string>> = ref([])
const setTab = (val: any) => {
if (nav.value.includes(val)) return
nav.value.push(val)
}
const currentValue = computed(() => {
return props.modelValue
})
provide('tabsRoot', {
currentValue,
setTab
})
const navRef = ref()
const navBottomLineList: any = ref({})
const activeStyle = computed(() => {
const currentLine = navBottomLineList.value[props.modelValue as string]
return {
width: currentLine?.width,
left: currentLine?.left
}
})
onMounted(async () => {
await nextTick()
const childSpans = Array.from(navRef.value?.querySelectorAll('span') || [])
if (childSpans.length) {
navBottomLineList.value = {}
childSpans.forEach((span: any) => {
const name = span.textContent
navBottomLineList.value[name] = {
width: `${span.offsetWidth}px`,
left: `${span.offsetLeft}px`
}
})
}
})
</script>
<template>
<el-card
:style="[
'width: 100%;',
'background-color: #fff;',
'transition: all 5s ease',
`${
(isFold && fold) || !isFold
? 'transition: all 5s ease'
: `transition: all 5s ease;width: ${props.foldWidth}px;`
}`,
`height: ${height ? height : ''}`
]"
>
<template #header v-if="title || slot.buttons || nav?.length">
<div class="card-header" v-if="(title || slot.buttons) && !nav?.length">
<span
class="title"
v-if="(isFold && fold) || !isFold"
:style="slot.buttons ? 'line-height:32px' : ''"
>
{{ title }}
</span>
<div class="title-right">
<slot v-if="(fold && isFold) || !isFold" name="buttons"></slot>
<el-icon
v-if="isFold"
class="fold"
@click="clickFold"
:style="`cursor: pointer;transition: transform 0.1s ease;${
!fold
? `${props.rightFold ? '' : 'transform: rotate(180deg)'}`
: `${props.rightFold ? 'transform: rotate(180deg)' : ''}`
}`"
:size="24"
><Fold
/></el-icon>
</div>
</div>
<div class="card-header-nav-wrap" v-if="nav?.length">
<div>
<div v-if="title">
<span class="title">{{ title }}</span>
</div>
<div ref="navRef" class="card-header-nav-content" v-if="nav?.length">
<div class="active-bar" :style="activeStyle"></div>
<div
v-for="(item, index) in nav"
:class="[
'card-header-nav',
modelValue === (item as any) ? 'active' : ''
]"
:key="index"
>
<span @click="clickNav(item as any)">{{ item as any }}</span>
</div>
</div>
</div>
<slot name="buttons"></slot>
</div>
</template>
<slot v-if="(fold && isFold) || !isFold"></slot>
</el-card>
</template>
<style scoped lang="scss">
::v-deep .el-card__header {
padding: 0;
}
::v-deep .el-card__body {
height: calc(100% - 71px);
}
.card-header {
display: flex;
padding: 20px;
justify-content: space-between;
}
.title {
font-size: 20px;
font-weight: bold;
white-space: nowrap; /* 防止文字换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis;
}
.title-right {
display: flex;
gap: 10px;
align-items: center;
}
.card-header-nav-wrap {
display: flex;
justify-content: space-between;
padding: 11px 20px 0 0;
> div {
display: flex;
}
& :last-child {
display: flex;
align-items: center;
}
.title {
padding: 0 20px;
line-height: 50px;
}
.card-header-nav-content {
position: relative;
height: 50px;
display: flex;
align-items: center;
.active-bar {
position: absolute;
left: 0;
bottom: 0;
height: 2px;
transition: all 0.3s;
background-color: #004892;
}
.card-header-nav {
padding: 0 20px;
span {
display: inline-block;
cursor: pointer;
line-height: 50px;
font-size: 20px;
border-bottom: 2px solid transparent;
color: #888888;
}
&.active {
span {
color: inherit;
}
}
}
}
}
</style>