Vue+Mui实现列表侧滑删除功能实战项目
简介:本文介绍如何结合Vue.js框架与Material UI(Mui)组件库,实现一个具备列表侧滑删除功能的移动端友好交互组件。通过监听触摸事件(touchstart、touchmove、touchend),判断滑动距离是否超过阈值,动态显示删除确认区域,并利用Vue的组件通信机制$emit将删除事件传递至父组件,实现数据更新。项目结构清晰,组件可复用,适用于需要手势操作的列表管理场景,提升用户体验。
Vue + MUI 侧滑删除组件实战:从零搭建高可用移动端交互体系
你有没有遇到过这种情况?
用户在你的待办事项 App 里,轻轻一划,“唰”地一下把一条任务删了——动作行云流水、反馈即时明确。那一刻,他们嘴角微微上扬,仿佛不是在操作手机,而是在弹奏一首数字生活的轻快小调 🎶。
但背后的你呢?可能正盯着一堆 touchstart 、 touchmove 的事件监听发愁:“为什么滑动卡顿?”、“删除后列表闪了一下?”、“横屏时按钮错位了怎么办?”……
别急!今天我们就来手把手打造一个 高性能、可复用、跨设备兼容的 Vue + MUI 风格侧滑删除组件 ,彻底解决这些“看似简单却处处是坑”的移动端交互难题 💥。
我们不走寻常路,也不会堆砌术语让你头晕。相反,我会像一位老司机带你穿行技术丛林:有弯道提示 ⚠️,有风景打卡 🌄,还有隐藏彩蛋 🎁 —— 让你在不知不觉中掌握现代前端工程的核心逻辑。
准备好了吗?系好安全带,咱们出发!
项目初始化:不只是 vue create 那么简单
说到搭建 Vue 项目,很多人第一反应就是:
npm install -g @vue/cli
vue create my-awesome-app
然后一路回车搞定 ✅
But……等等!这真的是“最佳实践”吗?
🤔 想象一下:三个月后团队来了新人,打开
package.json发现一堆插件版本对不上;或者你想加个 TypeScript 支持,结果发现 CLI 当初没选上,现在要手动配置——是不是有点抓狂?
所以啊, 初始化不是目的,而是为未来铺路的过程 。我们需要的是一个既能快速启动,又能长期维护的脚手架。
手动预设才是王道
我建议永远选择 Manually select features (手动选择功能)。哪怕你现在只想要一个最简单的 SPA,也请给自己留条后路。
下面是我常用的配置清单👇:
| 功能 | 是否启用 | 说明 |
|---|---|---|
| Babel | ✅ | ES6+ 转译必备,别问为啥 |
| TypeScript | ✅(推荐) | 类型即文档,尤其适合中大型项目 |
| Router | ✅ | 单页应用导航中枢 |
| Pinia(或 Vuex) | ✅ | 状态管理不能少,优先选 Pinia ,更轻量更现代 |
| CSS Pre-processors | ✅(Sass/SCSS) | 写样式怎能没有变量和嵌套? |
| Linter / Formatter | ✅(ESLint + Prettier) | 团队协作神器,统一代码风格 |
| Unit Testing | ✅(Vitest) | 测试驱动开发从这里开始 |
| E2E Testing | ✅(Cypress 或 Playwright) | 自动化端到端测试,保障核心流程 |
执行命令如下:
vue create vue-mui-list-project
? Please pick a preset: Manually select features
? Check the features needed for your project:
◉ Choose Vue version
◉ Babel
◉ TypeScript
◉ Router
◉ Pinia
◉ CSS Pre-processors
◉ Linter / Formatter
◉ Unit Testing
◉ E2E Testing
生成后的 package.json 会自动包含所有必要的依赖,比如:
{
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.5",
"pinia": "^2.1.7"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"@vue/test-utils": "^2.4.0",
"sass": "^1.69.0",
"typescript": "~5.3.0",
"vitest": "^1.2.0",
"cypress": "^13.6.0"
}
}
看到没?这就是一个面向未来的项目骨架 🏗️。
目录结构设计:让代码自己说话
CLI 自动生成的目录已经不错了,但我们还可以让它更有“秩序感”。
默认结构长这样:
src/
├── assets/
├── components/
├── views/
├── App.vue
└── main.js
但我更喜欢进一步分层,尤其是当项目变大时:
src/
├── api/ # 接口请求封装
├── assets/ # 图片、字体等静态资源
├── components/ # 全局通用组件
│ ├── base/ # 原子组件:Button, Icon, Input
│ ├── layout/ # 布局容器:Header, Sidebar
│ └── list/ # 列表相关:ListItemSwipe, ListContainer
├── composables/ # Vue 3 Composition API 抽离逻辑
├── router/
├── store/ # Pinia 模块化状态
├── styles/ # 全局样式与 SCSS 变量
│ ├── _variables.scss
│ ├── _mixins.scss
│ └── global.scss
├── utils/ # 工具函数库
├── views/ # 页面级视图
└── main.ts # 入口文件(TS 版)
这样的结构有什么好处?
- 职责清晰 :一看就知道每个目录干啥的;
- 易于扩展 :新增模块不用到处找地方塞;
- 利于协作 :新人接手也能快速定位代码位置。
而且你会发现,随着功能增多,这种组织方式能帮你避免“所有东西都往 components/ 里扔”的混乱局面 😅。
构建配置扩展: vue.config.js 的妙用
虽然 Vue CLI 封装了 Webpack,但总有需要自定义的时候。这时候就得靠 vue.config.js 出马了。
举个真实场景🌰:本地开发时接口走 /api ,但实际部署在另一个服务上。怎么代理?
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
outputDir: 'dist',
assetsDir: 'static'
}
就这么几行,就能让前端请求 /api/users 自动转发到后端服务,省去 CORS 麻烦。
另外,如果你打算迁移到 Vite(强烈推荐!构建速度提升显著),也可以通过插件平滑过渡:
vue add vite
CLI 会自动替换底层构建工具,无需重写配置 👍。
Material UI 怎么集成?别被名字骗了!
先说个冷知识: Material UI(MUI)其实是 React 库 ❗
那我们在 Vue 项目里还能用吗?当然可以,但得讲究方法。
很多文章一上来就说:
npm install @mui/material @emotion/react @emotion/styled
然后就开始封装各种组件……听起来很专业对吧?
可问题是:你真的需要完整的 MUI 组件树吗?还是只是想要它的设计语言和配色规范?
更聪明的做法:提取设计 Token
与其硬搬 React 组件,不如把 MUI 的“灵魂”留下来——也就是它的 Design Tokens (设计标记)。
我们可以从 MUI 主题系统中提取关键变量,转换成 SCSS 格式,在 Vue 中全局使用。
创建文件 src/styles/_mui-theme.scss :
// Color Palette
$primary-main: #1976d2;
$primary-light: #42a5f5;
$primary-dark: #1565c0;
$secondary-main: #9c27b0;
$error-main: #d32f2f;
$warning-main: #ed6c02;
$success-main: #2e7d32;
// Typography
$font-family: "Roboto", "Helvetica", "Arial", sans-serif;
$font-size-base: 14px;
$line-height: 1.5;
// Spacing
$spacing-unit: 8px;
// Breakpoints
$breakpoint-xs: 0;
$breakpoint-sm: 600px;
$breakpoint-md: 960px;
$breakpoint-lg: 1280px;
$breakpoint-xl: 1920px;
然后在 main.ts 中引入:
import '@/styles/mui-theme.scss';
这样一来,整个项目的字体、颜色、间距都统一了,连设计师看了都会点头称赞 😎。
全局样式重置:告别浏览器差异
不同浏览器默认样式千奇百怪,比如 <button> 在 Safari 和 Chrome 上 padding 就不一样。
解决办法?用 normalize.css + 自定义规则打一套组合拳!
安装:
npm install normalize.css
创建 src/styles/global.scss :
@import 'normalize.css';
// 全局盒模型
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: $font-family;
font-size: $font-size-base;
line-height: $line-height;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img {
max-width: 100%;
height: auto;
}
// 表单元素继承字体
button,
input,
select,
textarea {
font: inherit;
}
最后在 App.vue 中加载:
<style lang="scss">
@import './styles/global.scss';
</style>
这套机制就像城市的交通规则——谁都不能乱来,大家各行其道,才能高效通行 🚦。
列表组件设计:数据驱动才是王道
我们常说“UI 是数据的投影”,这句话在列表渲染中最明显不过。
设想这样一个任务列表:
const tasks = [
{ id: 1, title: '整理会议纪要', completed: false },
{ id: 2, title: '回复客户邮件', completed: true },
{ id: 3, title: '更新产品文档', completed: false }
];
对应的模板该怎么写?
<template>
<ul class="task-list">
<li
v-for="task in tasks"
:key="task.id"
class="task-item"
:class="{ completed: task.completed }"
>
{{ task.title }}
</li>
</ul>
</template>
看起来很简单?但背后藏着几个关键点 ⚡
为什么要用 :key ?搞懂 Vue 的 diff 算法
Vue 更新 DOM 的时候,并不会每次都重建全部节点。它有一套高效的 patching 算法 ,核心就是靠 key 来识别哪个元素对应哪个数据。
来看两个例子对比:
✅ 正确做法:
<li v-for="item in items" :key="item.id">...</li>
❌ 错误做法:
<li v-for="(item, index) in items" :key="index">...</li>
区别在哪?
假设原始数组是 [A, B, C] ,现在变成 [D, A, B, C] 。
- 如果用
index作 key,Vue 会觉得: - index=0 还是 A → 不变
- index=1 还是 B → 不变
- ……但实际上 A 已经往后移了一位!
于是 Vue 会傻乎乎地更新每一个节点,性能暴跌 📉。
- 而如果用唯一 ID 作 key,Vue 就知道:
- A、B、C 都还在,只是前面多了个 D;
- 只需插入新节点,其余复用即可。
这才是真正的“最小化重绘”。
所以记住一句话: 永远不要用 index 作为 v-for 的 key,除非你能保证顺序永不改变 。
数据抽象层级:让组件更健壮
一个好的列表组件,应该遵循分层原则:
| 层级 | 职责 |
|---|---|
| Model Layer | 定义数据结构(接口、字段含义) |
| Service Layer | 获取/更新数据(API 调用) |
| View Layer | 渲染 UI,绑定事件 |
| Component Layer | 封装交互逻辑(如侧滑) |
这样做有什么好处?
- 解耦 :数据变了不影响 UI 结构;
- 可测 :每一层都可以独立测试;
- 可维护 :改样式不影响业务逻辑。
比如你可以先把 TaskItem 做成纯展示组件,再在外面包一层 SwipeToDeleteItem 加手势支持——层层递进,清晰明了。
触摸事件监听:指尖上的舞蹈
终于到了激动人心的部分——如何让手指在屏幕上“跳舞”时,页面也能跟着节奏起舞?
移动端的交互基础,全靠这三个原生事件:
touchstart:手指按下touchmove:手指滑动touchend:手指抬起
它们的关系就像是交响乐的三个乐章 🎻。
生命周期详解:一次触摸旅程
sequenceDiagram
participant User
participant Browser
User->>Browser: 手指按下 (Touch Start)
Browser->>Handler: 触发 touchstart 事件
loop 持续滑动
User->>Browser: 手指移动 (Touch Move)
Browser->>Handler: 触发 touchmove 事件(多次)
end
User->>Browser: 手指抬起 (Touch End)
Browser->>Handler: 触发 touchend 事件
整个过程必须完整闭环,任何一个环节断掉,体验就会打折。
实战调试组件
为了看清事件细节,我们可以做一个简易调试器:
<template>
<div class="touch-debugger" @touchstart="onStart" @touchmove="onMove" @touchend="onEnd">
<p>请在此区域滑动查看日志👇</p>
<pre>{{ log }}</pre>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const log = ref('');
function onStart(e: TouchEvent) {
const t = e.touches[0];
log.value += `Start: (${t.clientX}, ${t.clientY})\n`;
}
function onMove(e: TouchEvent) {
if (e.touches.length > 1) return; // 忽略多点触控
const t = e.touches[0];
log.value += `Move: (${t.clientX}, ${t.clientY})\n`;
e.preventDefault(); // 阻止页面滚动
}
function onEnd(e: TouchEvent) {
const t = e.changedTouches[0];
log.value += `End: (${t.clientX}, ${t.clientY})\n`;
}
</script>
运行起来后,随便滑两下,就能看到坐标变化轨迹。这对排查“为什么滑不动”、“方向判断不准”等问题特别有用 🔍。
关键属性解析:clientX vs pageX
在计算滑动距离时,该用哪个坐标?
| 属性 | 含义 | 使用场景 |
|---|---|---|
clientX |
相对于视口左边缘 | 推荐!不受页面滚动影响 |
pageX |
相对于文档左边缘 | 适用于绝对定位 |
screenX |
相对于屏幕左边缘 | 很少用 |
举个例子🌰:页面已向下滚动 300px,某元素位于文档 Y=500px 处。
此时点击该元素:
- clientY = 200
- pageY = 500
由于我们关心的是“相对位移”,所以用 clientX 更稳定。
滑动逻辑实现:精准捕捉每一次拖拽
光有事件还不够,还得把它们串成一套流畅的动作流。
事件绑定方式:模板 vs 编程式
在 Vue 中有两种常见方式:
方式一:模板指令(推荐)
<div
@touchstart="handleStart"
@touchmove.prevent="handleMove"
@touchend="handleEnd"
/>
优点:简洁直观, .prevent 还能阻止默认行为。
方式二:编程式监听
onMounted(() => {
el.addEventListener('touchstart', start);
});
onBeforeUnmount(() => {
el.removeEventListener('touchstart', start);
});
优点:灵活控制生命周期,适合动态注册。
我建议: 以模板为主,必要时补充编程式控制 。
起始位置记录与实时追踪
这是滑动的核心逻辑:
data() {
return {
startX: 0,
currentX: 0,
isSwiping: false
};
},
methods: {
handleStart(e) {
this.startX = e.touches[0].clientX;
this.isSwiping = true;
},
handleMove(e) {
if (!this.isSwiping) return;
this.currentX = e.touches[0].clientX;
const deltaX = this.currentX - this.startX;
this.$el.style.transform = `translateX(${Math.min(deltaX, 0)}px)`;
},
handleEnd() {
this.isSwiping = false;
// 判断是否触发删除
}
}
注意这里用了 Math.min(deltaX, 0) ,确保只能向左滑动(负值),符合直觉。
性能优化:节流与 requestAnimationFrame
频繁的 touchmove 会导致大量 DOM 操作,影响帧率。
解决方案有两个:
- 节流(throttle)
import { throttle } from 'lodash-es';
handleMove: throttle(function(e) {
// 更新逻辑
}, 16) // ~60fps
- requestAnimationFrame
let ticking = false;
function updatePosition(deltaX) {
this.$el.style.transform = `translateX(${deltaX}px)`;
}
function handleMove(e) {
const deltaX = e.touches[0].clientX - this.startX;
if (!ticking) {
requestAnimationFrame(() => {
updatePosition.call(this, Math.min(deltaX, 0));
ticking = false;
});
ticking = true;
}
}
后者更适合动画场景,能更好地匹配浏览器刷新节奏。
删除逻辑控制:让用户安心地“破坏”
删除是个危险操作,必须谨慎对待。
阈值判定:防止误触
设定一个最小滑动距离(比如 80px),只有超过才算有效操作。
const THRESHOLD = 80;
handleEnd() {
const absDelta = Math.abs(this.currentX);
if (absDelta >= THRESHOLD) {
this.showDeleteButton = true;
this.isLocked = true; // 锁定状态
} else {
this.resetPosition();
}
}
加上 isLocked 标志位,防止动画过程中被重复触发。
状态流转图
graph TD
A[touchstart] --> B{记录起始X}
B --> C[touchmove]
C --> D[计算deltaX]
D --> E{abs(deltaX) >= threshold?}
E -- 否 --> F[更新currentX, 显示正常状态]
E -- 是 --> G[显示删除按钮]
G --> H[touchend]
H --> I{abs(currentX) >= threshold?}
I -- 是 --> J[锁定滑动, 固定位移]
I -- 否 --> K[恢复原位, 解锁]
J --> L[等待用户点击确认]
K --> M[结束流程]
这个流程确保了每一步都有据可依,不会出现“莫名其妙删了”的情况。
交互反馈设计:不只是动起来那么简单
好的反馈能让用户感到“我在掌控一切”。
transition 动画封装
用 Vue 内置 <transition> 组件轻松实现按钮入场动画:
<transition name="fade-slide">
<div v-if="showDelete" class="delete-btn" @click="confirmDelete">删除</div>
</transition>
CSS:
.fade-slide-enter-active {
opacity: 0;
transform: translateX(100%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-slide-enter-to {
opacity: 1;
transform: translateX(0);
}
Material Design 推荐的缓动函数,手感超顺滑~
背景色渐变警示
随着滑动加深,背景逐渐变红,提醒用户即将执行危险操作:
handleMove(e) {
const deltaX = e.touches[0].clientX - this.startX;
const progress = Math.min(Math.abs(deltaX) / THRESHOLD, 1);
this.$el.classList.toggle('warning', progress > 0.6);
}
.swipe-item.warning {
background-color: #ffebee; /* Red 50 */
}
视觉层次一下子就出来了!
组件通信与响应式更新:打通最后一公里
子组件删了数据,父组件怎么知道?
答案是: $emit + filter 。
子组件通知删除
handleEnd() {
if (Math.abs(this.currentX) >= this.threshold) {
this.$emit('delete-item', this.id);
} else {
this.reset();
}
}
父组件处理并更新
methods: {
handleDelete(id) {
this.items = this.items.filter(item => item.id !== id);
}
}
✅
filter返回新数组,Vue 会检测引用变化并重新渲染。
移动端适配实战:让每个设备都舒服
最后一步,适配各种屏幕。
Flexbox 弹性布局
.slide-list-item {
display: flex;
overflow: hidden;
}
.content-wrapper {
flex: 1;
transition: transform 0.3s ease-out;
}
.action-buttons {
width: 80px;
background: red;
}
内容区自适应,操作区固定宽度,完美!
rem + viewport 适配方案
<meta name="viewport" content="width=device-width, initial-scale=1.0">
JS 动态设置根字体:
function setRootFontSize() {
const width = document.documentElement.clientWidth;
document.documentElement.style.fontSize = (width / 375) * 100 + 'px';
}
window.addEventListener('resize', setRootFontSize);
setRootFontSize();
从此一套样式通吃所有机型 📱。
总结:我们到底构建了一个什么?
回顾整个流程,我们完成的不仅仅是一个“侧滑删除”功能,而是一整套 现代化前端开发范式 :
- ✅ 项目初始化规范
- ✅ 设计系统统一
- ✅ 组件分层架构
- ✅ 手势交互实现
- ✅ 动画反馈机制
- ✅ 响应式数据流
- ✅ 多端适配策略
这套体系完全可以复制到其他复杂交互场景中,比如拖拽排序、长按菜单、双指缩放等。
更重要的是,它教会我们一种思维方式: 把复杂问题拆解成可控的小模块,逐个击破,最终组装成强大的产品能力 。
下次当你面对一个新的交互需求时,不妨问问自己:
“我能把它分解成哪些原子动作?每个动作的状态如何管理?数据流向是否清晰?”
一旦掌握了这种工程化思维,你就不再是“写代码的人”,而是“构建系统的人”🚀。
🎉 恭喜你读到这里!现在就去试试吧~
把这篇笔记变成你的第一个高保真侧滑组件,然后骄傲地告诉全世界:“看,这是我做的!” 💪
简介:本文介绍如何结合Vue.js框架与Material UI(Mui)组件库,实现一个具备列表侧滑删除功能的移动端友好交互组件。通过监听触摸事件(touchstart、touchmove、touchend),判断滑动距离是否超过阈值,动态显示删除确认区域,并利用Vue的组件通信机制$emit将删除事件传递至父组件,实现数据更新。项目结构清晰,组件可复用,适用于需要手势操作的列表管理场景,提升用户体验。
更多推荐


所有评论(0)