还在写重复的增删改查的重复代码?还在重复写Ajax请求?Vue2+Element-ui实现常规后台查询展示页面来了

29 阅读4分钟

本来想直接用vue3来实现这个组件的,但是公司原先是在vue2+element的基础上封装的这个页面规格的组件。先把这个更新后下篇更新vue3+element-plus实现常规页面组件。

个人博客地址:web-miji.com/article?id=…

源码地址github.com/fengligao/v…

下面我们先来看下目录结构:

vue2-template目录.png

常用的组件我把它放到components文件下面。通过全局注册达到全局便捷使用无需多处引用。components/index.js代码:

import Vue from 'vue'

import TemplatePage from './template-page/index'

Vue.component('miji-template-page', TemplatePage) // 大家可以根据自己项目特性或者喜欢的特殊标识添加前缀

main.js的代码如下:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'element-ui/lib/theme-chalk/index.css'
import './plugins/element.js'

import './components/index'

因为使用element我们也要把element引入引来。

实现常规页面的组件封装我们先来梳理下页面中分别都有什么功能?

分页展示,条件查询,新增,编辑,删除。常规的操作就这几个,那么我们就有了实现思路。

我们先来来看下页面效果:

WeChat3ff132b50401a7e506dd81917a87b819.jpg

我们先来实现页面的基本框架。

首先我们在components下创建一个template-page的文件夹,在它下面创建index.js这是我们页面的主代码文件。不是用.vue template模版的方式是因为jsx通过函数条件来动态生成想要的dom会节省大量的html代码。让我们代码的可读性更高。逻辑也更清楚,而不是使用大量的if else来判断页面展示逻辑。

  render() {
    return (
      <div class="page">
        <div class="page-form">
          条件筛选区域
        </div>
        <div class="page-option">
          操作区域
        </div>
        <div class="page-container">
          内容区域
        </div>
      </div>
    )
  }

以上就是大致框架,分为三部分:条件查询区,其他操作区,内容展示区。

WeChatecc1983bfcc3862109554a97ec42dccb.png

我们首先来看看条件筛选区域的实现:

首先这里根据外部传入的表单数组来渲染对应的筛选控件。我们默认定制好常用的几种:input、select、checkbox、radio、datePicker。其他的后续可以自行扩展。然后表格根据传入的列数组动态渲染。操作区域添加插槽代码。分页,数据请求等操作都通过动态配置来实现。

下面看看详细代码:
import './index.less'
const tableListJson = [{
  date: '2016-05-02',
  name: '王小虎',
  address: '上海市普陀区金沙江路 1518 弄',
  age: 18,
  status: 0
}, {
  date: '2016-05-04',
  name: '王小虎',
  address: '上海市普陀区金沙江路 1517 弄',
  age: 19,
  status: 1
}, {
  date: '2016-05-01',
  name: '王小虎',
  address: '上海市普陀区金沙江路 1519 弄',
  age: 20,
  status: 1
}, {
  date: '2016-05-03',
  name: '王小虎',
  address: '上海市普陀区金沙江路 1516 弄',
  age: 21,
  status: 0
}]
const pickerOptions = {
  shortcuts: [{
    text: '最近一周',
    onClick(picker) {
      const end = new Date();
      const start = new Date();
      start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
      picker.$emit('pick', [start, end]);
    }
  }, {
    text: '最近一个月',
    onClick(picker) {
      const end = new Date();
      const start = new Date();
      start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
      picker.$emit('pick', [start, end]);
    }
  }, {
    text: '最近三个月',
    onClick(picker) {
      const end = new Date();
      const start = new Date();
      start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
      picker.$emit('pick', [start, end]);
    }
  }]
}

export default {
  props: {
    // 筛选标准字段有以下几个:
    // form[<Object>]
    // type 控件类型 // input, select, date, radio, checkbox
    // key 请求参数key
    // label 控件名称
    // value 控件值
    // defaultValue: 默认值
    // placeholder 控件提示
    // disabled 是否禁用
    // options 数据项
    // getOptions 异步获取数据项
    // props 自定义数据项key
    // 时间日期控件会多以下几个字段
    // dateType 时期控件类型 datetime/datetimerange/daterange
    // format 日期显示格式
    // valueFormat 选中值格式
    // isPickerOptions 是否带快捷选项
    form: {
      type: Array,
      default: () => []
    },
    // 表格配置
    tableConfig: {
      type: Object,
      default: () => {
        return {
          title: '',
          rowKey: 'id'
        }
      }
    },
    // 请求配置
    requestConfig: {
      type: Object,
      required: true
    },
    // 请求方法
    requestMethod: {
      type: String,
      default: () => 'Get'
    },
    // 请求前 参数处理的回调
    beforeRequest: {
      type: Function,
      default: () => null
    },
    // 请求后 数据处理的回调
    afterResponse: {
      type: Function,
      default: () => null
    },
    // 加载提示配置
    loadingConfig: {
      type: Object,
      default: () => {
        return {
          text: '拼命加载中',
          spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.8)'
        }
      }
    },
    // 表格列配置
    // 单元格的配置列字段包含以下几种:
    // column[Object]
    // label 单元格名称
    // prop 单元格数据展示key
    // type 单元格类型 text map option
    // slot 如果是插槽 单元格插槽名称
    // map 单元格 枚举对象
    // fixed 单元格是否固定
    // showOverflowTooltip 单元格内容是否超出省略 鼠标悬浮提示
    // options 操作类型的 按钮组字段
    columns: {
      type: Array,
      default: () => []
    },
    // 是否显示分页
    paginationShow: {
      type: Boolean,
      default: () => true
    },
    // 分页是否置底
    paginationFixed: {
      type: Boolean,
      default: () => false
    }
  },
  data() {
    // 初始化表单筛选项
    const formList = this.initForm(this.form)
    return {
      formList,
      tableData: [], // 表格数据
      loading: false,
      pagination: {
        pageNo: 1, // 当前页
        pageSize: 20, // 页码大小
        total: 0, // 总条数
        pageSizes: [10, 20, 50, 100] //每页显示条数
      }
    }
  },
  mounted() {
    console.log(this.formList);
    this.onSearch()
  },
  methods: {
    // 初始化表单数据
    initForm(form) {
      const formItems = []
      for (let i = 0; i < form.length; i++) {
        const formItem = form[i]
        switch (formItem.type) {
          case 'input':
            formItem.value = formItem.defaultValue || ''
            break;
          case 'select':
            formItem.value = formItem.defaultValue || ''
            formItem.options = formItem.options || [];

            if (typeof formItem.getOptions === 'function') {
              formItem.getOptions().then(res => {
                formItem.options = res
              })
            }
            break;
          case 'checkbox':
            formItem.value = formItem.defaultValue || []
            if (!formItem.options) {
              formItem.options = []
            }
            if (typeof formItem.getOptions === 'function') {
              formItem.getOptions().then((options) => {
                formItem.options = options;
              });
            }
            break;
          case 'radio':
            formItem.value = formItem.defaultValue || ''
            if (typeof formItem.getOptions === 'function') {
              formItem.getOptions().then((options) => {
                formItem.options = options;
              });
            }
            break;
          case 'date':
            formItem.value = formItem.defaultValue || ''
            if (formItem.isPickerOptions) {
              formItem.pickerOptions = pickerOptions
            }
            break;
          default:
            break;
        }
        formItems.push(formItem)
      }
      return formItems
    },
    // 分页操作
    handleCurrentChange(pageIndex) {
      this.pagination.pageNo = pageIndex;
      this.onSearch();
    },
    handleSizeChange(pageSize) {
      this.pagination.pageSize = pageSize;
      this.onSearch();
    },
    //数据请求触发方法
    onSearch() {
      const page = {
        pageNo: this.pagination.pageNo,
        pageSize: this.pagination.pageSize
      };
      let form = {}
      this.formList.forEach(v => {
        form[v.key] = v.value
      })

      if (this.paginationShow) {
        Object.assign(form, page);
      }

      console.log(form);
      let newForm = this.beforeRequest(form)
      console.log('请求接口入参:', newForm);
      // 数据请求的操作
      if (this.requestMethod === 'Get') {
        this.loading = true
        let params = { ...newForm }
        console.log(params);
        setTimeout(() => {
          let data = {
            code: 1,
            data: tableListJson,
            msg: '请求成功'
          }
          let newData = this.afterResponse ? this.afterResponse(data) : data
          console.log('请求返回值:', newData);
          // requestConfig.url 请求的地址 等其他操作可自行扩展
          this.tableData = newData.data
          this.pagination.total = 30
          this.loading = false
        }, 2000)
      } else if (this.requestMethod === 'Post') {
        // post请求同理的操作
        // return ...
      } else {
        this.$notify.warning({
          title: '提示',
          message: '仅支持Get或Post请求'
        })
      }
    },
    // 筛选操作函数
    onChangeFormItem(value, item, i) {
      console.log(value, item, i);
      this.formList[i].value = value
    },
    // 筛选input渲染
    renderInput(item, index) {
      return <el-form-item class="page-form__item" label={item.label}>
        <el-input
          class="page-form__item--input"
          type="text"
          value={item.value}
          placeholder={item.placeholder}
          onInput={(value) => this.onChangeFormItem(value, item, index)}
          disabled={item.disabled}>
        </el-input>
      </el-form-item>
    },
    // 筛选select渲染
    renderSelect(item, index) {
      return <el-form-item class="page-form__item" label={item.label}>
        <el-select
          value={item.value}
          placeholder={item.placeholder}
          onChange={(value) => this.onChangeFormItem(value, item, index)}
          disabled={item.disabled}>
          {
            item.options.map((v, i) => {
              return <el-option
                key={i}
                label={item.props && item.props.label ? v[item.props.label] : v.label}
                value={item.props && item.props.value ? v[item.props.value] : v.value}>
              </el-option>
            })
          }
        </el-select>
      </el-form-item>
    },
    // 筛选checkBox
    renderCheckBox(item, index) {
      return (
        <el-form-item class="page-form__item" label={item.label}>
          <el-checkbox-group
            value={item.value}
            onInput={(value) => this.onChangeFormItem(value, item, index)}
            disabled={item.disabled}>
            {
              item.options.map(v => {
                return <el-checkbox label={item.props && item.props.label ? v[item.props.label] : v.label} key={item.props && item.props.value ? v[item.props.value] : v.value}></el-checkbox>
              })
            }
          </el-checkbox-group>
        </el-form-item>
      )
    },
    // 筛选radio
    renderRadio(item, index) {
      return (
        <el-form-item class="page-form__item" label={item.label}>
          <el-radio-group
            value={item.value}
            onInput={(value) => this.onChangeFormItem(value, item, index)}
            disabled={item.disabled}>
            {
              item.options.map(v => {
                return <el-radio label={item.props && item.props.value ? v[item.props.value] : v.value}>{item.props && item.props.label ? v[item.props.label] : v.label}</el-radio>
              })
            }
          </el-radio-group>
        </el-form-item>
      )
    },
    // 筛选datePicker
    renderDatePicker(item, index) {
      return (
        <el-form-item class="page-form__item" label={item.label}>
          <el-date-picker
            value={item.value}
            type={item.dateType || 'date'}
            placeholder={item.placeholder}
            format={item.format || 'yyyy-MM-dd'}
            value-format={item.valueFormat || 'yyyy-MM-dd'}
            pickerOptions={item.dateType === 'datetimerange' && item.pickerOptions}
            onInput={(date) => this.onChangeFormItem(date, item, index)}
            disabled={item.disabled}>
          </el-date-picker>
        </el-form-item>
      )
    },
    // 表单渲染
    renderForm() {
      if (this.formList.length === 0 && !this.$scopedSlots.form) {
        return null
      }
      if (this.$scopedSlots.form && this.$scopedSlots.form()) {
        return <div>{this.$scopedSlots.form()}</div>
      }
      let formItems = []
      for (let i = 0; i < this.formList.length; i++) {
        const formItem = this.formList[i]
        switch (formItem.type) {
          case 'input':
            console.log(this.renderInput(formItem, i));
            formItems.push(this.renderInput(formItem, i))
            break;
          case 'select':
            formItems.push(this.renderSelect(formItem, i))
            break;
          case 'checkbox':
            formItems.push(this.renderCheckBox(formItem, i))
            break;
          case 'radio':
            formItems.push(this.renderRadio(formItem, i))
            break;
          case 'date':
            formItems.push(this.renderDatePicker(formItem, i))
            break;
          default:
            break;
        }
      }
      return <el-Form class="page-form">
        {formItems}
        <el-form-item class="margin-left-100px">
          <el-button type="primary" onClick={this.onSearch}>查 询</el-button>
          <el-button>重 置</el-button>
        </el-form-item>
      </el-Form>
    },
    // 表格单元格渲染
    renderTableColumn() {
      return this.columns.map(col => {
        const { fixed = null, showOverflowTooltip } = col

        const length = col.options && col.options.length || null

        const headerSlot = this.genHeaderSlot(col); // 自定义表头
        const defaultSlot = this.genDefaultSlot(col); // 自定义表格单元格
        const tableCellSlot = {
          scopedSlots: {
            ...headerSlot,
            ...defaultSlot,
          }
        }
        // 表格序号渲染
        if (col.type === '#') {
          return <el-table-column
            type="text"
            label={col.label}
            className={col.className}
            width={col.width || 60}
            fixed={fixed}
            formatter={(row, column, cellValue, index) => {
              return index + 1;
            }}
            {...tableCellSlot}>
          </el-table-column>
        }
        switch (col.type) {
          case 'options':
            return <el-table-column
              prop={col.prop}
              label={col.label}
              className={col.className}
              width={col.width}
              show-overflow-tooltip={showOverflowTooltip}
              fixed={fixed}
              formatter={(row) => {
                let btns, moreBtn
                if (length > 3) {
                  btns = col.options.slice(0, 2)
                  moreBtn = col.options.slice(2, length)
                } else {
                  btns = col.options
                }
                return length > 3 ? this.renderOptionsAndMore(btns, moreBtn, row) : this.renderOptions(btns, row)
              }}
            >
            </el-table-column>
          case 'map': // 枚举单元格
            return <el-table-column
              prop={col.prop}
              label={col.label}
              className={col.className}
              width={col.width}
              show-overflow-tooltip={showOverflowTooltip}
              fixed={fixed}
              formatter={(row) => {
                return col.map[row[col.prop]];
              }}
              {...tableCellSlot}
            >
            </el-table-column>
          default:
            return <el-table-column
              type="text"
              prop={col.prop}
              label={col.label}
              className={col.className}
              width={col.width}
              show-overflow-tooltip={showOverflowTooltip}
              fixed={fixed}
              {...tableCellSlot}>
            </el-table-column>
        }
      })
    },
    // 根据自定义表头的配置,动态生成需要的scopedSlot对象
    genHeaderSlot(col) {
      if (Object.prototype.hasOwnProperty.call(col, 'headerSlot')) {
        return {
          header: () => {
            return this.$scopedSlots.headerSlot();
          }
        };
      }
      return {};
    },
    // 自定义表格单元格的slot显示
    genDefaultSlot(col) {
      if (Object.prototype.hasOwnProperty.call(col, 'slot')) {
        return {
          // scope 是当前渲染单元格的数据
          default: scope => {
            return this.$scopedSlots[col.slot](scope);
          }
        }
      }
    },
    // 按钮时间函数
    buttonEvents(v, row) {
      if (v.clickEvent) {
        v.clickEvent(row)
      } else {
        this.$notify.warning({ title: '提示', message: '请传入clickEvent事件参数' })
      }
    },
    // 渲染按钮
    renderButton(v, row, type) {
      let color = 'page-template--button__' + v.type
      let className = type === 'more' ? 'page-template--button ' + color : color
      // 按钮的渲染操作 涵盖了样式类型自定义点击后的操作,包含按钮是否被禁用的回调等等
      return <el-button type="text" disabled={v.disabled && v.disabled(row)} class={className} onClick={() => this.buttonEvents(v, row)} style={{ color: v.color, display: v.isHide && v.isHide(row) ? 'none' : 'inline-block' }}>{v.text}</el-button>
    },
    // 表格操作渲染
    renderOptions(btns, row) {
      return btns && btns.map(v => {
        return this.renderButton(v, row)
      })
    },
    // 渲染更多按钮
    renderOptionsAndMore(btns, moreBtn, row) {
      return (<div>
        {this.renderOptions(btns, row)}
        {
          <el-dropdown>
            <el-button style={{ marginLeft: '10px' }} type="text">更多<i class="el-icon-arrow-down el-icon--right"></i></el-button>
            <el-dropdown-menu slot="dropdown">
              {moreBtn.map(v => {
                return <el-dropdown-item>
                  {this.renderButton(v, row, 'more')}
                </el-dropdown-item>
              })}
            </el-dropdown-menu>
          </el-dropdown>
        }
      </div>)
    },
    // 表格标题渲染 
    renderTableHead() {
      return this.tableConfig && this.tableConfig.title ? <h5>{this.tableConfig.title || ''}</h5> : null
    },
    // 表格内容区域渲染
    renderContainer() {
      const { contentSlots } = this.$scopedSlots;
      // 如果有自定义插槽默认展示自定义的内容
      return contentSlots ? <div>{contentSlots()}</div> : (
        <div>
          {this.renderTableHead()}
          <el-table
            data={this.tableData}
            style="width: 100%">
            {this.renderTableColumn()}
          </el-table>
        </div>
      )
    },
    // 分页渲染
    renderPagination() {
      return this.paginationShow ? (
        <div class={'page-pagination ' + (this.paginationFixed ? 'fixed' : '')}>
          <el-pagination
            on-size-change={this.handleSizeChange}
            on-current-change={this.handleCurrentChange}
            current-page={this.pagination.pageNo}
            page-size={this.pagination.pageSize}
            page-sizes={this.pagination.pageSizes}
            layout="total, prev, pager, next, sizes, jumper"
            total={this.pagination.total}
          >
          </el-pagination>
        </div>
      ) : null
    },
    // 页面操作区域渲染
    renderContentOpts() {
      const { contentOpts } = this.$scopedSlots;

      return (
        contentOpts ? <div class="page-option">
          {contentOpts()}
        </div> : null
      )
    },
    // 页面整体渲染
    renderPage() {
      return (
        <div>
          {this.renderForm()}
          {this.renderContentOpts()}
          <div
            class="page-container"
            v-loading={this.loading}
            element-loading-text={this.loadingConfig.text}
            element-loading-spinner={this.loadingConfig.spinner}
            element-loading-background={this.loadingConfig.background}>
            {this.renderContainer()}
          </div>
          {this.renderPagination()}
        </div>
      )
    }
  },
  render() {
    return (
      <div class="page">
        {this.renderPage()}
      </div>
    )
  }
}
上面实现组件后我们已经通过全局注册了页面组件,下面来看看在具体页面中的使用:
<template>
  <div class="home">
    <miji-template-page :tableConfig="{
      title: '表格标题'
    }" :form="form" requestMethod="Get" :requestConfig="{
      url: '/http/url'
    }" :beforeRequest="(param) => {
        return {
          ...param,
          otherKey: 'other key'
        }
      }" :afterResponse="(data) => {
        return {
          ...data,
          otherData: 'other data'
        }
      }" :paginationShow="true" :paginationFixed="false" :columns="[{
        type: '#',
        prop: '',
        label: '序号'
      }, {
        prop: 'name',
        label: '姓名'
      }, {
        prop: 'sex',
        label: '性别',
        slot: 'sex'
      }, {
        prop: 'date',
        label: '日期'
      }, {
        prop: 'address',
        label: '地址',
        showOverflowTooltip: true
      }, {
        prop: 'age',
        headerSlot: 'headerSlot',
        label: '年龄'
      }, {
        prop: 'status',
        type: 'map',
        label: '枚举的',
        map: {
          1: '成功',
          0: '失败'
        },
      }, {
        prop: 'age',
        type: 'options',
        label: '操作',
        width: 180,
        // 按钮的操作
        options: [
          {type: 'primary', text: '查看', clickEvent: (val) => this.view(val)},
          {type: 'warning', text: '编辑', clickEvent: (val) => this.edit(val)},
          {type: 'danger', text: '删除', clickEvent: (val) => this.del(val)},
          {type: 'info', text: '不能点的' },
          {type: 'info', text: '被禁用的', disabled: () => true },
        ]
      }]">
      <!-- 页面其他操作插槽 -->
      <template slot="contentOpts">
        <el-button type="primary" @click="add">新增</el-button>
        <el-button @click="outPut">导出</el-button>
      </template>
      <!-- 这是不使用默认表格 自定义内容的插槽 -->
      <!-- <template slot="contentSlots">
        <h1>自定义内容</h1>
      </template> -->
      <template slot="headerSlot">
        <span style="color: blue;">年龄-自定义</span>
      </template>
      <template slot="sex" slot-scope="scope">
        性别{{ scope.$index }}
      </template>
    </miji-template-page>
  </div>
</template>

<script>

export default {
  name: "HomeView",
  data() {
    return {
      form: [{
        type: 'input',
        key: 'searchKey',
        label: '搜索',
        value: '',
        defaultValue: '哈哈',
        placeholder: '请输入搜索内容',
        disabled: false
      }, {
        type: 'select',
        key: 'selectKey',
        label: '单选',
        value: '',
        defaultValue: '',
        placeholder: '请选择',
        // 默认的options
        // options: [{
        //   value: '1',
        //   label: '黄金糕'
        // }, {
        //   value: '2',
        //   label: '双皮奶'
        // }],
        // 自定义label value
        props: {
          label: 'name',
          value: 'id'
        },
        // 异步获取 options 数据
        getOptions: () => {
          return new Promise((resolve) => {
            setTimeout(() => {
              resolve([{
                id: '3',
                name: '蚵仔煎'
              }, {
                id: '4',
                name: '龙须面'
              }, {
                id: '5',
                name: '北京烤鸭'
              }])
            }, 3000)
          })
        },
        disabled: false
      }, {
        type: 'checkbox',
        key: 'checkBoxKey',
        label: '多选',
        value: '',
        defaultValue: ['上海'],
        placeholder: '请选择',
        options: [{
          value: '1',
          label: '上海'
        }, {
          value: '2',
          label: '北京'
        }],
        disabled: false
      }, {
        type: 'radio',
        key: 'radioKey',
        label: '单选',
        value: '',
        defaultValue: '1',
        placeholder: '请选择',
        options: [{
          value: '1',
          label: '开始'
        }, {
          value: '2',
          label: '停止'
        }],
        disabled: false
      }, {
        type: 'date',
        key: 'dateKey',
        label: '日期',
        value: '',
        defaultValue: '',
        dateType: 'datetimerange',
        format: 'yyyy-MM-dd HH:mm:ss',
        valueFormat: 'yyyy-MM-dd HH:mm:ss',
        placeholder: '请选择日期',
        isPickerOptions: true,
        disabled: false
      }]
    }
  },
  methods: {
    edit(val) {
      this.$notify.info({
        title: '提示',
        message: '编辑操作:' + JSON.stringify(val)
      })
    },
    del(val) {
      this.$notify.info({
        title: '提示',
        message: '删除操作:' + JSON.stringify(val)
      })
    },
    view(val) {
      this.$notify.info({
        title: '提示',
        message: '查看操作:' + JSON.stringify(val)
      })
    },
    add() {
      this.$notify.info({
        title: '提示',
        message: '新增操作'
      })
    },
    outPut() {
      this.$notify.info({
        title: '提示',
        message: '导出操作'
      })
    }
  }
};
</script>

<style>
.home {
  padding: 20px;
  height: 100%;
}
</style>

我们可以看到通过封装常用控件和表格展示方式后,在后台管理常规查询页面的开发中我们基本可以10分钟不到一个页面输出。

这样非常搞笑的提高我们的工作效率。

大家可以在这个常规组件的基础上进行扩展,添加新增删除等操作。

下一篇我会用Vue3来封装一个这样的常规页面组件,欢迎大家私信留言,多多点评。

可以扫描下方二维码关注我的公众号或添加我的微信 2545070038 联系沟通。