可以拖拽折叠的双飞翼布局

89 阅读2分钟

效果展示

ezgif-3-2cad8dd546.gif

使用方法

        <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>