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

背景

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

需求分析

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

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

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

解决方案

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

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

技术实现

核心设计

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

完整代码

vue 复制代码
<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>

使用方法

基本用法

vue 复制代码
<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>

自定义初始宽度

vue 复制代码
<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

相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60615 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen8 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马9 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒9 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端