背景
在前端开发中,左右分栏布局是一种常见的布局方式,尤其在管理系统中更为普遍。然而,固定宽度的布局往往无法满足所有用户的需求,不同屏幕尺寸和操作场景下,用户可能需要调整左右面板的宽度比例。
需求分析
在政策管理系统的开发过程中,我们遇到了以下需求:
- 客户信息管理:左侧客户查询列表,右侧客户详情确认
- 产品信息管理:左侧产品选择器,右侧产品详情和库存信息
- 政策配置:左侧政策类型列表,右侧具体政策配置表单
- 数据统计:左侧筛选条件,右侧图表展示
这些场景都需要一个通用的、流畅的拖拽调整宽度功能,以提升用户体验。
解决方案
经过分析,我们开发了一个通用的 PanelSplitter 组件,它具有以下特点:
- 流畅的拖拽体验:实时响应鼠标/触摸移动,无卡顿感
- 专业的视觉效果:统一的鼠标样式和平滑的过渡动画
- 高度可配置:支持自定义初始宽度,适应不同场景
- 易于集成:使用 Vue 的插槽机制,可轻松集成到任何项目中
- 跨设备兼容:同时支持鼠标和触摸事件
技术实现
核心设计
- 事件处理:使用鼠标和触摸事件实现拖拽功能
- 宽度计算:根据鼠标位置计算面板宽度比例
- 视觉效果:使用 CSS 过渡动画实现平滑效果
- 组件化:使用 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 组件已经成功应用于多个场景:
-
客户信息管理:用户可以根据客户列表的长度和详情的复杂度,自由调整左右面板的宽度,提高操作效率。
-
产品信息管理:对于不同类型的产品,用户可以调整面板宽度以适应不同的信息展示需求。
-
政策配置:在配置复杂政策时,用户可以增大右侧配置表单的宽度,方便填写和查看。
-
数据统计:在查看数据报表时,用户可以根据图表大小和筛选条件的复杂度,调整面板宽度。
性能优化
- 事件处理优化:使用事件委托,减少事件监听器数量
- 计算优化:避免在拖动过程中进行复杂计算
- 动画优化:使用 CSS transition,利用浏览器硬件加速
- 内存管理:确保组件卸载时清理事件监听器
扩展性
该组件可以通过以下方式进行扩展:
- 支持垂直分割:扩展为支持上下分割的面板
- 保存宽度配置:通过 localStorage 保存用户的宽度偏好
- 响应式断点:在小屏幕设备上自动调整布局
- 多面板支持:实现左中右三栏布局
- 拖拽提示:在拖拽过程中添加宽度百分比提示
总结
PanelSplitter 组件是一个解决前端左右布局宽度调整问题的通用解决方案。它通过流畅的拖拽体验、统一的视觉效果和高度的可配置性,为用户提供了更好的交互体验。
在实际项目中,该组件已经成功应用于多个场景,解决了布局调整的难题,为开发团队节省了大量的开发时间。同时,组件的模块化设计也提高了代码的可维护性和复用性。
适用场景
- 管理系统:客户管理、产品管理、订单管理等
- 数据分析:报表展示、数据可视化等
- 内容编辑:左侧目录,右侧编辑区
- 任何需要左右分栏布局的场景
技术栈
- 前端框架:Vue 3 (Vue Demi)
- 样式:SCSS
- 构建工具:Vite / Webpack