阅读 648

打造一款适合自己的快速开发框架-前端篇之框架分层及CURD样例

前言

和后端一样,前端也有对应的分层和CURD,本文主要是定制前端CURD模板样例,规范化代码,也为后续的代码生成器做铺垫。

功能分析

接口分析

我们先拿一个简单的CURD接口来做一下简单的分析,下面以用户管理接口为例:

功能 接口地址
添加用户 /sys/user/save
修改用户 /sys/user/update
删除用户 /sys/user/delete
通过id获取用户 /sys/user/get
分页查询用户列表 /sys/user/list

页面分析

功能 页面类型 操作
用户列表 页面 添加、修改、删除、查询(刷新)、重置
添加用户 弹窗 提交、取消
修改用户 弹窗 提交、取消

操作分析

这里的方法即js的方法。

方法名 方法说明
requestData 请求获取用户列表
getDetails 通过id获取用户信息
handleSearch 处理查询操作
handleReset 重置搜索表单
handleOpenAddDialog 打开添加用户弹出框
handleOpenEditDialog 打开修改用户弹出框
handleOpenDetailsDialog 打开用户详情弹出框
handleCancel 取消提交
handleSubmit 提交用户信息-添加/修改
handleRemove 删除操作

框架分层

上一篇对路由进行了模块化,也简单地对页面进行了模块化,本文会进一步对单个模块的页面进行说明。

目录结构

├── src/api
	├──	sys
		├──	sys.user.service.js	用户接口服务,对应sys-用户管理接口
		└── ...
	└── ...
├── src/views/modules
		├──	sys	系统管理模块
			├── user	用户管理
				├── componets
					├──	form.vue	添加/修改表单组件
					└──	search.vue	搜索表单组件
				├──	add.vue			添加-引入form.vue
				└──	details.vue		详情-仅显示
				├──	edit.vue		修改-引入form.vue
				└──	index.vue		用户管理页-引入add/details/edit/search
			└──	...
		└── ...
复制代码

上面就是对一个单表操作要创建的全部文件,其实就简单性而已,只放在一个index.vue上,CV操作会更方便点,不过因为后面会配套有代码生成器,所以这样分层也并不是很麻烦。

文件详解

  • src/views/modules/sys/user/index.vue

用户管理首页,这里可以重点看一下弹框的处理方式,使用到了vue的动态组件。

<template>
  <div class="app-container">
    <!--start========头部折叠面板===========start-->
    <el-collapse accordion value="1" style="padding:0px;">
      <el-collapse-item name="1">
        <template slot="title">
          <el-page-header title="返回" content="搜索条件" @back="goBack"></el-page-header>
        </template>
        <!--搜索模块-->
        <m-search ref="searchForm" @on-search="handleSearch" />
      </el-collapse-item>
    </el-collapse>
    <!--start========头部折叠面板===========start-->
    <!--start========顶部工具栏===========start-->
    <el-row :gutter="10" class="mb8 mt10">
      <el-col :span="1.5">
        <el-button type="primary" icon="el-icon-plus" size="small" @click="handleOpenAddDialog">
          添加
        </el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="success" icon="el-icon-edit" size="small" :disabled="single" @click="handleOpenEditDialog">
          修改
        </el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="danger" icon="el-icon-delete" size="small" :disabled="multiple" @click="handleRemove">
          删除
        </el-button>
      </el-col>
    </el-row>
    <!--end========顶部工具栏===========end-->
    <!--start========表格列表===========start-->
    <el-table stripe :header-cell-style="{background:'#eef1f6',color:'#606266'}" v-loading="loading" :data="tableData" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column prop="userName" label="用户名">
        <template slot-scope="scope">
          {{ scope.row.userName }}
        </template>
      </el-table-column>
      <el-table-column prop="realName" label="姓名">
        <template slot-scope="scope">
          {{ scope.row.realName }}
        </template>
      </el-table-column>
      <el-table-column prop="mobilePhone" label="手机号">
        <template slot-scope="scope">
          {{ scope.row.mobilePhone }}
        </template>
      </el-table-column>
      <el-table-column prop="sex" label="性别">
        <template slot-scope="scope">
          <span v-if="scope.row.sex === 1">男</span>
          <span v-else-if="scope.row.sex === 2">女</span>
          <span v-else>未知</span>
        </template>
      </el-table-column>
      <el-table-column prop="isLocked" label="是否锁定">
        <template slot-scope="scope">
          <span v-if="scope.row.isLocked === 1">是</span>
          <span v-else-if="scope.row.isLocked === 2">否</span>
        </template>
      </el-table-column>
      <el-table-column prop="createTime" label="创建时间">
        <template slot-scope="scope">
          {{ scope.row.createTime }}
        </template>
      </el-table-column>
      <el-table-column
        label="操作"
        align="center">
        <template slot-scope="scope">
		  <!-- click.native.stop==>阻止单击事件冒泡 -->
          <el-button type="text" size="small" icon="el-icon-view" @click.native.stop="handleOpenDetailsDialog(scope.row)">查看</el-button>
          <el-button type="text" size="small" icon="el-icon-edit" @click.native.stop="handleOpenEditDialog(scope.row)">修改</el-button>
          <el-button type="text" size="small" icon="el-icon-delete" @click.native.stop="handleRemove(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!--end========表格列表===========end-->
    <!--start========分页===========start-->
    <pagination
      v-show="recordCount>0"
      :total="recordCount"
      :page.sync="pageNum"
      :limit.sync="pageSize"
      @pagination="requestData"
    />
    <!--end========分页===========end-->
    <!--start========弹框===========start-->
    <el-dialog :title="title" :visible.sync="isOpenDialog" width="500px" append-to-body @close="handleCancel">
      <!--动态组件-->
      <component :ref="currentView" :is="currentView" :id="id"></component>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" v-if="showOk" :loading="submitLoading" @click="handleSubmit">确 定</el-button>
        <el-button @click="handleCancel">取 消</el-button>
      </div>
    </el-dialog>
    <!--end========弹框===========end-->
  </div>
</template>
<script>
// 搜索组件
import MSearch from './components/search'
// 添加
import Add from './add'
// 修改
import Edit from './edit'
// 详情
import Details from './details'
// 接口服务
import { list as listUser, remove as removeUser } from '@/api/sys/sys.user.service.js'

export default {
  components: {
    MSearch,
    Add,
    Edit,
    Details
  },
  data() {
    return {
      // 当前id
      id: undefined,
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 加载中
      loading: false,
      // 总记录数
      recordCount: 0,
      // 当前页
      pageNum: 1,
      // 每页大小
      pageSize: 10,
      // 列表数据
      tableData: [],
      // 当前勾选行id
      ids: [],
      // 当前勾选行集合
      selection: [],
      // 当前弹出框页面
      currentView: 'add',
      // 弹框标题
      title: '我是标题',
      // 是否打开弹出框
      isOpenDialog: false,
      // 是否显示弹出框确认按钮
      showOk: true,
      // 提交加载中
      submitLoading: false
    }
  },
  created() {
    this.requestData()
  },
  methods: {
    // 查询
    handleSearch() {
      this.requestData()
    },
    // 打开添加弹出框
    handleOpenAddDialog() {
      this.title = '添加用户'
      this.isOpenDialog = true
      this.showOk = true
      this.currentView = 'Add'
    },
    // 打开修改弹出框
    handleOpenEditDialog(row) {
      if (row.id) {
        this.id = row.id
      } else {
        this.id = this.ids.length ? this.ids[0] : row.id
      }
      this.title = '修改用户'
      this.isOpenDialog = true
      this.showOk = true
      this.currentView = 'Edit'
    },
    // 打开详情
    handleOpenDetailsDialog(row) {
      this.id = row.id
      this.title = '用户详情'
      this.isOpenDialog = true
      this.showOk = false
      this.currentView = 'Details'
    },
    // 删除
    handleRemove(row) {
      this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        var ids = []
        if (row.id) {
          ids.push(row.id)
        } else {
          ids = this.ids
        }
        removeUser({
          ids: ids
        }).then(res => {
          if (res.code === 0) {
            this.$message({ message: '删除成功', type: 'success' })
            this.requestData()
          } else {
            this.$message({ message: res.msg || '删除失败', type: 'error' })
          }
        })
      }).catch(() => {
        this.$message({ type: 'info', message: '已取消删除' })
      })
    },
    // 表格选中事件
    handleSelectionChange(selection) {
      this.selection = selection
      this.ids = selection.map(item => item.id)
      this.single = selection.length !== 1
      this.multiple = !selection.length
    },
    // 请求数据
    requestData(page) {
      if (!page) {
        page = {
          page: this.pageNum,
          limit: this.pageSize
        }
      }
      this.loading = true
      listUser({
        pageNum: page.page,
        pageSize: page.limit,
        ...this.$refs['searchForm'] ? this.$refs['searchForm'].getData() : {}
      }).then(res => {
        this.loading = false
        if (res.code === 0) {
          this.tableData = res.data.rows
          this.recordCount = res.data.recordCount
        }
      }).catch(() => {
        this.loading = false
      })
    },
    // 表单提交
    handleSubmit() {
      this.submitLoading = true
      this.$refs[this.currentView].submit().then(() => {
        this.submitLoading = false
        this.handleCancel()
        this.requestData()
      }).catch(() => {
        this.submitLoading = false
      })
    },
    // 取消提交
    handleCancel() {
      this.id = undefined
      this.isOpenDialog = false
    }
  }
}
</script>

复制代码
  • src/api/sys/sys.user.service.js

用户接口服务,对应sys-用户管理接口

import request from '@/utils/request'
/**
 * 添加用户
 * @param {*} data
 */
export function save(data) {
  return request({
    url: '/sys/user/save',
    method: 'post',
    data
  })
}
/**
 * 修改用户
 * @param {*} data
 */
export function update(data) {
  return request({
    url: '/sys/user/update',
    method: 'post',
    data
  })
}
/**
 * 删除用户
 * @param {*} data
 */
export function remove(data) {
  return request({
    url: '/sys/user/remove',
    method: 'post',
    data
  })
}
/**
 * 通过id获取用户
 * @param {*} data
 */
export function get(data) {
  return request({
    url: '/sys/user/get',
    method: 'post',
    data
  })
}
/**
 * 分页查询用户列表
 * @param {*} data
 */
export function list(data) {
  return request({
    url: '/sys/user/list',
    method: 'post',
    data
  })
}
复制代码
  • src/views/modules/sys/user/componets/form.vue
<template>
  <el-form ref="form" :model="form" :rules="rules" label-width="100px">
    <el-form-item label="用户名" prop="userName">
      <el-input v-model="form.userName" placeholder="请输入用户名"></el-input>
    </el-form-item>
    <el-form-item label="姓名" prop="realName">
      <el-input v-model="form.realName" placeholder="请输入姓名"></el-input>
    </el-form-item>
    <el-form-item label="邮箱" prop="email">
      <el-input v-model="form.email" placeholder="请输入邮箱"></el-input>
    </el-form-item>
    <el-form-item label="手机号" prop="mobilePhone">
      <el-input v-model="form.mobilePhone" placeholder="请输入手机号"></el-input>
    </el-form-item>
    <el-form-item label="性别" prop="sex">
      <el-select v-model="form.sex">
        <el-option label="男" :value="1"></el-option>
        <el-option label="女" :value="2"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="是否锁定" prop="isLocked">
      <el-select v-model="form.isLocked">
        <el-option label="是" :value="2"></el-option>
        <el-option label="否" :value="1"></el-option>
      </el-select>
    </el-form-item>
  </el-form>
</template>
<script>
import { save as saveUser, update as updateUser, get as getUser } from '@/api/sys/sys.user.service.js'

export default {
  props: {
    isEdit: {
      type: Boolean,
      default: false
    },
    id: {
      type: [String, Number],
      default: undefined
    }
  },
  data() {
    return {
      form: {
        id: undefined,
        userName: undefined,
        realName: undefined,
        email: undefined,
        mobilePhone: undefined,
        sex: 1,
        isLocked: 2
      },
      rules: {
        userName: [
          { required: true, message: '用户名不能为空', trigger: 'blur' }
        ],
        realName: [
          { required: true, message: '姓名不能为空', trigger: 'blur' }
        ],
        mobilePhone: [
          { required: true, message: '手机号不能为空', trigger: 'blur' }
        ],
        sex: [
          { required: true, message: '性别不能为空', trigger: 'blur' }
        ],
        isLocked: [
          { required: true, message: '是否锁定不能为空', trigger: 'blur' }
        ]
      }
    }
  },
  watch: {
    id(n, o) {
      // 因为共用一个dialog,所以需要通过监听id变化来调用接口
      this.getDetails()
    }
  },
  mounted() {
    this.getDetails()
  },
  methods: {
    submit(isShowMessage = 1) {
      // isShowMessage=>是否显示消息提示,默认1
      return new Promise((resolve, reject) => {
        this.$refs['form'].validate((valid) => {
          if (valid) {
            if (this.isEdit) {
              // 调用修改用户接口服务
              updateUser(this.form).then(res => {
                if (isShowMessage) {
                    if (res.code === 0) {
                      this.$message({
                        message: res.msg || '操作成功',
                        type: 'success'
                      })
                    }
                }
                resolve(res)
              }).catch(e => {
                reject(e)
              })
            } else {
              delete this.form.id
              // 调用添加用户接口服务
              saveUser(this.form).then(res => {
                if (isShowMessage) {
                    if (res.code === 0) {
                      this.$message({
                        message: res.msg || '操作成功',
                        type: 'success'
                      })
                    }
                }
                resolve(res)
              }).catch(e => {
                reject(e)
              })
            }
          } else {
            reject(new Error('error'))
          }
        })
      })
    },
    resetFields() {
      // 重置一下表单
      this.$refs['form'].resetFields()
      // 重置一下校验
      this.$refs['form'].clearValidate()
    },
    getDetails() {
      if (this.isEdit && this.id) {
        // 当然,肯定是修改模式下且id存在的情况下才会调用接口
        getUser({
          id: this.id
        }).then(res => {
          this.form = this.$util.copy(res.data, this.form)
        })
      }
    }
  }
}
</script>

复制代码
  • src/views/modules/sys/user/componets/search.vue

搜索组件这里使用特殊的带m_前辍的参数,目的是给全局的src/utils/request.js处理成后端的数据,如:

m_EQ_userName====>

{
    operateType: "EQ",
    propertyName: "userName",
    propertyValue: value
}
复制代码
<template>
  <el-form ref="form" :model="form" :inline="true">
    <el-form-item label="用户名" prop="m_EQ_userName">
      <el-input v-model="form.m_EQ_userName" placeholder="请输入用户名" size="small" style="width: 240px"></el-input>
    </el-form-item>
    <el-form-item label="姓名" prop="m_EQ_realName">
      <el-input v-model="form.m_EQ_realName" placeholder="请输入姓名" size="small" style="width: 240px"></el-input>
    </el-form-item>
    <el-form-item label="手机号" prop="m_EQ_mobilePhone">
      <el-input v-model="form.m_EQ_mobilePhone" placeholder="请输入手机号" size="small" style="width: 240px"></el-input>
    </el-form-item>
    <el-form-item label="是否锁定" prop="m_EQ_isLocked">
      <el-select v-model="form.m_EQ_isLocked" size="small">
        <el-option label="所有" :value="undefined">所有</el-option>
        <el-option label="是" :value="1"></el-option>
        <el-option label="否" :value="2"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="创建时间" prop="m_BT_createTime">
      <el-date-picker
        v-model="form.m_BT_createTime"
        size="small"
        style="width: 240px"
        value-format="yyyy-MM-dd"
        type="daterange"
        range-separator="-"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
      ></el-date-picker>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" icon="el-icon-search" size="small" @click="handleSearch">查询</el-button>
      <el-button icon="el-icon-refresh" size="small" @click="resetform">重置</el-button>
    </el-form-item>
  </el-form>
</template>
<script>
export default {
  props: {
  },
  data() {
    return {
      form: {
        m_EQ_userName: undefined,
        m_EQ_realName: undefined,
        m_EQ_mobilePhone: undefined,
        m_EQ_isLocked: undefined,
        m_BT_createTime: undefined
      }
    }
  },
  methods: {
    // 搜索
    handleSearch(e) {
      this.$emit('on-search', this.form, e)
    },
    // 重置
    resetform(e) {
      this.$refs['form'].resetFields()
    },
    // 获取表单数据
    getData() {
      var data = Object.assign({}, this.form)
      return data
    }
  }
}
</script>
复制代码
  • src/views/modules/sys/user/add.vue

添加页,这里主要引入./componts/form.vue组件

<template>
  <m-form ref="form" :is-edit="false" :id="id"></m-form>
</template>
<script>
import MForm from './components/form'

export default {
  components: { MForm },
  props: {
    id: {
      type: [String, Number],
      default: undefined
    }
  },
  methods: {
    submit() {
      return this.$refs.form.submit()
    },
    resetFields() {
      this.$refs.form.resetFields()
    }
  }
}
</script>
复制代码
  • src/views/modules/sys/user/details.vue

详情组件,暂时还是使用./componts/form.vue组件

<template>
  <m-form ref="form" :is-edit="true" :id="id"></m-form>
</template>
<script>
import MForm from './components/form'

export default {
  components: { MForm },
  props: {
    id: {
      type: [String, Number],
      default: undefined
    }
  },
  methods: {
  }
}
</script>

复制代码
  • src/views/modules/sys/user/edit.vue

修改页,同添加页一样引入./componts/form.vue组件

<template>
  <m-form ref="form" :is-edit="true" :id="id"></m-form>
</template>
<script>
import MForm from './components/form'

export default {
  components: { MForm },
  props: {
    id: {
      type: [String, Number],
      default: undefined
    }
  },
  methods: {
    submit() {
      return this.$refs.form.submit()
    },
    resetFields() {
      this.$refs.form.resetFields()
    }
  }
}
</script>

复制代码
  • src/utils/request.js

这里新增了特殊的请求参数处理,代码片段

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent
    if (store.getters.token) {
      // 存在token,就放到请求头中
      // 这里修改一下请求头与后端一致,X-Token->Auth-Token
      config.headers['Auth-Token'] = getToken()
    }
    if (config.data) {
      // 这里对全局的请求参数做处理,主要是拼接查询条件
      var whereParams = []
      Object.keys(config.data).forEach(item => {
        if (item.startsWith('m_')) {
          // 处理以m_开头的参数
          var value = config.data[item]
          if (value) {
            var arr = item.split('_')
            if (arr.length === 3) {
              whereParams.push({
                operateType: arr[1],
                propertyName: arr[2],
                propertyValue: value
              })
            }
          }
          // 处理完后就删掉
          delete config.data[item]
        }
      })
      if (whereParams.length) {
        config.data.whereParams = whereParams
      }
    }

    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)
复制代码

效果图

  • 列表页

  • 添加弹框

  • 修改弹框

  • 查看详情(还未细做)

小结

本文只是先简单地过一下前端的CURD模板样例,现在这个版本的页面还不是最终版本,还不能作为代码生成器的基础模板,后面的文章会更细化来讲解。这里先简单做个预告:

  1. 如何优雅的处理表单的一行一列、一行多列;

  2. 如何制作一些常用的业务组件,如上传、字典、下拉关联表(单选、多选)、富文本等

  3. 按钮级权限的设计

  4. 当然,最最核心的还是如何做前端的代码生成器

  5. ……

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-前端脚手架搭建

打造一款适合自己的快速开发框架-前端篇之登录与路由模块化