PanelSplitter 组件:前端左右布局宽度调整的实用解决方案

11 阅读6分钟

背景

在前端开发中,左右分栏布局是一种常见的布局方式,尤其在管理系统中更为普遍。然而,固定宽度的布局往往无法满足所有用户的需求,不同屏幕尺寸和操作场景下,用户可能需要调整左右面板的宽度比例。

需求分析

在政策管理系统的开发过程中,我们遇到了以下需求:

  1. 客户信息管理:左侧客户查询列表,右侧客户详情确认
  2. 产品信息管理:左侧产品选择器,右侧产品详情和库存信息
  3. 政策配置:左侧政策类型列表,右侧具体政策配置表单
  4. 数据统计:左侧筛选条件,右侧图表展示

image.png

image.png

image.png 这些场景都需要一个通用的、流畅的拖拽调整宽度功能,以提升用户体验。

解决方案

经过分析,我们开发了一个通用的 PanelSplitter 组件,它具有以下特点:

  • 流畅的拖拽体验:实时响应鼠标/触摸移动,无卡顿感
  • 专业的视觉效果:统一的鼠标样式和平滑的过渡动画
  • 高度可配置:支持自定义初始宽度,适应不同场景
  • 易于集成:使用 Vue 的插槽机制,可轻松集成到任何项目中
  • 跨设备兼容:同时支持鼠标和触摸事件

技术实现

核心设计

  1. 事件处理:使用鼠标和触摸事件实现拖拽功能
  2. 宽度计算:根据鼠标位置计算面板宽度比例
  3. 视觉效果:使用 CSS 过渡动画实现平滑效果
  4. 组件化:使用 Vue 3 的 Composition API 实现组件逻辑

完整代码

<template>
  <div
    class="split-container"
    ref="splitContainerRef"
    @mousemove="handleResize"
    @mouseup="stopResize"
    @mouseleave="stopResize"
    @touchmove="handleResize"
    @touchend="stopResize"
  >
    <div class="left-panel" :style="{ width: leftWidth + '%' }">
      <slot name="left"></slot>
    </div>
    <div
      class="resizer"
      :class="{ resizing: isResizing }"
      @mousedown="startResize"
      @touchstart="startResize"
    ></div>
    <div class="right-panel" :style="{ width: 100 - leftWidth + '%' }">
      <slot name="right"></slot>
    </div>
  </div>
</template>

<script>
  import { defineComponent, ref } from 'vue-demi'

  export default defineComponent({
    name: 'PanelSplitter',
    props: {
      initialLeftWidth: {
        type: Number,
        default: 66.67,
      },
    },
    setup(props) {
      const leftWidth = ref(props.initialLeftWidth)
      const isResizing = ref(false)
      const splitContainerRef = ref(null)

      function startResize(e) {
        isResizing.value = true
        e.preventDefault()
        e.stopPropagation()
      }

      function handleResize(e) {
        if (!isResizing.value || !splitContainerRef.value) return

        const rect = splitContainerRef.value.getBoundingClientRect()
        const x = e.clientX || (e.touches && e.touches[0].clientX)
        let width = ((x - rect.left) / rect.width) * 100

        width = Math.max(20, Math.min(80, width))
        leftWidth.value = width
      }

      function stopResize() {
        isResizing.value = false
      }

      return {
        leftWidth,
        isResizing,
        startResize,
        handleResize,
        stopResize,
        splitContainerRef,
      }
    },
  })
</script>

<style scoped lang="scss">
  .split-container {
    display: flex;
    height: 100%;
    position: relative;

    &:active {
      cursor: ew-resize;
    }
  }

  .left-panel {
    height: 100%;
    overflow: auto;
    padding-right: 10px;
    &::-webkit-scrollbar {
      display: none;
    }
    -ms-overflow-style: none;
    scrollbar-width: none;
  }

  .resizer {
    width: 8px;
    background-color: transparent;
    cursor: ew-resize;
    user-select: none;
    position: relative;
    z-index: 10;

    &:before {
      content: '';
      position: absolute;
      top: 0;
      left: 3px;
      width: 2px;
      height: 100%;
      background-color: #dcdfe6;
    }

    &:hover {
      cursor: ew-resize;
    }

    &:active,
    &.resizing {
      cursor: ew-resize;
    }
  }

  .left-panel,
  .right-panel {
    transition: width 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }

  .right-panel {
    height: 100%;
    overflow: auto;
    padding-left: 10px;
    &::-webkit-scrollbar {
      display: none;
    }
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
</style>

使用方法

基本用法

<template>
  <div style="height: 500px;">
    <PanelSplitter>
      <template #left>
        <div class="left-content">
          <h3>客户列表</h3>
          <ul>
            <li v-for="customer in customers" :key="customer.id" @click="selectCustomer(customer)">
              {{ customer.name }}
            </li>
          </ul>
        </div>
      </template>
      <template #right>
        <div class="right-content">
          <h3>客户详情</h3>
          <div v-if="selectedCustomer">
            <p>姓名: {{ selectedCustomer.name }}</p>
            <p>电话: {{ selectedCustomer.phone }}</p>
            <p>地址: {{ selectedCustomer.address }}</p>
          </div>
          <div v-else>
            请选择客户
          </div>
        </div>
      </template>
    </PanelSplitter>
  </div>
</template>

<script>
import PanelSplitter from '@/views/policy/components/PanelSplitter'

export default {
  components: {
    PanelSplitter
  },
  data() {
    return {
      customers: [
        { id: 1, name: '张三', phone: '13800138000', address: '北京市朝阳区' },
        { id: 2, name: '李四', phone: '13900139000', address: '上海市浦东新区' },
        { id: 3, name: '王五', phone: '13700137000', address: '广州市天河区' }
      ],
      selectedCustomer: null
    }
  },
  methods: {
    selectCustomer(customer) {
      this.selectedCustomer = customer
    }
  }
}
</script>

<style scoped>
.left-content,
.right-content {
  padding: 20px;
  height: 100%;
  background: #f5f7fa;
  border-radius: 4px;
}

ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

li {
  padding: 10px;
  border-bottom: 1px solid #e4e7ed;
  cursor: pointer;
  
  &:hover {
    background: #ecf5ff;
  }
}
</style>

自定义初始宽度

<template>
  <div style="height: 600px;">
    <PanelSplitter :initial-left-width="50">
      <template #left>
        <div class="left-content">
          <h3>产品分类</h3>
          <!-- 产品分类列表 -->
        </div>
      </template>
      <template #right>
        <div class="right-content">
          <h3>产品详情</h3>
          <!-- 产品详情信息 -->
        </div>
      </template>
    </PanelSplitter>
  </div>
</template>

技术要点

1. 事件处理机制

  • 开始拖动:使用 @mousedown@touchstart 事件,设置 isResizing 为 true
  • 处理拖动:使用 @mousemove@touchmove 事件,实时计算并更新面板宽度
  • 结束拖动:使用 @mouseup@mouseleave@touchend 事件,设置 isResizing 为 false

2. 宽度计算

  • 使用 getBoundingClientRect() 获取容器的位置和尺寸
  • 根据鼠标/触摸位置计算相对于容器左边缘的距离
  • 将距离转换为宽度百分比,并限制在 20%-80% 之间

3. 视觉效果

  • 使用 cubic-bezier(0.25, 0.46, 0.45, 0.94) 缓动函数,实现平滑的过渡效果
  • 隐藏滚动条,提供更干净的视觉效果
  • 统一鼠标样式为 ew-resize,与专业组件保持一致

4. 组件设计

  • 使用 Vue 3 的 Composition API,代码结构清晰
  • 使用插槽机制,提高组件的灵活性和可复用性
  • 保持代码简洁,易于维护和扩展

实际应用效果

在政策管理系统中,PanelSplitter 组件已经成功应用于多个场景:

  1. 客户信息管理:用户可以根据客户列表的长度和详情的复杂度,自由调整左右面板的宽度,提高操作效率。

  2. 产品信息管理:对于不同类型的产品,用户可以调整面板宽度以适应不同的信息展示需求。

  3. 政策配置:在配置复杂政策时,用户可以增大右侧配置表单的宽度,方便填写和查看。

  4. 数据统计:在查看数据报表时,用户可以根据图表大小和筛选条件的复杂度,调整面板宽度。

性能优化

  1. 事件处理优化:使用事件委托,减少事件监听器数量
  2. 计算优化:避免在拖动过程中进行复杂计算
  3. 动画优化:使用 CSS transition,利用浏览器硬件加速
  4. 内存管理:确保组件卸载时清理事件监听器

扩展性

该组件可以通过以下方式进行扩展:

  1. 支持垂直分割:扩展为支持上下分割的面板
  2. 保存宽度配置:通过 localStorage 保存用户的宽度偏好
  3. 响应式断点:在小屏幕设备上自动调整布局
  4. 多面板支持:实现左中右三栏布局
  5. 拖拽提示:在拖拽过程中添加宽度百分比提示

总结

PanelSplitter 组件是一个解决前端左右布局宽度调整问题的通用解决方案。它通过流畅的拖拽体验、统一的视觉效果和高度的可配置性,为用户提供了更好的交互体验。

在实际项目中,该组件已经成功应用于多个场景,解决了布局调整的难题,为开发团队节省了大量的开发时间。同时,组件的模块化设计也提高了代码的可维护性和复用性。

适用场景

  • 管理系统:客户管理、产品管理、订单管理等
  • 数据分析:报表展示、数据可视化等
  • 内容编辑:左侧目录,右侧编辑区
  • 任何需要左右分栏布局的场景

技术栈

  • 前端框架:Vue 3 (Vue Demi)
  • 样式:SCSS
  • 构建工具:Vite / Webpack