本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文介绍如何结合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 操作,影响帧率。

解决方案有两个:

  1. 节流(throttle)
import { throttle } from 'lodash-es';

handleMove: throttle(function(e) {
  // 更新逻辑
}, 16) // ~60fps
  1. 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();

从此一套样式通吃所有机型 📱。


总结:我们到底构建了一个什么?

回顾整个流程,我们完成的不仅仅是一个“侧滑删除”功能,而是一整套 现代化前端开发范式

  • ✅ 项目初始化规范
  • ✅ 设计系统统一
  • ✅ 组件分层架构
  • ✅ 手势交互实现
  • ✅ 动画反馈机制
  • ✅ 响应式数据流
  • ✅ 多端适配策略

这套体系完全可以复制到其他复杂交互场景中,比如拖拽排序、长按菜单、双指缩放等。

更重要的是,它教会我们一种思维方式: 把复杂问题拆解成可控的小模块,逐个击破,最终组装成强大的产品能力

下次当你面对一个新的交互需求时,不妨问问自己:

“我能把它分解成哪些原子动作?每个动作的状态如何管理?数据流向是否清晰?”

一旦掌握了这种工程化思维,你就不再是“写代码的人”,而是“构建系统的人”🚀。


🎉 恭喜你读到这里!现在就去试试吧~
把这篇笔记变成你的第一个高保真侧滑组件,然后骄傲地告诉全世界:“看,这是我做的!” 💪

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文介绍如何结合Vue.js框架与Material UI(Mui)组件库,实现一个具备列表侧滑删除功能的移动端友好交互组件。通过监听触摸事件(touchstart、touchmove、touchend),判断滑动距离是否超过阈值,动态显示删除确认区域,并利用Vue的组件通信机制$emit将删除事件传递至父组件,实现数据更新。项目结构清晰,组件可复用,适用于需要手势操作的列表管理场景,提升用户体验。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐