ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Vue3与Ant Design Vue的中后台管理系统工程化实践

基于Vue3与Ant Design Vue的中后台管理系统工程化实践 简介基于Vue3与Ant Design Vue构建的中后台管理系统项目包面向具备一定Vue基础、希望系统掌握企业级管理界面开发与工程化实践的开发者。压缩包共304个文件包含95个JavaScript逻辑文件、72个CSS样式文件、71个Vue组件文件以及依赖压缩包、字体与图标资源等整体仅3.71MB结构紧凑适合直接导入本地工程研读或二次开发。项目中完整呈现了Vue3组合式API、ref/reactive响应式处理、Teleport弹层渲染、Suspense异步组件加载等核心特性并结合Ant Design Vue的表单、表格、通知等组件延伸出路由管理、状态管理、API请求与Mock数据、按需加载、代码分割、Eslint/Prettier代码规范等实践方法同时涵盖主题定制、国际化、无障碍访问等企业级场景可帮助读者理解组件封装逻辑与中后台项目的组织方式。已有1044人学习下载适合对照源码快速上手Vue3后台开发是一份兼具学习与参考价值的实战资源。1. 基于 vue3 ant-design-vue 的中后台管理系统为什么值得自己搭一套中后台管理系统大概是前端领域里需求最统一、重复度最高的项目类型左侧菜单、顶部栏、表格页、表单页、权限控制、数据看板。正因为它高度同质化选型反而比业务代码更影响长期维护成本。vue3 的组合式 API 让逻辑复用不再依赖 mixin 那套容易命名冲突的机制ant-design-vue 则把表格、表单、弹窗这类高频组件的交互细节封装得比较完整两者组合出来的工程能满足多数中后台场景的交付速度要求也能在业务复杂化之后继续撑住。但这个标题里真正值钱的不是“vue3”也不是“ant-design-vue”而是“中后台管理系统”这七个字背后的工程化整合环境怎么搭、路由怎么根据权限生成、请求层怎么统一错误处理、表格和表单怎么降低维护成本。这些能力分散在多个依赖里官方文档各讲各的没人替你串起来。这篇文章会按一条实际可落地的路径把这些串起来——从环境初始化开始经过组件库集成、权限路由、请求封装最后落到高频的表格和表单写法上。如果你是刚开始接触 vue3 后台项目或者已经在用 vue2 element-ui 想迁移这篇文章能给你一条少踩坑的路线如果你已经在写 vue3 后台末尾关于组件自动导入和类型推导的细节也值得停留一下。2. 从零初始化vue3 安装及环境配置的推荐组合2.1 为什么推荐 Vite 而不是 Vue CLI创建一个 vue3 项目现在最常见的做法是用 Vite 官方脚手架。Vue CLI 虽然仍然可用但它基于 Webpack开发服务器的冷启动速度和热更新效率在项目变大后明显落后于 Vite。Vite 利用浏览器原生 ESM 的能力开发阶段只编译当前页面实际用到的模块中后台系统动辄上百个路由页面这种按需编译的收益非常直接。还有一个实际原因ant-design-vue 的按需加载和主题定制在 Vite 下有成熟的插件支持配置起来比 Webpack 的 babel-plugin-import 方式更简洁。后面第 3 章会详细讲。生产构建方面Vite 默认使用 Rollup对中后台这种常规业务项目来说构建产物的大小和分包策略已经够用不需要额外引入重型方案。node -v # 推荐 Node.js 18低于 16 会直接报错 npm create vitelatest my-admin -- --template vue cd my-admin npm install npm run dev这里说几个容易忽略的点。npm create vitelatest中的latest确保拉取的是当前最新的脚手架版本避免本地缓存了旧版导致模板结构不一致。模板选择vue而不是vue-ts是因为多数中后台团队仍然使用 JavaScript 开发TypeScript 的接入成本在文章里暂不展开如果你所在团队已经统一 TypeScript选择vue-ts后在组件里用script setup langts即可后面的代码基本兼容。Node 版本需要特别注意Vite 5 要求 Node.js 18 及以上版本如果你还在用 Node.js 16npm run dev时会收到一条明确的版本警告但某些依赖安装时可能直接失败。2.2 工程目录的划分方式脚手架生成的是最简结构直接在里面写业务很快就会乱。我一般会在一开始就调整目录把中后台系统的通用能力从业务代码里分离出来。下面这个结构是中后台项目里比较常见的组织方式src/ ├── api/ # 接口定义 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── layout/ # 后台布局 ├── router/ # 路由配置 ├── store/ # Pinia 状态 ├── styles/ # 全局样式 ├── utils/ # 工具函数 ├── views/ # 页面组件 ├── App.vue └── main.jsapi目录单独拆出来是因为中后台系统的接口调用逻辑通常涉及请求地址、请求方法、参数转换和返回类型约定集中管理比散落在页面里更容易应对接口变更。layout目录放后台的整体框架——侧边菜单、顶部栏、内容区、标签页它只关心框架本身不关心具体业务页面。router和store对应路由和全局状态这部分权限控制和用户信息的耦合度很高拆出来方便单独维护。2.3 路由与状态管理的基础配置路由和状态管理是后台系统的骨架在引入权限和接口之前先把地基打稳。以下两个文件是最小可用配置。// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, name: Login, component: () import(/views/login/index.vue), meta: { title: 登录 } }, { path: /, component: () import(/layout/index.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 工作台, icon: DashboardOutlined } } ] } ] const router createRouter({ history: createWebHistory(), routes }) export default routercreateWebHistory是 HTML5 的 history 模式URL 更干净但生产环境需要 nginx 配置 try_files 回退到 index.html否则刷新二级页面会 404。meta字段在后台上承担了页面标题、菜单图标、权限标识等多重职责后续权限控制就是读取这里的配置。懒加载通过动态 import 实现每个路由页面单独打包中后台系统页面多首屏只加载当前页面对应的代码块这个收益在路由数量超过 20 个之后体感明显。// src/store/index.js import { createPinia } from pinia const pinia createPinia() export default pinia// src/store/modules/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: {} }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, logout() { this.token this.userInfo {} localStorage.removeItem(token) } } })Pinia 相比 Vuex 最大的优势是去掉了 mutations直接在 actions 里修改 state模板代码少了很多。token 存入 localStorage 是常见做法刷新页面后从本地恢复登录状态如果对安全性要求更高可以改用 cookie 并设置 httpOnly但前后端分离模式下通常由服务端下发 token 并处理刷新逻辑前端只负责存储和携带。3. 集成 ant-design-vue按需加载、主题定制与国际化3.1 安装与基础接入ant-design-vue 的接入方式有过几次变化。目前推荐的方式是使用unplugin-vue-components配合unplugin-auto-import实现组件和 API 的自动导入不需要每次手动在组件里注册打包时会按需引入。先安装依赖npm install ant-design-vue4.x npm install -D unplugin-vue-components unplugin-auto-import然后在 Vite 配置文件里加入插件// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import Components from unplugin-vue-components/vite import { AntDesignVueResolver } from unplugin-vue-components/resolvers import AutoImport from unplugin-auto-import/vite export default defineConfig({ plugins: [ vue(), Components({ resolvers: [ AntDesignVueResolver({ importStyle: less }) ] }), AutoImport({ imports: [vue, vue-router, pinia], dts: src/auto-imports.d.ts }) ] })AntDesignVueResolver负责解析模板里使用的组件比如a-table会被自动转换为对应的组件导入。importStyle: less表示按 less 源码方式引入样式这是主题定制的前提条件——如果使用默认的 css 方式后面修改主题色会非常困难。AutoImport 插件会把 vue 的ref、computedvue-router 的useRoute、useRouter等 API 自动导入组件里不需要再写 import。对于 5 年以上的前端来说这个能力节省的代码量可能不算多但它同时解决了另一个问题团队新成员写代码时不用担心漏导入导致的报错。这之后main.js里只需要做两件事注册全局的 message、notification、modal 这类函数式组件以及配置国际化语言。ant-design-vue 组件里内置了大量文案比如分页器的“跳至”、日期选择器的月份名称默认全英文需要手动改为中文。// src/main.js import { createApp } from vue import Antd from ant-design-vue import App from ./App.vue import router from ./router import pinia from ./store import ant-design-vue/dist/reset.css const app createApp(App) app.use(pinia) app.use(router) app.use(Antd) app.mount(#app)注意上面的app.use(Antd)是全量注册。如果你已经配置了 unplugin 自动导入其实可以去掉这一行组件在模板中使用时会被插件自动解析。但 message、Modal.confirm 这类函数式调用的组件不会经过模板解析器它们需要手动从包里引入。我通常保留全量注册因为中后台开发节奏快自动导入对消息类组件处理不完整全量注册省心但代价是打包体积会大一些。折中做法是只针对 message 做按需引入import { message } from ant-design-vue3.2 主题定制的两种方式中后台系统的主题色变动非常频繁——公司换品牌色、领导审美变化、暗黑模式上线。ant-design-vue 4.x 支持通过 ConfigProvider 的theme属性定制 token这套机制覆盖了颜色、圆角、字体大小等设计变量改动一处全局生效。!-- src/App.vue -- template a-config-provider :localezhCN :themethemeConfig router-view / /a-config-provider /template script setup import zhCN from ant-design-vue/es/locale/zh_CN import dayjs from dayjs import dayjs/locale/zh-cn dayjs.locale(zh-cn) const themeConfig { token: { colorPrimary: #1677ff, borderRadius: 6, fontSize: 14 } } /scriptcolorPrimary是最常用的 token所有组件的主题色都会跟着这个变量联动——按钮、链接、选中态、进度条、日期高亮。borderRadius控制全局圆角如果你的设计系统里按钮要直角风格改成 2 就行。fontSize设全局基础字号antd 所有组件的文字大小都会以此为基础计算。另一种定制方式是修改 less 变量适合需要覆盖 antd 内部样式变量的场景但 ConfigProvider 的 token 方案已经覆盖了绝大多数需求而且支持运行时动态切换主题——比如做暗黑模式时只需要把colorPrimary换成暗色背景下的亮色调即可less 方案做不到这件事。我的建议是优先使用 token 方案只有当某个组件内部硬编码了颜色无法通过 token 改变时才用样式覆盖。3.3 容易踩的坑ant-design-vue 集成过程中有几个问题出现频率很高。第一个是日期组件报错4.x 版本的日期选择器基于 dayjs如果项目里同时安装了 moment 和 dayjs会出现格式不一致的警告统一使用 dayjs 即可。第二个是 message 样式不生效如果你在上面的代码里使用了app.use(Antd)全量注册但样式因为按需加载而没有引入message 弹出来没有样式。解决方式是确保消息组件从ant-design-vue里 import 后其样式也能被加载——全量引入 reset.css 只能保证基础样式消息组件的样式需要单独按需引入或改用全量样式引用的方式。第三个是自动导入后编辑器提示找不到组件这是因为 dts 文件还没有生成运行一次npm run dev让插件生成src/auto-imports.d.ts即可。如果编辑器仍然报警告把dts配置项指向的文件 exclude 掉或者检查一下 tsconfig 的 include 路径。4. 权限路由与请求层中后台系统的地基4.1 路由权限控制的前后端协作方式中后台系统的权限通常分两层页面级权限控制用户能访问哪些路由按钮级权限控制用户在页面里能看到哪些操作按钮。页面级权限在后端下发路由和菜单,前端根据接口返回动态生成这背后需要前后端信息对齐——后端返回什么格式、前端怎么解析。最直接的方案是后端返回一个树形结构每个节点包含路径、组件地址、名称和图标前端递归生成路由。但实际项目中前端所有页面都是提前写好的后端返回的权限数据往往不由前端动态创建文件而是通过映射关系关联到前端已有的组件。常见的实现方式是这样路由表里定义所有页面每个路由的meta里配置permission标识登录后从接口获取当前用户的权限标识列表遍历路由表过滤出有权限的路由再通过addRoute动态注册。// src/router/permission.js import router from ./index import { useUserStore } from /store/modules/user const whiteList [/login] router.beforeEach(async (to, from, next) { const userStore useUserStore() if (userStore.token) { if (to.path /login) { next({ path: / }) } else { // 判断用户信息是否已加载避免刷新后重复请求 if (!userStore.userInfo.name) { try { await userStore.fetchUserInfo() // 根据权限标识动态注册路由 const accessRoutes filterRoutes(routes, userStore.permissions) accessRoutes.forEach(route router.addRoute(route)) next({ ...to, replace: true }) } catch (error) { userStore.logout() next(/login?redirect${to.path}) } } else { next() } } } else { if (whiteList.includes(to.path)) { next() } else { next(/login?redirect${to.path}) } } })whiteList是无需登录即可访问的路由这里只配置了登录页。filterRoutes函数遍历路由配置检查每一项的meta.permission是否在当前用户的权限列表中同时递归处理嵌套子路由。router.addRoute是 vue-router 4 的动态路由注册方法注册完成后必须使用next({ ...to, replace: true })重新进入一次导航因为刷新页面时首次进入的路由还没来得及注册直接next()会匹配不到组件而白屏。刷新页面后权限数据会丢失所以保证用户信息在刷新后能重新拉取很关键上面代码里的!userStore.userInfo.name就是处理这个场景的哨兵逻辑。一个常见错误是用户信息里没有权限字段那filterRoutes永远返回空数组用户能登录但看不到任何菜单排查时先确认接口返回的数据结构。4.2 axios 封装拦截器、错误处理与请求取消中后台的接口调用量大每个页面都单独处理状态码和错误提示会让代码极度重复。axios 拦截器是统一处理的最优场所——请求发出去之前统一加 token响应回来之后统一判断业务状态码遇到登录过期统一跳转登录页。// src/utils/request.js import axios from axios import { message } from ant-design-vue import { useUserStore } from /store/modules/user import router from /router const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) // 请求拦截器 service.interceptors.request.use( config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }, error Promise.reject(error) ) // 响应拦截器 service.interceptors.response.use( response { const res response.data // 假设后端约定 code 0 表示成功 if (res.code 0) { return res.data } message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { if (error.response) { switch (error.response.status) { case 401: message.error(登录已过期请重新登录) const userStore useUserStore() userStore.logout() router.push(/login) break case 403: message.error(没有权限执行此操作) break case 404: message.error(请求的资源不存在) break case 500: message.error(服务器内部错误) break default: message.error(error.response.data?.message || 网络错误) } } else { message.error(error.message || 网络异常) } return Promise.reject(error) } ) export default service这里有两个细节对中后台系统特别重要。第一个是import.meta.env.VITE_API_BASE_URL这是 Vite 读取环境变量的方式在项目根目录的.env.development和.env.production文件里配置不同的地址代码里不用改。第二个是响应拦截器里返回res.data而不是完整响应体这样页面里调用api函数的返回值直接就是业务数据少一层解包逻辑所有页面保持一致的写法。请求取消是容易被忽略的能力。用户在表格页快速切换查询条件上一次请求可能比下一次晚返回导致页面展示旧数据。axios 的 CancelToken 可以解决但更现代的做法是利用 AbortController 在组件卸载或条件变化时中止请求。在 axios 的拦截器场景里可以给请求队列做一个简单的取消机制// 请求时记录 controller const pendingControllers new Map() export const cancelAllRequests () { pendingControllers.forEach(controller controller.abort()) pendingControllers.clear() } service.interceptors.request.use(config { const controller new AbortController() config.signal controller.signal pendingControllers.set(config.url, controller) return config }) service.interceptors.response.use( response { pendingControllers.delete(response.config.url) return response.data }, error { pendingControllers.delete(error.config?.url) return Promise.reject(error) } )cancelAllRequests可以在路由切换时调用避免上一个页面的请求还在占用资源。中后台系统里有一个更实际的应用场景用户点击退出登录时立即取消所有在途请求防止退出后某些接口才返回数据把 token 又写回 localStorage。4.3 按钮级权限的通用指令写法页面路由权限解决的是“能不能进这个页面”但同一个页面里不同角色的操作权限往往不同——普通员工看不到导出按钮部门主管能看到但看不到删除按钮。按钮级权限最常用的实现是自定义指令// src/directives/permission.js import { useUserStore } from /store/modules/user const permission { mounted(el, binding) { const { value } binding const userStore useUserStore() const permissions userStore.permissions if (value !permissions.includes(value)) { el.parentNode?.removeChild(el) } } } export default permissiontemplate a-button v-permissionuser:delete typedanger clickhandleDelete 删除 /a-button /template script setup import permission from /directives/permission /script指令的mounted钩子里读取用户权限如果不包含当前按钮需要的权限标识直接从 DOM 中移除该元素。这种做法对表格操作列里的按钮尤其适用——操作列是动态渲染的每个按钮通过 v-permission 控制显隐接口权限和前端权限标识需要后端统一下发否则前端无法判断。如果你的按钮状态只读不发请求可以把权限标识硬编码在代码里由前端配置但权限标识的维护责任必须清晰否则离职交接时没人知道user:delete对应后端哪个接口。5. 表格与表单中后台页面的高频组合5.1 搜索区加表格的标准结构中后台系统的业务页面里表格和表单的密度最高。搜索区查询、表格展示、分页操作这套组合在几乎每个业务模块里都会出现。先看一个统一的表格页写法。template div classuser-list a-form :modelqueryParams layoutinline finishhandleSearch a-form-item label用户名 nameusername a-input v-model:valuequeryParams.username placeholder请输入用户名 allow-clear / /a-form-item a-form-item label状态 namestatus a-select v-model:valuequeryParams.status placeholder请选择状态 allow-clear a-select-option value1启用/a-select-option a-select-option value0禁用/a-select-option /a-select /a-form-item a-form-item a-button typeprimary html-typesubmit查询/a-button a-button stylemargin-left: 8px clickhandleReset重置/a-button /a-form-item /a-form a-table :columnscolumns :data-sourcetableData :loadingloading :paginationpagination row-keyid changehandleTableChange template #bodyCell{ column, record } template v-ifcolumn.key status a-tag :colorrecord.status 1 ? green : red {{ record.status 1 ? 启用 : 禁用 }} /a-tag /template template v-else-ifcolumn.key action a-button typelink clickhandleEdit(record)编辑/a-button a-popconfirm title确定删除该用户吗 confirmhandleDelete(record) a-button typelink danger删除/a-button /a-popconfirm /template /template /a-table /div /template这段模板里有两个值得记住的细节。第一是#bodyCell插槽ant-design-vue 4.x 用这个具名插槽统一处理表格单元格的自定义渲染插槽参数里column.key和record分别对应当前列配置和当前行数据你可以在同一个模板里通过条件判断渲染不同的列。第二是change事件表格的分页、排序、筛选变化都会触发这个事件它把分页参数的变更收口到了一处避免每个操作单独写事件处理。对应的脚本部分script setup import { ref, reactive, onMounted } from vue import { message } from ant-design-vue import { getUserList, deleteUser } from /api/user const loading ref(false) const tableData ref([]) const queryParams reactive({ username: , status: undefined, pageNum: 1, pageSize: 10 }) const pagination reactive({ total: 0, current: 1, pageSize: 10, showSizeChanger: true, showTotal: total 共 ${total} 条 }) const columns [ { title: 用户名, dataIndex: username, key: username }, { title: 状态, dataIndex: status, key: status }, { title: 创建时间, dataIndex: createTime, key: createTime }, { title: 操作, key: action } ] const fetchData async () { loading.value true try { const data await getUserList({ ...queryParams }) tableData.value data.records pagination.total data.total } finally { loading.value false } } const handleSearch () { pagination.current 1 fetchData() } const handleTableChange pag { queryParams.pageNum pag.current queryParams.pageSize pag.pageSize pagination.current pag.current pagination.pageSize pag.pageSize fetchData() } const handleDelete async record { await deleteUser(record.id) message.success(删除成功) fetchData() } onMounted(fetchData) /script这里分页参数的同步方式值得细说。queryParams里存的是最终要发给后端的参数pagination对象是给表格组件展示用的。查询时把pageNum重置为 1这是表格页的常见语义——用户修改查询条件后应该回到第一页而不是停留在他滚动到的页码。handleTableChange收到分页变化后同时更新 queryParams 和 pagination避免下一次查询时参数不同步。如果后端接口返回的分页字段名不是current和pageSize只需要在这里做一层映射比如把pageNum改成page改这一处就够。5.2 表单校验的常用规则ant-design-vue 的表单校验基于 async-validator最常见的校验场景是必填、长度限制、格式校验。正确写法是使用rules属性配合a-form-item的name属性绑定而不是在a-input上手动判断。a-form refformRef :modelformState :rulesrules layouthorizontal label-width100px a-form-item label用户名 nameusername a-input v-model:valueformState.username placeholder请输入用户名 / /a-form-item a-form-item label手机号 namephone a-input v-model:valueformState.phone placeholder请输入手机号 / /a-form-item a-form-item label邮箱 nameemail a-input v-model:valueformState.email placeholder请输入邮箱 / /a-form-item /a-formconst formRef ref() const formState reactive({ username: , phone: , email: }) const rules { username: [ { required: true, message: 请输入用户名, trigger: blur }, { min: 3, max: 20, message: 用户名长度在 3 到 20 个字符, trigger: blur } ], phone: [ { required: true, message: 请输入手机号, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ], email: [ { type: email, message: 邮箱格式不正确, trigger: blur } ] } const handleSubmit async () { try { await formRef.value.validate() // 校验通过后提交 } catch (error) { // 校验失败错误信息已由表单组件展示 } }trigger: blur表示失焦时触发校验另外还有change两者的区别在于用户输入过程中是否实时提示。pattern用于正则匹配校验手机号、身份证、税号这类有固定格式的字段都用它。type: email是 async-validator 的内置类型不需要手写正则。提交时调用formRef.value.validate()方法它会返回一个 Promise只有所有规则通过才 resolve失败时表单组件会在对应表单项下方自动展示错误信息不需要自己写错误展示逻辑。还有一个容易踩的坑当某个表单项的值从有值变成空值时如果 rule 配置了required: true在触发表单提交时会被拦截但如果用户输入后立刻提交而trigger只配了blur此时校验不会触发提交时校验也会检查出来所以校验发生在提交动作里的时机一定是对的不用担心中间过程漏校验。5.3 弹窗表单的复用方式弹窗加表单是另一个高频组合新增和编辑共用一个弹窗用open属性控制显隐initialValues或者编辑时手动回填数据。template a-modal v-model:openmodalOpen :titleisEdit ? 编辑用户 : 新增用户 okhandleOk cancelhandleCancel a-form refformRef :modelformState :rulesrules layoutvertical a-form-item label用户名 nameusername a-input v-model:valueformState.username / /a-form-item /a-form /a-modal /template弹窗的ok事件里做表单校验校验通过才关闭弹窗并调用接口。一个常见问题是关闭弹窗时表单数据没有重置下次打开时还残留上次的输入。解决方案是在close事件里调用formRef.value.resetFields()这个方法会重置表单数据到初始状态同时清除校验状态。如果你的表单初始值是异步获取的比如编辑时先查详情再回填需要处理的是 resetFields 会覆盖回填数据的问题——正确的顺序是先 reset 再 setFieldsValue或者用nextTick保证表单实例已经准备好。6. 在表格行内直接编辑的落地技巧日常开发里表格的编辑形态不止弹窗一种。行内编辑模式在很多业务场景中更高效——不需要打开弹窗直接在表格行内修改字段后保存。ant-design-vue 的表格支持通过自定义单元格实现行内编辑但实现方式有讲究关键点在于编辑状态的数据隔离。先定义一个可编辑单元格组件!-- src/components/EditableCell.vue -- template div template v-ifediting a-input v-ifinputType input v-model:valueeditValue sizesmall pressEnterhandleSave blurhandleSave / a-select v-else v-model:valueeditValue :optionsoptions sizesmall changehandleSave / /template span v-else classcell-text clickhandleStartEdit {{ editValue || 点击编辑 }} /span /div /template script setup import { ref, watch } from vue const props defineProps({ value: { type: [String, Number], default: }, inputType: { type: String, default: input }, options: { type: Array, default: () [] } }) const emit defineEmits([save]) const editing ref(false) const editValue ref(props.value) watch(() props.value, val { editValue.value val }) const handleStartEdit () { editing.value true } const handleSave () { if (editValue.value ! props.value) { emit(save, editValue.value) } editing.value false } /scriptediting状态控制当前单元格是显示文本还是可编辑控件。点击文本进入编辑状态输入框失焦或回车时保存并退出编辑。这里要注意的是watch监听外部传入的 value因为父组件更新数据后需要同步到单元格否则表格行内编辑的数据和接口返回的数据会不一致。然后在表格的列配置里使用这个组件template a-table :columnscolumns :data-sourcetableData row-keyid template #bodyCell{ column, record } template v-ifcolumn.key name EditableCell :valuerecord.name saveval handleCellSave(record, name, val) / /template /template /a-table /templatehandleCellSave接收行数据、字段名和新值调用对应的更新接口。这种方式把编辑状态控制在单元格内部不需要额外的行编辑状态标识维护成本最低。顺带提一个跟表格相关的实用技巧当表格数据量在几千行以内时不需要引入虚拟滚动ant-design-vue 的分页能力已经能保证性能。真正的性能瓶颈通常出在列配置频繁新建对象导致表格重渲染把columns定义在组件外部或使用computed缓存可以避免这一层开销。本文还有配套的精品资源点击获取
返回列表