导读如果你已经完成了 Day 3 的表格/表单/分页标准化开发那么今天要解决的是后台系统中“最难复用、最易失控”的两个环节复杂组件与状态管理。这篇文章不是泛泛而谈的“组件封装技巧”而是基于我当前真实项目uni-platform-admin的工程结构给出可直接落地的实战方案TreeSelect/Cascader 高复用组件模板统一接口、统一交互、统一请求链路Pinia 状态管理最佳实践权限/菜单/用户信息的统一管理组件通信边界何时用 Emits、何时用 Store、何时用 Provide/Inject性能与错误处理可观测 可追踪的前端稳定性策略所有代码都基于当前项目技术栈Vue 3 TypeScript Vite Element Plus Pinia vue-request并严格遵循项目规范。1. 项目基线与目录结构1.1 技术栈根据项目package.json当前项目核心依赖如下{vue:^3.5.26,typescript:~5.9.3,vite:^7.3.0,element-plus:^2.13.6,vue-router:^4.6.4,pinia:^3.0.4,vue-request:^2.0.4,axios:^1.13.2}1.2 目录结构src/ ├── components/ # 通用组件 │ └── form/ # 表单相关组件 ├── stores/ # Pinia 状态管理 ├── service/ # 接口服务 │ └── api/ # API 定义 ├── views/ # 页面组件 ├── directives/ # 自定义指令 ├── router/ # 路由配置 └── App.vue # 根组件1.3 项目规范要点组件使用script setup langts语法请求统一使用vue-request样式优先使用 Tailwind CSS代码通过 ESLint Oxlint Prettier 检查测试使用 Vitest Playwright2. 复杂组件开发实战TreeSelect2.1 需求分析在后台管理系统中树形选择器是高频组件常见场景包括组织架构选择楼栋/项目选择菜单权限配置如果每个页面都单独实现会导致代码重复交互不统一维护成本高因此我们需要封装一个高复用的 TreeSelect 组件。2.2 组件设计与目录落点目标文件src/components/form/TreeSelect.vue设计要点统一 Props 接口统一 Emits 事件统一树形数据结构支持多选、单选、禁用等配置2.3 组件实现script setup langts import { computed } from vue import type { TreeSelectProps } from element-plus // 树节点数据结构 interface TreeNode { id: string label: string children?: TreeNode[] disabled?: boolean } // 组件 Props interface Props { modelValue?: string[] options: TreeNode[] placeholder?: string multiple?: boolean checkStrictly?: boolean disabled?: boolean } // Props 默认值 const props withDefaults(definePropsProps(), { modelValue: () [], placeholder: 请选择, multiple: true, checkStrictly: true, disabled: false, }) // Emits 定义 const emit defineEmits{ update:model-value: [value: string[]] }() // 树形配置 const treeProps computedTreeSelectProps[props](() ({ value: id, label: label, children: children, disabled: disabled, })) // 处理选择变化 const handleChange (value: string[]) { emit(update:model-value, value) } /script template el-tree-select :dataprops.options :propstreeProps :multipleprops.multiple :check-strictlyprops.checkStrictly :disabledprops.disabled :model-valueprops.modelValue :placeholderprops.placeholder changehandleChange / /template2.4 接口封装目标文件src/service/api/asset.tsimport{request}from/service/request// 树形数据响应exportinterfaceAssetTreeResponse{data:TreeNode[]}// 获取资产树形数据exportconstassetTreeList(params?:any){returnrequest.getAssetTreeResponse(/ams/asset/tree-list,{params})}2.5 业务页接入示例目标文件src/views/asset/building/index.vuescript setup langts import { ref, onMounted } from vue import { useRequest } from vue-request import TreeSelect from /components/form/TreeSelect.vue import { assetTreeList } from /service/api/asset // 树形数据 const treeOptions refTreeNode[]([]) // 选中的值 const selectedIds refstring[]([]) // 使用 vue-request 管理请求 const { runAsync: fetchTree, loading: treeLoading } useRequest(assetTreeList) // 加载树形数据 const loadTree async () { const { data } await fetchTree() treeOptions.value data } // 生命周期 onMounted(() { loadTree() }) /script template section-group title资产选择 tree-select v-modelselectedIds :optionstreeOptions placeholder请选择资产 :loadingtreeLoading / /section-group /template3. Pinia 状态管理最佳实践3.1 为什么需要状态管理在后台管理系统中以下场景需要状态管理用户信息跨页面共享权限列表菜单权限、按钮权限全局配置主题、语言跨页面表单数据如果通过 Props 逐层传递会导致Props 透传地狱组件耦合度高数据同步困难3.2 权限状态管理目标文件src/stores/permission.tsimport{defineStore}frompinia// 权限状态接口interfacePermissionState{permissions:string[]// 权限标识列表menus:string[]// 菜单列表roles:string[]// 角色列表}exportconstusePermissionStoredefineStore(permission,{state:():PermissionState({permissions:[],menus:[],roles:[],}),getters:{// 检查是否有某个权限hasPermission:(state)(permission:string){returnstate.permissions.includes(permission)},// 检查是否有某个角色hasRole:(state)(role:string){returnstate.roles.includes(role)},},actions:{// 设置权限列表setPermissions(list:string[]){this.permissionslist},// 设置菜单列表setMenus(list:string[]){this.menuslist},// 设置角色列表setRoles(list:string[]){this.roleslist},// 清空权限clearPermission(){this.permissions[]this.menus[]this.roles[]},},})3.3 用户信息状态管理目标文件src/stores/user.tsimport{defineStore}frompinia// 用户信息接口interfaceUserInfo{id:stringusername:stringnickname:stringavatar?:stringemail?:stringphone?:string}interfaceUserState{userInfo:UserInfo|nulltoken:string}exportconstuseUserStoredefineStore(user,{state:():UserState({userInfo:null,token:,}),getters:{// 是否已登录isLogin:(state)!!state.token,// 用户昵称nickname:(state)state.userInfo?.nickname||state.userInfo?.username||,},actions:{// 设置用户信息setUserInfo(info:UserInfo){this.userInfoinfo},// 设置 TokensetToken(token:string){this.tokentoken},// 登出logout(){this.userInfonullthis.token},},persist:{key:user-store,storage:localStorage,paths:[token,userInfo],},})3.4 在组件中使用 Storescript setup langts import { computed } from vue import { usePermissionStore } from /stores/permission import { useUserStore } from /stores/user const permissionStore usePermissionStore() const userStore useUserStore() // 计算属性 const hasEditPermission computed(() permissionStore.hasPermission(building:edit) ) const username computed(() userStore.nickname) // 登出 const handleLogout () { userStore.logout() // 跳转到登录页 } /script template div p当前用户{{ username }}/p el-button v-ifhasEditPermission typeprimary编辑/el-button el-button clickhandleLogout退出登录/el-button /div /template4. 组件通信与事件边界4.1 通信方式选择原则场景推荐方案说明父子组件通信Props Emits最基础、最直接的方式跨层级通信Provide/Inject避免Props透传跨页面状态Pinia Store全局状态管理兄弟组件通信Event Bus / Store优先使用Store复杂业务逻辑Store Actions保持组件纯粹4.2 Props Emits父子通信!-- 父组件 -- script setup langts import { ref } from vue import ChildComponent from ./ChildComponent.vue const value ref() const handleChange (newValue: string) { value.value newValue } /script template child-component :model-valuevalue update:model-valuehandleChange / /template !-- 子组件 -- script setup langts interface Props { modelValue: string } const props definePropsProps() const emit defineEmits{ update:model-value: [value: string] }() const handleInput (e: Event) { emit(update:model-value, (e.target as HTMLInputElement).value) } /script template input :valueprops.modelValue inputhandleInput / /template4.3 Provide/Inject跨层级通信!-- 祖先组件 -- script setup langts import { provide, ref } from vue const theme ref(light) provide(theme, theme) /script template div child-component / /div /template !-- 后代组件 -- script setup langts import { inject } from vue const theme injectRefstring(theme) /script template div :classtheme内容/div /template4.4 Store跨页面状态script setup langts import { useUserStore } from /stores/user const userStore useUserStore() // 读取状态 const userInfo userStore.userInfo // 调用 Action userStore.setUserInfo({ id: 1, username: admin }) /script5. 性能优化与错误处理5.1 组件性能优化5.1.1 使用 v-memo 优化列表渲染template div v-foritem in list :keyitem.id v-memo[item.id, item.status] {{ item.name }} /div /template5.1.2 使用 shallowRef/shallowReactiveimport{shallowRef,shallowReactive}fromvue// 大型数据使用 shallowRefconstlargeDatashallowRef({/* 大量数据 */})// 大型对象使用 shallowReactiveconstlargeObjectshallowReactive({/* 大量属性 */})5.1.3 组件懒加载import{defineAsyncComponent}fromvueconstHeavyComponentdefineAsyncComponent(()import(./HeavyComponent.vue))5.2 错误处理5.2.1 全局错误处理// main.tsimport{createApp}fromvueimportAppfrom./App.vueconstappcreateApp(App)app.config.errorHandler(err,instance,info){console.error(全局错误:,err)console.error(错误信息:,info)// 上报错误到监控系统}5.2.2 请求错误处理import{useRequest}fromvue-requestconst{runAsync,error}useRequest(apiFunction,{onError:(error){console.error(请求错误:,error)ElMessage.error(请求失败请重试)},})5.2.3 组件错误边界script setup langts import { ref, onErrorCaptured } from vue const error refError | null(null) onErrorCaptured((err) { error.value err // 阻止错误继续向上传播 return false }) /script template div v-iferror p组件加载失败/p el-button clickerror null重试/el-button /div slot v-else / /template6. 完整实战案例权限管理页面6.1 需求描述实现一个权限管理页面包含树形选择器选择权限权限列表展示权限启用/禁用权限删除6.2 完整代码目标文件src/views/permission/index.vuescript setup langts import { ref, reactive, onMounted } from vue import { ElMessage, ElMessageBox } from element-plus import { useRequest } from vue-request import { usePermissionStore } from /stores/permission import TreeSelect from /components/form/TreeSelect.vue import { permissionList, permissionEnable, permissionDelete, } from /service/api/permission // Store const permissionStore usePermissionStore() // 表单状态 const searchForm reactive({ permissionName: , status: , }) // 表格数据 const tableData ref([]) const loading ref(false) const total ref(0) // 选中的权限 const selectedIds refstring[]([]) // 树形数据 const treeOptions refTreeNode[]([]) // 使用 vue-request const { runAsync: fetchList } useRequest(permissionList) const { runAsync: toggleEnable } useRequest(permissionEnable) const { runAsync: deleteItem } useRequest(permissionDelete) const { runAsync: fetchTree } useRequest(assetTreeList) // 获取列表数据 const getList async () { loading.value true try { const { data, total: resTotal } await fetchList(searchForm) tableData.value data total.value resTotal } finally { loading.value false } } // 获取树形数据 const getTree async () { const { data } await fetchTree() treeOptions.value data } // 搜索 const handleSearch () { getList() } // 切换状态 const handleToggle async (row: any) { await ElMessageBox.confirm( 确定${row.status 1 ? 禁用 : 启用}该权限吗, 提示 ) await toggleEnable({ id: row.id, status: row.status 1 ? 0 : 1 }) ElMessage.success(操作成功) getList() } // 删除 const handleDelete async (id: string) { await ElMessageBox.confirm(确定删除该权限吗, 提示, { type: warning, }) await deleteItem({ id }) ElMessage.success(删除成功) getList() } // 生命周期 onMounted(() { getList() getTree() }) /script template section-group title权限管理 !-- 搜索表单 -- el-form :modelsearchForm inline el-form-item label权限名称 el-input v-modelsearchForm.permissionName placeholder请输入 / /el-form-item el-form-item label状态 el-select v-modelsearchForm.status placeholder请选择 clearable el-option label启用 value1 / el-option label禁用 value0 / /el-select /el-form-item el-form-item el-button typeprimary clickhandleSearch搜索/el-button /el-form-item /el-form !-- 权限选择 -- section-group title权限选择 tree-select v-modelselectedIds :optionstreeOptions placeholder请选择权限 / /section-group !-- 数据表格 -- el-table v-loadingloading :datatableData border el-table-column label权限名称 proppermissionName / el-table-column label权限标识 proppermissionCode / el-table-column label状态 propstatus template #default{ row } el-switch :model-valuerow.status 1 changehandleToggle(row) / /template /el-table-column el-table-column label操作 fixedright template #default{ row } el-button link typedanger clickhandleDelete(row.id) 删除 /el-button /template /el-table-column /el-table /section-group /template7. 性能监控与可观测性7.1 性能监控// 使用 Performance APIconstmeasurePerformance(){conststartperformance.now()// 执行操作constresultheavyOperation()constendperformance.now()console.log(操作耗时:${end-start}ms)}7.2 错误上报// 错误上报函数constreportError(error:Error,context?:any){// 上报到监控系统console.error(错误上报:,error,context)}// 在全局错误处理中使用app.config.errorHandler(err,instance,info){reportError(err,{instance,info})}8. 总结8.1 今日要点复杂组件封装TreeSelect 组件的完整实现与接入Pinia 状态管理权限、用户信息的统一管理组件通信边界明确不同场景的通信方案性能优化v-memo、shallowRef、懒加载等技巧错误处理全局错误处理、请求错误处理、组件错误边界8.2 明日预告明日将讲解路由与权限控制实战包括路由守卫的实现动态路由配置按钮权限控制菜单权限控制路由懒加载与性能优化9. 参考资源Vue 3 官方文档Pinia 官方文档Element Plus 官方文档vue-request 官方文档