Vant 4 实战教程:Vue3 移动端后台管理系统从选型到开发|Vue生态精选篇

【Vant + Vue3】移动端后台开发:从组件选型、按需引入到双端路由实战,彻底搞懂 H5 管理页与审批流的最佳写法,避开列表重置、安全区适配等高频坑!

📑 文章目录

  • [一、为什么要单独聊 Vant 在「后台」里的使用?](#一、为什么要单独聊 Vant 在「后台」里的使用?)
  • [二、概念扫盲:Vant 是什么,和 Element 有啥区别?](#二、概念扫盲:Vant 是什么,和 Element 有啥区别?)
    • [2.1 快速对比](#2.1 快速对比)
    • [2.2 常见误解](#2.2 常见误解)
  • [三、Vant 4 vs Vant 3:如何选择?](#三、Vant 4 vs Vant 3:如何选择?)
    • [3.1 核心区别](#3.1 核心区别)
    • [3.2 选择建议](#3.2 选择建议)
  • 四、实战场景一:简单的移动端管理页
    • [4.1 需求描述](#4.1 需求描述)
    • [4.2 项目初始化](#4.2 项目初始化)
    • [4.3 按需引入:unplugin-vue-components 配置(推荐)](#4.3 按需引入:unplugin-vue-components 配置(推荐))
    • [4.4 手动按需引入(不装插件时)](#4.4 手动按需引入(不装插件时))
    • [4.5 完整页面示例](#4.5 完整页面示例)
  • 五、实战场景二:审批流页面
    • [5.1 需求描述](#5.1 需求描述)
    • [5.2 审批详情页示例](#5.2 审批详情页示例)
  • [六、移动端 + PC 双端路由区分实战](#六、移动端 + PC 双端路由区分实战)
    • [6.1 思路](#6.1 思路)
    • [6.2 设备检测工具](#6.2 设备检测工具)
    • [6.3 路由结构](#6.3 路由结构)
    • [6.4 路由配置示例](#6.4 路由配置示例)
    • [6.5 简要说明](#6.5 简要说明)
  • 七、日常开发中的常见坑与规范
    • [7.1 列表重置时机](#7.1 列表重置时机)
    • [7.2 Toast / Dialog 使用方式](#7.2 Toast / Dialog 使用方式)
    • [7.3 安全区适配](#7.3 安全区适配)
    • [7.4 与 PC 后台共存](#7.4 与 PC 后台共存)
    • [7.5 按需引入 vs 全量引入](#7.5 按需引入 vs 全量引入)
  • 八、小结
  • [🔍 本系列专栏导航](#🔍 本系列专栏导航)

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱「面向搜索引擎写代码」的尴尬。


一、为什么要单独聊 Vant 在「后台」里的使用?

一说后台,大家会想到 Element Plus、Ant Design Vue。但在很多企业里,还有这种需求:

  • 领导出差时用手机审批单据
  • 运维在机房用手机查看监控、处理告警
  • 门店店员用手机完成盘点、退货等操作

这些本质上都是后台逻辑 ,只是终端是手机 / 平板 。Element 这种 PC 端 UI 库在小屏上体验不好,所以需要移动端组件库,而 Vant 就是 Vue 生态里最常用的一个。

一句话:Vant = 面向移动端的 Vue 组件库,适合移动管理页、H5 审批页等「轻量后台」场景。

[⬆ 返回目录](#⬆ 返回目录)


二、概念扫盲:Vant 是什么,和 Element 有啥区别?

2.1 快速对比

维度 Element Plus Vant
定位 PC 端管理后台 移动端 / H5
适配 大屏 小屏、触摸
典型场景 复杂表格、多级菜单 列表、表单、弹窗、上拉加载

[⬆ 返回目录](#⬆ 返回目录)

2.2 常见误解

误解 1:Vant 只能做 C 端页面?

不是。审批流、数据查询、简单增删改查,都可以用 Vant,只是交互要按移动端习惯来设计。

误解 2:后台必须全用 Element?

不一定。PC 用 Element,移动端用 Vant,可以并存,路由上区分即可。

误解 3:Vant 只能做 H5,不能做 App?

Vant 本身是 Web 组件,用 UniApp、Taro 等可以打包成 App,但本文只讨论「H5 管理页」这种典型场景。

[⬆ 返回目录](#⬆ 返回目录)


三、Vant 4 vs Vant 3:如何选择?

3.1 核心区别

维度 Vant 3 Vant 4
Vue 版本 Vue 3 Vue 3
包体积 约 7MB 安装体积 约 5MB,减少约 30%
深色模式 需自行实现 原生支持
Nuxt / SSR 支持一般 对 Nuxt 3 支持更好
新组件 - Skeleton 子组件、PickerGroup、DatePicker、TimePicker、BackTop 等
主题定制 灵活 700+ 主题变量,主色统一为 #1989fa
TypeScript 支持 类型更完整

[⬆ 返回目录](#⬆ 返回目录)

3.2 选择建议

  • 新项目:建议直接用 Vant 4,体积更小,功能更全。
  • 老项目:Vant 3 稳定运行的情况下不必强迁,可按需求迭代;若用到深色模式、Nuxt 3 等,再考虑升级。
  • Vant 3 → 4:官方有升级指南,整体迁移路径较清晰。

[⬆ 返回目录](#⬆ 返回目录)


四、实战场景一:简单的移动端管理页

4.1 需求描述

  • 一个「设备管理」H5 页
  • 顶部搜索 + 筛选
  • 列表展示设备,支持上拉加载
  • 点击某行进入详情

[⬆ 返回目录](#⬆ 返回目录)

4.2 项目初始化

bash 复制代码
# 创建 Vue 3 项目
npm create vue@latest vant-admin-h5

# 进入项目后安装 Vant
npm i vant

[⬆ 返回目录](#⬆ 返回目录)

4.3 按需引入:unplugin-vue-components 配置(推荐)

生产项目建议用 @vant/auto-import-resolver + unplugin-vue-components,自动按需引入组件和样式,减小包体积。

安装依赖:

bash 复制代码
npm i @vant/auto-import-resolver unplugin-vue-components unplugin-auto-import -D

Vite 配置示例:

javascript 复制代码
// vite.config.js 或 vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from '@vant/auto-import-resolver'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [VantResolver()],
      dts: 'src/auto-imports.d.ts', // 生成类型声明
    }),
    Components({
      resolvers: [
        VantResolver({
          importStyle: true,  // 自动引入样式,默认 true
          // exclude: ['Button'], // 排除某些组件
        }),
      ],
      dts: 'src/components.d.ts', // 生成组件类型声明
    }),
  ],
})

可选配置:

  • importStyle:是否自动引入样式,默认 true
  • exclude:不自动引入的组件或 API
  • module'esm''cjs',默认 'esm'

配置完成后,模板中可直接使用 Vant 组件,无需手动 import

html 复制代码
<template>
  <van-button type="primary">按钮</van-button>
</template>
<!-- 无需在 script 中 import -->

函数式 API 也会自动引入:

javascript 复制代码
showToast('操作成功')
await showConfirmDialog({ title: '确认', message: '确定删除?' })

Vue CLI 项目示例:

javascript 复制代码
// vue.config.js
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { VantResolver } = require('@vant/auto-import-resolver')

module.exports = {
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [VantResolver()],
      }),
      Components({
        resolvers: [VantResolver()],
      }),
    ],
  },
}

[⬆ 返回目录](#⬆ 返回目录)

4.4 手动按需引入(不装插件时)

javascript 复制代码
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { Search, Field, Cell, CellGroup, List, Empty, Toast, showToast } from 'vant'
import 'vant/lib/index.css'

const app = createApp(App)
app.use(Search).use(Field).use(Cell).use(CellGroup).use(List).use(Empty).use(Toast)
app.mount('#app')

[⬆ 返回目录](#⬆ 返回目录)

4.5 完整页面示例

html 复制代码
<template>
  <div class="device-list-page">
    <van-search
      v-model="keyword"
      placeholder="请输入设备名称或编号"
      show-action
      @search="onSearch"
      @cancel="onCancel"
    />

    <van-dropdown-menu>
      <van-dropdown-item v-model="status" :options="statusOptions" @change="onFilterChange" />
    </van-dropdown-menu>

    <van-list
      v-model:loading="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <van-cell
        v-for="item in list"
        :key="item.id"
        :title="item.name"
        :label="item.code"
        is-link
        @click="goDetail(item.id)"
      />
      <van-empty v-if="!loading && list.length === 0" description="暂无设备" />
    </van-list>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'

const router = useRouter()
const keyword = ref('')
const status = ref('')
const list = ref([])
const loading = ref(false)
const finished = ref(false)
const page = ref(1)
const pageSize = 10

const statusOptions = [
  { text: '全部状态', value: '' },
  { text: '正常', value: 'normal' },
  { text: '维修中', value: 'repair' },
  { text: '停用', value: 'disabled' },
]

const fetchList = async () => {
  const res = await fetch(
    `/api/devices?keyword=${keyword.value}&status=${status.value}&page=${page.value}&pageSize=${pageSize}`
  )
  return res.json()
}

const onLoad = async () => {
  loading.value = true
  try {
    const { data } = await fetchList()
    list.value = page.value === 1 ? data : [...list.value, ...data]
    if (data.length < pageSize) finished.value = true
    page.value++
  } catch (e) {
    showToast('加载失败')
    finished.value = true
  } finally {
    loading.value = false
  }
}

const onSearch = () => {
  page.value = 1
  list.value = []
  finished.value = false
  onLoad()
}

const onCancel = () => {
  keyword.value = ''
  onSearch()
}

const onFilterChange = () => {
  page.value = 1
  list.value = []
  finished.value = false
  onLoad()
}

const goDetail = (id) => {
  router.push({ name: 'DeviceDetail', params: { id } })
}
</script>

<style scoped>
.device-list-page {
  padding-bottom: 50px;
}
</style>

说明要点:

  1. van-search@search@cancel 都需触发列表刷新
  2. van-dropdown-menu:筛选变更时重置 pagelistfinished
  3. van-list:通过 loadingfinished@load 控制上拉加载
  4. van-empty:无数据时展示,避免空白

[⬆ 返回目录](#⬆ 返回目录)


五、实战场景二:审批流页面

5.1 需求描述

  • 待审批列表
  • 进入详情可查看单据信息、附件
  • 操作:通过 / 驳回,驳回需填写原因

[⬆ 返回目录](#⬆ 返回目录)

5.2 审批详情页示例

html 复制代码
<template>
  <div class="approval-detail-page">
    <van-cell-group inset>
      <van-cell title="申请单号" :value="detail.sn" />
      <van-cell title="申请人" :value="detail.applicant" />
      <van-cell title="申请时间" :value="detail.createTime" />
      <van-cell title="申请事由" :value="detail.reason" />
    </van-cell-group>

    <van-cell title="附件" v-if="detail.files?.length">
      <template #right-icon>
        <van-button size="small" type="primary" plain @click="previewFiles">查看</van-button>
      </template>
    </van-cell>

    <div class="action-bar">
      <van-button block type="danger" @click="showRejectDialog = true">驳回</van-button>
      <van-button block type="primary" @click="approve">通过</van-button>
    </div>

    <van-dialog
      v-model:show="showRejectDialog"
      title="驳回原因"
      show-cancel-button
      :before-close="beforeRejectClose"
    >
      <van-field
        v-model="rejectReason"
        type="textarea"
        rows="4"
        placeholder="请输入驳回原因(必填)"
      />
    </van-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { showToast, showSuccessToast } from 'vant'

const route = useRoute()
const id = route.params.id

const detail = ref({})
const showRejectDialog = ref(false)
const rejectReason = ref('')

const fetchDetail = async () => {
  const res = await fetch(`/api/approvals/${id}`)
  detail.value = await res.json()
}

const approve = async () => {
  try {
    await fetch(`/api/approvals/${id}/approve`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'approve' }),
    })
    showSuccessToast('审批通过')
    setTimeout(() => history.back(), 1000)
  } catch (e) {
    showToast('操作失败')
  }
}

const beforeRejectClose = (action) => {
  if (action === 'confirm') {
    if (!rejectReason.value.trim()) {
      showToast('请填写驳回原因')
      return false
    }
    doReject()
  }
  return true
}

const doReject = async () => {
  try {
    await fetch(`/api/approvals/${id}/reject`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        action: 'reject',
        reason: rejectReason.value.trim(),
      }),
    })
    showSuccessToast('已驳回')
    setTimeout(() => history.back(), 1000)
  } catch (e) {
    showToast('操作失败')
  }
}

const previewFiles = () => {
  showToast('附件预览功能')
}

onMounted(fetchDetail)
</script>

<style scoped>
.approval-detail-page {
  padding-bottom: 70px;
}

.action-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  gap: 12px;
  padding: 12px 16px;
  background: #fff;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}
</style>

说明要点:

  1. van-dialog + before-close:点击确认时校验,返回 false 阻止关闭
  2. 底部操作栏:position: fixed,内容区加 padding-bottom 防遮挡
  3. 安全区:padding-bottom: env(safe-area-inset-bottom)

[⬆ 返回目录](#⬆ 返回目录)


六、移动端 + PC 双端路由区分实战

6.1 思路

  • User-Agent 区分手机/平板和 PC
  • 为 PC 和移动端分别建路由表
  • 根路径或指定路径根据设备重定向到对应端

[⬆ 返回目录](#⬆ 返回目录)

6.2 设备检测工具

javascript 复制代码
// utils/device.js

/**
 * 判断是否为移动端(含平板)
 */
export function isMobile() {
  const ua = navigator.userAgent
  return /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(ua)
}

/**
 * 获取终端类型
 */
export function getTerminalType() {
  return isMobile() ? 'mobile' : 'pc'
}

[⬆ 返回目录](#⬆ 返回目录)

6.3 路由结构

复制代码
src/
├── router/
│   └── index.js
├── views/
│   ├── pc/           # PC 端页面(Element)
│   │   ├── Home.vue
│   │   ├── DeviceList.vue
│   │   └── ApprovalList.vue
│   └── mobile/       # 移动端页面(Vant)
│       ├── Home.vue
│       ├── DeviceList.vue
│       └── ApprovalDetail.vue

[⬆ 返回目录](#⬆ 返回目录)

6.4 路由配置示例

javascript 复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { isMobile } from '@/utils/device'

// PC 端布局(带侧边栏等)
const PCLayout = () => import('@/layouts/PCLayout.vue')
// 移动端布局(简单导航)
const MobileLayout = () => import('@/layouts/MobileLayout.vue')

// PC 端路由
const pcRoutes = [
  {
    path: '/pc',
    component: PCLayout,
    children: [
      { path: '', name: 'PCHome', component: () => import('@/views/pc/Home.vue') },
      { path: 'devices', name: 'PCDeviceList', component: () => import('@/views/pc/DeviceList.vue') },
      { path: 'approvals', name: 'PCApprovalList', component: () => import('@/views/pc/ApprovalList.vue') },
    ],
  },
]

// 移动端路由
const mobileRoutes = [
  {
    path: '/mobile',
    component: MobileLayout,
    children: [
      { path: '', name: 'MobileHome', component: () => import('@/views/mobile/Home.vue') },
      { path: 'devices', name: 'MobileDeviceList', component: () => import('@/views/mobile/DeviceList.vue') },
      { path: 'approvals/:id', name: 'MobileApprovalDetail', component: () => import('@/views/mobile/ApprovalDetail.vue') },
    ],
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', redirect: () => (isMobile() ? '/mobile' : '/pc') },
    ...pcRoutes,
    ...mobileRoutes,
  ],
})

// 路由守卫:防止跨端访问
router.beforeEach((to, from, next) => {
  const mobile = isMobile()

  // 根路径已在 routes 里做 redirect
  if (to.path === '/') return next()

  const toPC = to.path.startsWith('/pc')
  const toMobile = to.path.startsWith('/mobile')

  if (mobile && toPC) {
    // 手机访问 PC 路由 → 重定向到移动端对应页
    const map = { '/pc': '/mobile', '/pc/devices': '/mobile/devices', '/pc/approvals': '/mobile' }
    next(map[to.path] || '/mobile')
  } else if (!mobile && toMobile) {
    // PC 访问移动端路由 → 重定向到 PC 对应页
    const map = { '/mobile': '/pc', '/mobile/devices': '/pc/devices', '/mobile/approvals': '/pc/approvals' }
    next(map[to.path] || '/pc')
  } else {
    next()
  }
})

export default router

[⬆ 返回目录](#⬆ 返回目录)

6.5 简要说明

  • PC 和移动端各自一套路由,可共用接口和业务逻辑
  • 通过 redirectbeforeEach 做设备自动跳转
  • 可用 m.xxx.com 单独部署 H5,或与 PC 同域用路径区分

[⬆ 返回目录](#⬆ 返回目录)


七、日常开发中的常见坑与规范

7.1 列表重置时机

切换筛选项或搜索时,必须:

  1. page = 1
  2. list = []
  3. finished = false

否则会新旧数据混合,或 van-list 不再触发 @load

[⬆ 返回目录](#⬆ 返回目录)

7.2 Toast / Dialog 使用方式

javascript 复制代码
import { showToast, showConfirmDialog } from 'vant'

showToast('操作成功')
await showConfirmDialog({ title: '确认', message: '确定删除?' })

优先用函数式调用,而不是在 template 里用 <van-toast /> 手动控制。

[⬆ 返回目录](#⬆ 返回目录)

7.3 安全区适配

有底部固定栏时建议加安全区:

css 复制代码
.action-bar {
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}
  • constant(safe-area-inset-bottom):兼容 iOS 11.0--11.2
  • env(safe-area-inset-bottom):兼容 iOS 11.3+ 及现代浏览器

[⬆ 返回目录](#⬆ 返回目录)

7.4 与 PC 后台共存

  • PC 用 Element,移动端用 Vant
  • 通过 User-Agent 或独立域名(如 m.xxx.com)区分
  • 共用接口和业务逻辑

[⬆ 返回目录](#⬆ 返回目录)

7.5 按需引入 vs 全量引入

  • 生产环境:优先用 unplugin-vue-components + @vant/auto-import-resolver 按需引入
  • 学习/演示:可全量引入,但正式项目不推荐

[⬆ 返回目录](#⬆ 返回目录)


八、小结

场景 常用组件 注意点
移动端管理列表 Search、DropdownMenu、List、Cell、Empty 搜索/筛选时重置分页和列表
审批流 CellGroup、Dialog、Field、Button 驳回前校验必填,底部栏留安全区
双端路由 - User-Agent 检测 + 路由守卫重定向

Vant 在后台里的角色很清晰:不是替代 Element,而是负责移动端部分。选好 Vant 版本、按需引入、理清双端路由,就能更顺畅地做移动端管理页和审批页。

[⬆ 返回目录](#⬆ 返回目录)

🔍 本系列专栏导航

一、《Vue3 中后台实战:Element Plus 的"企业后台常用组件"用法扫盲|Vue生态精选篇》
二、《Vue3 中后台实战:Element + VXE Table 搜索表格分页完整方案|Vue生态精选篇》
三、《Vue3 中后台实战:VXE-Table 从基础表格到复杂业务表格全攻略|Vue生态精选篇》
四、《Vue3 表格封装实战:列配置 + slot 扩展 + 请求生命周期|Vue生态精选篇》

五、《Vant 4 实战教程:Vue3 移动端后台管理系统从选型到开发|Vue生态精选篇》
六、《Excel 上传解析 + 导出实战:Vue+xlsx 避坑指南|Vue生态精选篇》
七、《Vue3 + Element Plus 全局 Message、Notification 封装与规范|Vue生态精选篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

[⬆ 返回目录](#⬆ 返回目录)


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

相关推荐
AlienZHOU2 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
刺客码5 小时前
Layui 表格固定列行高错位问题解决方案
前端框架·layui·jquery·web
Captaincc5 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师7 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen7 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒8 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow8 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8188 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800788 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明8 小时前
第 6 章 组件进阶
前端·vue.js