恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

基于Vite5+Vue3+AntDesignVue4的配置化中后台脚手架实践

  • 首页
  • 资讯中心
  • /
  • 基于Vite5+Vue3+AntDesignVue4的配置化中后台脚手架实践

相关资讯

医疗视频私有化部署实践:EasyDSS流媒体平台从架构到落地 2026/9/15 5:35:06
AI爬虫识别与防护:robots.txt、IP段与UA三重验证指南 2026/9/15 5:35:06
社恐逆袭AI医疗:从大模型到RAG的入门路径 2026/9/15 5:35:06

最新资讯

2026 AI Agent实战指南:原理、架构与生产落地避坑手册
Flutter+鸿蒙音乐播放器开发实战与Vibe Coding解析
基于HuggingFace的聊天机器人开发实战指南
HTML+CSS实现星巴克咖啡网站主页:网页设计大作业完整攻略
Apache Calcite实现多数据源联邦查询实战指南
国产无刷驱动方案选型的5个隐形战场

今日推荐

GDPR下大数据架构重构与隐私保护实践
多组学数据平台架构设计与优化实践
企业主数据管理系统架构设计与实施全解析

本周热门

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

基于Vite5+Vue3+AntDesignVue4的配置化中后台脚手架实践

发布时间:2026/9/15 5:40:06
基于Vite5+Vue3+AntDesignVue4的配置化中后台脚手架实践 简介基于 Vite5.x Vue3.x ant-design-vue4.x 与 TypeScript hooks 的后台管理系统源码包面向需要搭建中后台前端框架的开发者。项目聚焦 RBAC 权限系统、JSON Schema 动态表单、动态表等常见业务模块并展示了 Vue3 全家桶Vuex、Vue Router及 Composition API 的实际组合方式可帮助读者快速建立从权限校验到动态渲染的完整链路。压缩包共 33 个文件以 mjs、json、ts、yaml、md 为主涵盖 Vite 构建脚本、依赖配置、TS 类型声明、Dockerfile、环境变量拆分及 README 文档整体仅 147KB轻量便于逐行研读。源码中实践了 Vue3 新特性对 Composition API 与 Options API 的区别做了示范性演示同时收集了 UnoCSS、ESLint、Stylelint 等工程化配置并给出 openapi 配置示例便于结合 Swagger 文档与 json2ts 工具自动生成类型整体来看这份源码既适合作为学习 Vue3 中后台开发的练手素材也可以在此基础上快速扩展业务模块沉淀为团队内部的基础工程。已有 158 人学习下载适合具备一定 Vue 基础、想进阶工程化和后台权限设计的开发者参考。1. 这套技术栈为什么值得复制中后台管理系统做到第三四个页面大多数团队会意识到路由守卫里的权限判断、表单的十几个 v-if 分支、表格写死的 columns才是真正把开发拖慢的地方。标题里这套组合vite5.x 管构建速度vue3.x 管组合式复用ant-design-vue4.x 管组件覆盖度TypeScript hooks 管类型推导最后落点在 RBAC 权限、JSON Schema 动态表单、动态表这三件套上。一句话说清它的价值权限、表单、表格全部从“写页面”变成“写配置”新增一个带增删改查的页面核心工作量被压到定义一份菜单权限码、一份字段 schema、一份列配置。适合准备搭中后台脚手架的前端也适合正在做低代码配置化改造的团队参考。2. Vite5.x 与 TypeScript hooks 的工程底座2.1 初始化项目时把版本锁死在 Vite5 与 Vue3.4直接用 create-vite 最新版拉模板可能会生成 vite6 或更高版本的项目骨架再回头降级反而麻烦。我一般会显式指定版本线npm create vite5 my-admin -- --template vue-ts cd my-admin npm install npm install ant-design-vue^4.2.0 vue-router^4.3.0 pinia^2.1.0 axios这组命令的效果是create-vite 走 5.x 版本生成模板项目依赖里 vite 落在 5.xvue 落在 3.4同时补上 ant-design-vue 4.x、路由、状态管理、请求库。ant-design-vue 4.x 的a-form校验机制是自己实现的一套 rules和 Vue 生态其他表单库不通用这也是后面 JSON Schema 动态表单需要单独做一层映射的原因。版本锁定后把 package.json 里的依赖范围明确出来包版本线选用理由vite^5.4.0esbuild 预构建加原生 ESM冷启动和 HMR 表现稳定配置心智负担小vue^3.4.0defineModel、useTemplateRef 等 API 让自定义组件和 hooks 更精简ant-design-vue^4.2.0表格、表单、弹窗组件覆盖度够且 Table 的 change 事件签名自带分页和排序信息typescript~5.5.0泛型、satisfies、模板字符串类型在处理 schema 配置时非常有用pinia^2.1.0去掉了 mutations 概念storeToRefs 解构后不丢响应式权限状态读写比 vuex 少一层模板代码状态管理这里选 pinia 而不是 vuex核心原因是权限系统里“用户信息、权限码、菜单树”会被路由守卫、侧边栏、指令三类模块读取pinia 的组合式 store 写法可以直接复用 TypeScript hooks 的返回类型不需要额外定义 Getter 类型。安装完成后先跑一次npm run dev确认基础链路易位再继续改配置。2.2 vite.config.ts 多环境变量与按需拆包开发期最闹心的两件事一个是接口跨域一个是打包后 vendor 文件巨大。跨域在server.proxy里解决拆包在build.rollupOptions里配置这两个都属于项目初始化就要写好的东西// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig(({ mode }) { const isProd mode production return { plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)), }, }, server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端接口没有 /api 前缀这里去掉 rewrite: (path) path.replace(/^\/api/, ), }, }, }, build: { sourcemap: !isProd, chunkSizeWarningLimit: 1024, rollupOptions: { output: { manualChunks: { antd: [ant-design-vue], vue-core: [vue, vue-router, pinia], }, }, }, }, } })server.proxy的作用是让前端代码里统一写/api/xxx开发时由 Vite 转发到真实后端避免每个接口都写全路径也绕开浏览器跨域限制。manualChunks把 ant-design-vue 单独拆成一个 chunkvue 全家桶拆成另一个这样 antd 的样式和组件缓存可以独立于业务代码改业务逻辑时浏览器只需要拉业务包首屏体积保持在 1MB 以下。.env.production里只放VITE_API_BASE、VITE_APP_TITLE这类以VITE_开头的变量Vite 打包时会把它内联到产物里import.meta.env.VITE_API_BASE就能读到。打包后如果出现“布局异常”这类问题第一反应检查base是否设置为相对路径第二检查路由是否用了createWebHistory而服务器没有做 history 回退。2.3 useTable 的 hooks 抽象状态与请求绑在一起组合式 API 在这个工程里最常见的落地形态是把“分页、加载、数据列表、查询条件”封装成一个useTablehooks页面组件里十行代码完成列表初始化import { ref, computed } from vue import type { Ref } from vue import type { TableProps } from ant-design-vue import { message } from ant-design-vue export interface PageQuery { page: number pageSize: number [key: string]: unknown } export interface PageResultT { list: T[] total: number } export function useTableT extends Recordstring, unknown( fetcher: (query: PageQuery) PromisePageResultT ) { const loading ref(false) const list refT[]([]) as RefT[] const total ref(0) const query refPageQuery({ page: 1, pageSize: 10 }) const pagination computedTableProps[pagination]({ get: () ({ current: query.value.page, pageSize: query.value.pageSize, total: total.value, showSizeChanger: true, showTotal: (t: number) 共 ${t} 条, }), set: (val) { if (val?.current) query.value.page val.current if (val?.pageSize) query.value.pageSize val.pageSize }, }) async function fetchData() { loading.value true try { const res await fetcher(query.value) list.value res.list total.value res.total } catch (e) { message.error(列表加载失败) console.error(e) } finally { loading.value false } } return { loading, list, total, query, pagination, fetchData } }pagination用 computed 的 get/set 写法是为了直接绑定到a-table的v-model:pagination上组件内部触发分页变化时自动回到 set 分支更新 query。调用方拿到fetchData而不是在 hooks 内部自动触发 onMounted原因很实际列表页需要先拉字典再查数据或者复用时想先填默认筛选条件再请求把触发时机交给页面控制比“自动跑”更灵活。这个 hooks 不绑定 ant-design-vue 的 Table 实例只绑定分页和数据结构后面动态表格组件会基于同样的返回结构再包一层。2.4 让 TypeScript 在 Vue 模板里不飘红的三处配置vue-tsc 和 Vite 的 esbuild 转译是两套体系前者做类型检查后者只负责去类型。项目里最常见的飘红来自三个地方别名不识别、import.meta.env没有类型、.vue文件自身类型缺失。这三处一次性配置完后面写 schema 类型时才不会满屏报错{ compilerOptions: { strict: true, baseUrl: ., paths: { /*: [src/*] }, types: [vite/client], moduleResolution: bundler, skipLibCheck: true }, include: [src/**/*.ts, src/**/*.d.ts, src/**/*.vue] }types: [vite/client]会带上ImportMetaEnv的接口声明import.meta.env.VITE_XXX才能推导出string | undefined。moduleResolution: bundler是 Vite 项目的推荐值它按打包器的模块解析规则找依赖避免 Node 的 CommonJS 解析方式把 ESM 包类型路径弄错。skipLibCheck: true不是偷懒ant-design-vue 的 .d.ts 里偶尔会有跟 Vue 3.4 类型定义冲突的边界跳过第三方声明文件检查能省掉大量无效报错。配置完跑一次npm run build能过就说明类型链路是通的。3. RBAC 权限系统从菜单表到动态路由与按钮指令3.1 后端菜单接口的数据结构带权限码的树RBAC 的核心关系是“用户属于角色角色拥有权限码菜单上挂权限码”前端拿到的不是扁平列表而是一棵可以递归渲染的菜单树。先约定接口返回结构前端的所有逻辑都围绕它展开export interface MenuVO { id: string parentId: string | null name: string // 路由名称必须唯一 path: string // 完整路径如 /system/user component: string // 组件路径字符串如 system/user/index meta: { title: string icon?: string hidden?: boolean permissions: string[] // 按钮权限码如 system:user:add } children?: MenuVO[] }component存字符串而不是直接存组件对象是因为后端数据库里只能存字符串前端用import.meta.glob按路径动态加载组件。权限码三段式约定为模块:资源:操作例如system:user:add、system:user:delete每段用冒号分隔前端判断时按前缀做粗粒度校验后端做细粒度鉴权。有人问为什么不做“菜单表、按钮表、角色表”三张表经验是按钮数量一多维护成本和权限码直接写进 meta 没有本质差别权限码本身就能表达按钮级权限建表反而让前端要多一次联表查询。这样一个典型的 RBAC 对应关系是层次主体表达方式用户层登录用户用户 id、token、角色 id 列表角色层管理员/运营/访客每个角色持有若干权限码集合资源层菜单、路由、按钮菜单树节点携带权限码数组前端拿到登录用户信息时同时返回permissions数组和菜单树这两个数据放到 pinia 的 auth store 里后续路由守卫和按钮指令都从这里取。3.2 动态路由注册刷新白屏的三种修复思路动态路由的标准流程是登录后拉取菜单树buildRoutes把菜单树递归转换成RouteRecordRaw再逐个router.addRoute。这里最经典的坑是刷新页面白屏原因不是路由注册失败而是addRoute执行后当前导航已经结束了刷新后首次导航是在菜单加载完成前发生的。先看完整的守卫写法import { createRouter, createWebHistory, type RouteRecordRaw } from vue-router import { useAuthStore } from /stores/auth const modules import.meta.glob(../views/**/*.vue) function buildRoutes(menus: MenuVO[]): RouteRecordRaw[] { return menus.map((menu) { const record: RouteRecordRaw { path: menu.path, name: menu.name, component: modules[../views/${menu.component}.vue], meta: { ...menu.meta, permissions: menu.meta.permissions ?? [], }, children: menu.children ? buildRoutes(menu.children) : undefined, } return record }) } router.beforeEach(async (to) { const auth useAuthStore() if (!auth.token) { return { path: /login, query: { redirect: to.fullPath } } } if (auth.menusLoaded) return true const menus await api.getMenus() const routes buildRoutes(menus) routes.forEach((r) router.addRoute(r)) auth.menusLoaded true return { ...to, replace: true } })关键在最后一行return { ...to, replace: true }。addRoute后当前导航的匹配记录里还没有这些新路由直接放行会命中 404所以守卫返回一份和当前目标相同的路由信息并标记 replace让 vue-router 按新注册的路由重新解析一遍。常见做法里还有两种补充方案一是把菜单树存 sessionStorage刷新后先恢复 store 再跳转减少一次请求二是在布局路由下用router.addRoute(Layout, child)注册子页否则儿童路由的路径拼接可能不符合预期。menusLoaded只放内存即可刷新后自然清空守卫重新拉菜单并重新注册不需要把它持久化。3.3 按钮级权限v-permission 与 usePermission 二选一页面里最常用的是按钮级权限控制两种做法各有用处。第一种是自定义指令适合权限码在渲染前就确定的静态按钮import type { Directive, DirectiveBinding } from vue import { useAuthStore } from /stores/auth function check(codes: string | string[], all false): boolean { const auth useAuthStore() const list Array.isArray(codes) ? codes : [codes] return all ? list.every((c) auth.permCodes.includes(c)) : list.some((c) auth.permCodes.includes(c)) } export const permission: DirectiveHTMLElement, string | string[] { mounted(el: HTMLElement, binding: DirectiveBindingstring | string[]) { if (!check(binding.value, binding.arg every)) { el.parentNode?.removeChild(el) } }, }v-permission的使用方式是a-button v-permissionsystem:user:add新增/a-button多个权限码传数组。指令在 mounted 阶段移除 DOM简单直接但它只执行一次如果权限是登录后异步加载的按钮已经渲染再更新权限码不会恢复。第二种做法是回到模板条件渲染export function usePermission() { const auth useAuthStore() const has (code: string | string[]): boolean check(code) return { has } }页面里const { has } usePermission()模板用template v-ifhas(system:user:add)包住按钮。因为has内部读取的是响应式的permCodes权限码更新时模板会跟着重新渲染登录态恢复类场景更可靠。两条路线的取舍是静态按钮用指令更干净权限受异步状态影响的按钮用usePermission更稳。记住一点两个都要在 main.ts 里注册好 store否则指令里访问 pinia 会报 no active pinia。3.4 权限指令的边界表格列和菜单树不能靠指令自定义指令有明确的应用边界。a-table的 columns 是普通 JavaScript 对象指令无法作用到对象属性上所以动态表格里的列权限要在列配置里过滤侧边栏菜单同理菜单树的 visible 判断要在 store 初始化时用permissions过滤一遍而不是渲染完再用指令删。另一个边界是 v-if 和 v-permission 同时写在一个元素上时v-if 的优先级更高先被移除的节点根本轮不到指令执行逻辑上就会变成“权限通过 but 不渲染”。实际项目中我更倾向把按钮封装成AuthButton组件内部用usePermission决定渲染a-button还是渲染一个禁用态 Tooltip因为直接删除按钮会让用户不知道为什么功能没了防误点场景下禁用比删除体验更好。4. JSON Schema 动态表单定义字段、生成校验、注册组件4.1 从标准 JSON Schema 到表单 DSL 的取舍标准 JSON Schema 核心字段是type、properties、required、enum它描述的是数据合法性不管界面怎么渲染。表单场景需要额外的 UI 信息label 文案、placeholder、栅格宽度、显隐条件所以工程里常见做法是定义一套扩展版 schema既保留 JSON 的序列化能力又能直接被渲染器消费export type FieldType | input | textarea | number | select | radio | date | switch | custom export interface FieldSchema { type: FieldType key: string label: string required?: boolean placeholder?: string options?: { label: string; value: string | number }[] rules?: Recordstring, any[] hidden?: string // 显隐表达式如 status 2 span?: number // 栅格占位默认 12 }字段的具体配置和渲染对应关系如下schema 字段类型影响范围typeinput/select/date...决定渲染哪个组件keystring对应表单 state 的字段名requiredboolean生成必填校验规则options数组下拉/单选的数据源hidden字符串表达式控制当前字段是否渲染hidden是字符串而不是函数是因为 schema 要从后端下发或存数据库函数无法序列化。代价是前端要写一个小解释器来求值这是配置化表单绕不开的一层。4.2 渲染器schema 到 a-form-item 的映射渲染组件要做三件事遍历字段、按 type 映射到a-input等组件、用v-model:value绑定到 model 的对应 key。这个组件不做任何业务逻辑纯粹是 schema 的执行器script setup langts import { computed } from vue import { Form, FormItem, Input, Select, DatePicker, Switch } from ant-design-vue const props defineProps{ fields: FieldSchema[] model: Recordstring, any }() function simpleEval(expr: string, model: Recordstring, any): boolean { const match expr.trim().match(/^(\w)\s*(||!|!||||)\s*(.)$/) if (!match) return true const [, key, op, raw] match const actual model[key] const expect raw.replace(/^[]|[]$/g, ) switch (op) { case : return actual expect case : return actual expect case !: return actual ! expect case !: return actual ! expect case : return Number(actual) Number(expect) case : return Number(actual) Number(expect) default: return true } } const visualFields computed(() props.fields.filter((f) !f.hidden || simpleEval(f.hidden, props.model)) ) /script template a-form :modelmodel a-row :gutter16 a-col v-forfield in visualFields :keyfield.key :spanfield.span || 12 a-form-item :labelfield.label :namefield.key a-input v-iffield.type input v-model:valuemodel[field.key] :placeholderfield.placeholder / a-select v-else-iffield.type select v-model:valuemodel[field.key] :optionsfield.options / a-date-picker v-else-iffield.type date v-model:valuemodel[field.key] stylewidth: 100% / a-switch v-else-iffield.type switch v-model:valuemodel[field.key] / a-input v-else-iffield.type textarea v-model:valuemodel[field.key] typetextarea :rows3 / /a-form-item /a-col /a-row /a-form /templatesimpleEval故意只支持单个字段和操作符的简单表达式不引入new Function后端配置的字符串不能被当成代码执行。实际业务里的联动大多是“支付方式等于转账时显示收款账户”这种单条件设计时引导后端同学拼这种最简单的表达式就够了。a-col的span让表单自动排列成栅格一个字段一行还是半行由配置决定页面不需要关心。visualFields用 computed 包裹model 里某个联动字段变化时关联字段会自动重新计算显隐。4.3 rules 生成required 与 trigger 的匹配问题a-form的校验规则从 schema 的required和rules字段合并生成比手写少一半重复代码export function toRules(field: FieldSchema): Recordstring, any[] { const rules: Recordstring, any[] [] if (field.required) { rules.push({ required: true, message: ${field.label}不能为空, trigger: field.type select ? change : blur, }) } if (field.rules) { rules.push(...field.rules) } return rules }trigger的类型是最容易踩的坑。输入框用blur用户离开输入框才提示下拉框如果也用 blur键盘操作时会出现“选择选项前先触发失焦校验”的错觉而且 ant-design-vue 的 Select 在展开面板时 input 会先 blur 一次错误提示会在选择完成前冒出来所以 select 和 date 这类组件固定用change触发。toRules返回的数组直接在模板的a-form-item上做:rules绑定但注意a-form上还要设置:model和:rules才能让 formRef.validate() 整体生效组件的a-form-item上只做字段级声明。这里建议把toRules放在独立工具文件里因为列配置、抽屉表单、步骤表单都要复用同一套 schema-to-rules 逻辑。4.4 自定义组件注册表不要改动 SchemaFormcustom类型是给业务特殊组件留的口子比如用户选择器、部门树、富文本。SchemaForm 组件内部维护一个组件注册表通过动态组件渲染import type { Component } from vue import { Input, Select, DatePicker, Switch } from ant-design-vue import { UserPicker } from /components/UserPicker import { DeptTree } from /components/DeptTree export const componentMap: Recordstring, Component { input: Input, select: Select, date: DatePicker, switch: Switch, userPicker: UserPicker, deptTree: DeptTree, }模板里加一行兜底渲染component :iscomponentMap[field.type] v-else-ifcomponentMap[field.type] v-model:valuemodel[field.key] /新增一个自定义字段时只需要把组件注册进componentMapschema 的 type 填对应的 keySchemaForm 的代码完全不用动。这个注册表的另外一种形态是直接在 main.ts 里用app.component全局注册但显式 map 的好处是类型提示和依赖关系清晰编辑器能列出所有可注册的字段类型。到这里表单层已经完整定义 schema、渲染组件、生成规则、扩展组件四个环节相互独立任何一环都可以单独替换。5. 动态表格列配置、权限过滤与服务端交互5.1 ColumnSchema 到 a-table columns 的转换动态表格的思路和 dynamic form 一样列来源于配置而非硬编码。定义一个最小可用的列 schemaexport interface ColumnSchema { key: string title: string dataIndex?: string width?: number slot?: string // 作用域插槽名用于自定义单元格 sortable?: boolean // 是否支持服务端排序 permission?: string // 权限码无权限时整列隐藏 } export interface ActionSchema { label: string code: string permission?: string danger?: boolean hidden?: (record: any) boolean }把配置转换成 ant-design-vue 的 columns需要做两件事过滤不可见列把 slot 配置映射成customRenderconst visibleColumns computed(() { const auth useAuthStore() return props.columns .filter((col) !col.permission || auth.permCodes.includes(col.permission)) .map((col) ({ ...col, customRender: col.slot ? ({ record }: { record: any }) slots[col.slot!]?.(record) : undefined, })) })dataIndex支持user.name这种路径式写法时需要自己做一个按路径取值的小函数ant-design-vue 的列配置里 dataIndex 的路径解析能力和渲染器的预期不一定完全一致。表格的列配置、数据、分页都由外部传入这个组件就是“给什么列显示什么列”的纯渲染层页面里的业务逻辑集中在列 slot 里。5.2 分页、排序、过滤怎么和后端参数对齐服务端分页时a-table 的 change 事件会同时给出分页、过滤、排序三份状态常见做法是在事件回调里统一转成后端参数script setup langts import type { TablePaginationConfig } from ant-design-vue function onTableChange(pagination: TablePaginationConfig, _filters: any, sorter: any) { const params: Recordstring, any { ...query, page: pagination.current, pageSize: pagination.pageSize, } if (sorter sorter.columnKey) { params.sortField sorter.columnKey params.sortOrder sorter.order ascend ? asc : desc } emit(change, params) } /script传给后端参数时注意大小写。很多后端框架默认用小驼峰pageSize而 a-table 的pagination对象上是pageSize对齐一次就不会到处出问题。sorter.columnKey取的是列配置里的 key 而不是 dataIndex如果列配置里两者不一致排序字段会传错建议列 schema 里强制 key 与 dataIndex 相同或者显式指定 sortField 映射。过滤条件_filters的 value 是数组后端只接受单个值时取filters[field]?.join(,)处理后再拼接。动态表格组件只负责把 change 事件转换成统一参数对象向上 emit请求逻辑依然交给 useTable 的 fetcher。5.3 操作列保留插槽配置化不是万能操作列编辑、删除、权限分配最常出现 Popconfirm、Dropdown、Tooltip 组合这些交互很难在配置里表达函数强行配置化会引入 JSX 或函数类型的 fields结果 schema 就没法从后端下发了。实操上的折中是普通按钮走配置复杂交互走插槽。a-table :columnsvisibleColumns :data-sourcedata :loadingloading template v-if$slots.opColumn #opColumn{ record } slot nameopColumn :recordrecord/slot /template /a-table列 schema 里给操作列留一个固定的slot: opColumn页面组件的插槽写template #opColumn{ record }里面自由使用 Popconfirm 和 Dropdown。纯配置模式负责数据列插槽模式负责操作列这套边界几轮迭代下来比“全部配置化”稳得多因为表格列本身就是数据展示操作列本质是组件组合。5.4 表单和表格配置同源的页面组合动态表单和动态表格分开用只是第一步组合起来效果才明显。某个管理页面的典型组合方式是后端返回的页面配置里同时包含 columns 和 form 配置列表页用 TableSchema 渲染表格点新增时把 form 配置塞进 Drawer 里的 SchemaFormexport interface PageConfig { columns: ColumnSchema[] form: FieldSchema[] permissions?: { query: string[]; create: string[]; update: string[]; delete: string[] } } export function usePageConfig(config: PageConfig) { const { columns, form } config return { columns, form } }最初从零写一个页面要两个文件加三处字段定义现在变成一份配置和两个通用组件。配置化节约的不只是开发量最重要的是产品调整字段顺序、增删字段时改配置就能上线前端代码不重新发版。动态表单和动态表格的分工边界就一句话表单管录入校验表格管查询展示配置里各自独立页面里通过同一个 config 对象拼装。6. 收尾技巧给 schema 补上完整的类型推导配置化的最大隐患是 key、type 这些字符串拼错运行时才发现。Vue 3.3 开始 SFC 支持泛型组件TypeScript 的satisfies可以校验对象数组符合 schema 定义但不改变值的类型这两个特性组合起来让动态 schema 在编码阶段就具备完整提示。先在 FieldSchema 上加一个泛型参数让 key 限定在具体业务模型的字段集里export type FieldSchemaK extends string string OmitBaseFieldSchema, key { key: K }页面里定义业务表单配置时用satisfies校验export interface UserForm { name: string role: string status: number } export const userFormSchema [ { type: input, key: name, label: 姓名, required: true }, { type: select, key: role, label: 角色, options: [], trigger: change }, ] satisfies FieldSchemakeyof UserForm[]如上配置存在一个显著收益把 key 错写成userName时编辑器会给出字面量类型上“userName 不能赋值给 keyof UserForm”的错误提示不再等到运行时才在表单字段上发现绑定失效。SchemaForm 组件对应调整为script setup langts genericT extends Recordstring, any defineProps{ fields: FieldSchemakeyof T[] model: T }() /script模板里使用 SchemaForm 时会自动把model的类型传进去fields里每个字段的 key 都被约束在 T 的属性里。动态表格的 ColumnSchema 也按同样方式约束 dataIndex。再补上指令的类型声明让模板里的v-permission也有提示declare module vue { export interface ComponentCustomDirectives { permission: { value: string | string[] } } } export {}这段代码放到src/types/directives.d.tsvue-tsc 就会在模板里校验 v-permission 传入的参数类型。最后提醒一点TypeScript 版本最好整体对齐到 5.x 的同一小版本线常见类型推导失效都源于项目根目录和编辑器的 TS 版本不一致。这套配置跑完从字段 schema 到列 schema 再到权限指令配置化代码的编辑体验就接近强类型业务代码了。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号