一:前言

在前三篇文章中,已完成配置化 CRUD 体系的核心铺垫 —— 通过动态表单组件避免了重复编写表单模板,用搜索重置组件实现了列表筛选的统一封装,再借助 useTable Hooks 搞定了列表查询与分页的通用逻辑,三者联动让列表页实现了 “配置即开发” 的高效模式。不过,在完整的 CRUD 流程中,新增与编辑功能也是不可或缺的一部分,接下来,我们将结合前序动态表单组件,实现统一的新增编辑页,完成配置化CRUD开发的完美闭环。

文章指路(一起食用,味道更加):

1.动态表单深度实践: juejin.cn/post/757946… ;

2.搜索重置组件:juejin.cn/post/760056… ;

3.useTable Hooks: juejin.cn/post/760068… ;

二:新增编辑页实现

统一的新增编辑页,是为了保持新增与编辑页面的统一风格,同时将重复的按钮操作进行统一的封装及事件暴露处理。

其核心设计思路可概括为:封装统一的顶部展示区域、底部操作栏区域(支持显示 / 隐藏控制),暴露默认插槽(主体内容)与具名插槽(顶部展示区域、底部操作区域),让重复逻辑内聚、个性化需求通过插槽灵活实现,既保证一致性,又不丧失扩展性。

三:代码实现

3.1 顶部区域:展示相关路由名称

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>

3.2 主体区域:暴露默认插槽

  <!-- 主体内容:默认插槽 -->
  <slot></slot>

3.3 底部操作栏区域 :默认操作栏 + 暴露默认插槽

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>

3.4 完整代码

<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>

四:实战使用

4.1 页面引入:

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>

4.2 运行截图:

进入页面:

触发表单校验:

以上就是新增编辑页的封装过程以及实战案例~

五:写在最后: 专栏总结 —— 配置化CRUD开发

经过四篇文章的逐步拆解与实战,已经完整搭建了一套 “配置化驱动、高复用” 的 CRUD 开发体系。从动态表单的基础铺垫,到搜索重置组件useTable Hooks 的核心封装,再到新增编辑页的闭环实现,每一步都围绕 “减少重复编码、统一开发规范、提升开发效率” 的核心目标,最终让后台管理系统的CRUD 转变为 “配置即开发”。

当然配置化的前提是:“高复用、少定制”!!

当前的配置化CRUD开发体系已覆盖 “列表 + 搜索 + 分页 + 新增 + 编辑” 核心流程,希望这套体系能为你提供实际的帮助,也欢迎根据自身业务场景进行调整与扩展。

专栏到此结束,感谢你的阅读!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com