药师学社
65.31M · 2026-02-04
在前三篇文章中,已完成配置化 CRUD 体系的核心铺垫 —— 通过动态表单组件避免了重复编写表单模板,用搜索重置组件实现了列表筛选的统一封装,再借助 useTable Hooks 搞定了列表查询与分页的通用逻辑,三者联动让列表页实现了 “配置即开发” 的高效模式。不过,在完整的 CRUD 流程中,新增与编辑功能也是不可或缺的一部分,接下来,我们将结合前序动态表单组件,实现统一的新增编辑页,完成配置化CRUD开发的完美闭环。
文章指路(一起食用,味道更加):
1.动态表单深度实践: juejin.cn/post/757946… ;
2.搜索重置组件:juejin.cn/post/760056… ;
3.useTable Hooks: juejin.cn/post/760068… ;
统一的新增编辑页,是为了保持新增与编辑页面的统一风格,同时将重复的按钮操作进行统一的封装及事件暴露处理。
其核心设计思路可概括为:封装统一的顶部展示区域、底部操作栏区域(支持显示 / 隐藏控制),暴露默认插槽(主体内容)与具名插槽(顶部展示区域、底部操作区域),让重复逻辑内聚、个性化需求通过插槽灵活实现,既保证一致性,又不丧失扩展性。
const route = useRoute()
const router = useRouter()
//找到页面指定的活跃路由
const findRouteByActiveMenuId = (
routes: any,
targetActiveMenuId: any
): any => {
for (const route of routes) {
if (route.meta?.menu_id === targetActiveMenuId) {
return route
}
// 递归检查子路由
if (route.children) {
const found = findRouteByActiveMenuId(
route.children,
targetActiveMenuId
)
if (found) return found
}
}
return {}
};
const loginStore = useLogin()
const activeMenuId = route?.meta?.activeMenuId || ''
//找到页面指定的路由
const activeMenuObj = findRouteByActiveMenuId(
loginStore.Matchroute,
activeMenuId
)
const match = [activeMenuObj, route];
<div class="top-title">
<div class="bread-item">
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in match"
:key="index"
>{{ $t(item?.meta?.label) }}</el-breadcrumb-item
>
</el-breadcrumb>
</div>
</div>
<!-- 主体内容:默认插槽 -->
<slot></slot>
type SearchPrams = {
showConfirm?: boolean
showCancel?: boolean
confirmText: string
cancelText?: string
}
const props = withDefaults(defineProps<SearchPrams>(), {
showConfirm: true,
showCancel: true,
confirmText: 'Confirm',
cancelText: 'Cancel',
})
const slots = useSlots()
const emits = defineEmits(['submit'])
<template v-if="slots.bottom">
<!-- 外部自定义底部内容-->
<slot name="bottom"> </slot>
</template>
<template v-else>
<div class="btn-content">
<ElButton
v-if="showConfirm"
:style="{ width: '100px' }"
@click="() => router.back()"
>{{ $t(cancelText) }}</ElButton
>
<ElButton
v-if="showCancel"
type="primary"
:style="{ width: '100px' }"
@click="() => emits('submit')"
>{{ $t(confirmText) }}</ElButton
>
</div>
</template>
<script lang="ts" setup>
import { ElButton } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import useLogin from '@/stores/login'
import {useSlots } from 'vue'
type SearchPrams = {
showConfirm?: boolean
showCancel?: boolean
confirmText: string
cancelText?: string
}
const props = withDefaults(defineProps<SearchPrams>(), {
showConfirm: true,
showCancel: true,
confirmText: 'Confirm',
cancelText: 'Cancel',
})
const route = useRoute()
const router = useRouter()
const slots = useSlots()
const emits = defineEmits(['submit', 'cancel'])
const findRouteByActiveMenuId = (
routes: any,
targetActiveMenuId: any
): any => {
for (const route of routes) {
if (route.meta?.menu_id === targetActiveMenuId) {
return route
}
// 递归检查子路由
if (route.children) {
const found = findRouteByActiveMenuId(
route.children,
targetActiveMenuId
)
if (found) return found
}
}
return {}
}
const loginStore = useLogin()
const activeMenuId = route?.meta?.activeMenuId || ''
//找到页面指定的路由
const activeMenuObj = findRouteByActiveMenuId(
loginStore.Matchroute,
activeMenuId
)
const match = [activeMenuObj, route]
</script>
<template>
<div class="addPage">
<!-- 顶部区域展示 -->
<div class="top-title">
<div class="bread-item">
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in match"
:key="index"
>{{ $t(item?.meta?.label) }}</el-breadcrumb-item
>
</el-breadcrumb>
</div>
</div>
<!-- // 内容区 -->
<div class="main-content pt-3">
<!-- 主体内容:默认插槽 -->
<slot></slot>
<!-- 底部操作栏:具名插槽 -->
<div class="bottom">
<el-divider />
<template v-if="slots.bottom">
<!-- 外部自定义底部内容-->
<slot name="bottom"> </slot>
</template>
<template v-else>
<div class="btn-content">
<ElButton
v-if="showConfirm"
:style="{ width: '100px' }"
@click="() => router.back()"
>{{ $t(cancelText) }}</ElButton
>
<ElButton
v-if="showCancel"
type="primary"
:style="{ width: '100px' }"
@click="() => emits('submit')"
>{{ $t(confirmText) }}</ElButton
>
</div>
</template>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.addPage {
height: 100vh;
width: 100vw;
position: relative;
/* // 新增编辑页覆盖所有展示 */
background-color: #f5f5f5;
:deep(.el-breadcrumb__inner) {
color: white !important;
}
:deep(.el-divider--horizontal) {
margin-bottom: 0 !important;
}
}
.bread-item {
margin-top: 5px;
}
.top-title {
padding: 10px;
font-size: 18px;
background-color: black;
display: flex;
align-items: center;
gap: 10px;
}
.main-content {
width: 56%;
margin: 0 auto;
background-color: white;
height: calc(100% - 50px);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
> :first-child:not(.bottom) {
flex: 1;
overflow-y: auto;
padding: 12px 12px 80px;
}
.bottom {
position: sticky;
bottom: 0;
width: 100%;
height: 80px;
text-align: center;
line-height: 50px;
background: white;
z-index: 1;
}
.btn-content {
margin: 0 auto;
}
}
.addPage ::-webkit-scrollbar {
display: none !important;
}
</style>
const codeFormRef: any = ref(null)
const schema = [
{
prop: 'age',
label: '年龄',
component: 'Input',
componentProps: {
type: 'number',
}
},
rules: [
{
required: true,
message: '请输入年龄',
trigger: 'blur'
}
]
},
{
prop: 'csa',
label: '阶段',
component: 'Select',
componentProps: {
disabled: true,
options: [
{
label: '儿童',
value: 1
},
{
label: '青年',
value: 2
},
{
label: '老年',
value: 3
}
]
}
},
]
// 确认函数
const submit = async () => {
const isVaild = await codeFormRef.value.validate()
if (isVaild) {
const data = await codeFormRef.value.getData()
console.log(data, '数据::')
router.back()
} else {
console.log('表单验证失败')
}
}
// 引入CommonAddPage组件
<template>
<CommonAddPage @submit="submit">
<CodeForm
ref="codeFormRef"
:schema="schema"
:isCenter="true"
:labelWidth="'100px'"
>
</CodeForm>
</CommonAddPage>
</template>
进入页面:
触发表单校验:
以上就是新增编辑页的封装过程以及实战案例~
经过四篇文章的逐步拆解与实战,已经完整搭建了一套 “配置化驱动、高复用” 的 CRUD 开发体系。从动态表单的基础铺垫,到搜索重置组件、useTable Hooks 的核心封装,再到新增编辑页的闭环实现,每一步都围绕 “减少重复编码、统一开发规范、提升开发效率” 的核心目标,最终让后台管理系统的CRUD 转变为 “配置即开发”。
当然配置化的前提是:“高复用、少定制”!!
当前的配置化CRUD开发体系已覆盖 “列表 + 搜索 + 分页 + 新增 + 编辑” 核心流程,希望这套体系能为你提供实际的帮助,也欢迎根据自身业务场景进行调整与扩展。
专栏到此结束,感谢你的阅读!