Vue - 基于Element UI封装一个表格动态列组件

1 组件需求背景

在后台管理系统中,表格的使用频率非常高,统一封装表格动态列组件并全局注册使用,可大大提升代码的复用性可维护性

2 全局注册

  • src/plugins/index.js:
import columns from './columns/index'

export default {
  install(Vue) {
    // 动态列
    Vue.prototype.$columns = columns
  }
}

  • src/main.js:
import plugins from './plugins' // plugins
// 表格动态列组件
import DynamicColumn from '@/components/DynamicColumn'
Vue.component('DynamicColumn', DynamicColumn)
Vue.use(plugins)
  • 页面(Page.vue)中使用
 <DynamicColumn
       v-for="(ite, index) in columns"
       :key="index"
       :item="ite"
       :data-list="infoList"
 />

infoList: [],
columns: this.$columns.getColumns('investmentDecision_list'),

Vue注册组件相关内容可见我的另外一篇博客:Vue - 组件注册及其原理

3 具体组件相关代码

  • src/plugins/columns/index.js:
const columns = {
  getColumns: function(columnType) {
    const list = columnList[columnType].list
    const newColumn = []
    list.forEach((v, i) => {
      newColumn.push({
        ...v,
        visible: v.visible === undefined ? true : v.visible, // 是否显示字段
        prop: v.prop, // 字段key
        label: v.label, // 字段名称
        align: v.align, // 对齐方式
        sortable: v.sortable, // 是否排序
        inputType: v.inputType, // 输入方式
        fixed: v.fixed, // 是否固定列
        tip: v.tip, // 是否列名解析
        width: v.width, // 列宽
        unit: v.unit, // 后缀,如"%"等
        keepDecimals: v.keepDecimals, // 保留小数位
        formatThousand: v.formatThousand, // 是否格式化千分位
        division: v.division, // 是否需要除数,如格式化万则需要除10000
        class: v.class, // 样式
        style: v.style, // 样式
        disabled: v.disabled, // 禁用
        controls: v.controls,
        tagTypeStyle: v.tagTypeStyle, // tag标签风格,函数类型,返回一个字符串,参考值为:default | primary | success | info | warning | danger
        render: v.render, // 渲染函数,返回一个处理后的值展示到页面中
        min: v.min, // 最小值
        max: v.max, // 最大值
        routerLink: v.routerLink,
        chartType: v.chartType, // Echart 图表类型,参考值为:1代表柱形折线图,2代表饼图
        chartData: v.chartData, // Echart 图表数据
        chartKey: v.chartKey, // Echart 图表Id的后缀
        columnTagText: v.columnTagText // 列标签标识
      })
    })
    return newColumn
  }
}

export default columns
  • src/components/DynamicColumn.vue:
<!--动态列-->
<template>
  <!-- :key="`${item.prop}+${item.label}`" -->
  <!-- :show-overflow-tooltip="!item.chartType" -->
  <el-table-column
    v-if="item.visible"
    :key="item.prop==='secCode'?Math.random():`${item.prop}+${item.label}`"
    :prop="item.prop"
    :label="item.label"
    :fixed="item.fixed"
    :min-width="getCellWidth(item,dataList)"
    :align="item.align||'left'"
    :show-overflow-tooltip="true"
    :sortable="item.sortable"
  >
    <template slot="header">
      <el-tooltip
        v-if="item.tip"
        class="item"
        effect="dark"
        :content="item.tip"
        placement="top-start"
      >
        <span>
          <span v-if="item.valid" style="color: #ec051b; font-size: 1.5em">*</span>
          <span>{{ item.label }}</span>
          <i class="el-icon-question" />
        </span>
      </el-tooltip>
      <span v-else>
        <span v-if="item.valid" style="color: #ec051b; font-size: 1.5em">*</span>
        <span>{{ item.label }}</span>
      </span>
    </template>

    <template slot-scope="scope">
      <el-input
        v-if="item.inputType === 'input'"
        v-model="scope.row[item.prop]"
        size="mini"
        :placeholder="$t('pleaseEnter')"
        :disabled="item.disabled||(scope.row.disabled&&scope.row.disabled.includes(item.prop))"
        @change="onChange(scope.row,item)"
        @blur="onBlur(scope.row,item)"
      />
      <el-input-number
        v-else-if="item.inputType === 'number'"
        v-model="scope.row[item.prop]"
        size="mini"
        :placeholder="$t('pleaseEnter')"
        :precision="item.keepDecimals"
        :controls="item.controls"
        :min="item.min"
        :max="item.max"
        :disabled="item.disabled||(scope.row.disabled&&scope.row.disabled.includes(item.prop))"
        @change="onChange(scope.row,item)"
        @blur="onBlur(scope.row,item)"
      />
      <el-select
        v-else-if="item.inputType === 'select'"
        v-model="scope.row[item.prop]"
        :placeholder="$t('pleaseSelect')"
        size="mini"
        :disabled="item.disabled||(scope.row.disabled&&scope.row.disabled.includes(item.prop))"
        @click.native="lastValue = scope.row[item.prop]"
        @change="onChange(scope.row,item)"
      >
        <el-option
          v-for="ite in scope.row[item.prop + 'list'] || []"
          :key="ite.value"
          :label="ite.label"
          :value="ite.value"
          :disabled="ite.disabled"
        />
      </el-select>
      <el-date-picker
        v-else-if="item.inputType === 'date'"
        v-model="scope.row[item.prop]"
        :format="item.format||'yyyy-MM-dd'"
        :value-format="item.format||'yyyy-MM-dd'"
        :type="item.dateType||'date'"
        :placeholder="$t('pleaseSelect')"
        :disabled="item.disabled||(scope.row.disabled&&scope.row.disabled.includes(item.prop))"
        size="mini"
        @change="onChange(scope.row,item)"
      />
      <el-switch
        v-else-if="item.inputType === 'switch'"
        v-model="scope.row[item.prop]"
        :active-text="item.activeText"
        :inactive-text="item.inactiveText"
        :active-value="item.activeValue"
        :inactive-value="item.inactiveValue"
        @change="onChange(scope.row,item)"
      />
      <el-radio-group
        v-else-if="item.inputType === 'radio'"
        v-model="scope.row[item.prop]"
        :disabled="item.disabled||(scope.row.disabled&&scope.row.disabled.includes(item.prop))"
        @change="onChange(scope.row,item)"
      >
        <el-radio
          v-for="ite in scope.row[item.prop + 'List'] || []"
          :key="ite.value"
          :disabled="ite.disabled"
          :label="ite.value"
        >{{ ite.label }}</el-radio>
      </el-radio-group>
      <el-checkbox
        v-else-if="item.inputType === 'checkbox' && scope.row[item.prop]!==undefined"
        v-model="scope.row[item.prop]"
        :disabled="item.disabled||(scope.row.disabled&&scope.row.disabled.includes(item.prop))"
        @change="onChange(scope.row,item)"
      >{{ scope.row[item.prop+"Label"] }}</el-checkbox>
      <el-tag
        v-else-if="item.tagTypeStyle"
        :disable-transitions="true"
        :type="item.tagTypeStyle(scope.row,scope.row[item.prop])||'primary'"
      >{{ formatter(scope.row, item) }}</el-tag>
      <router-link
        v-else-if="!item.columnTagText && item.routerLink && item.routerLink(scope.row)"
        class="dy-router-link"
        :to="item.routerLink(scope.row)"
      >
        <span>{{ formatter(scope.row, item) }}</span>
      </router-link>
      <BarLineChart
        v-else-if="item.chartType==1 && scope.row[item.prop]"
        :dom-id="`${scope.row[item.chartKey]}${item.prop}`"
        :data="scope.row[item.prop]"
        height="50px"
        background-color="transparent"
      />

      <!-- <GridsBarLineChart
        v-else-if="item.chartType==2 && scope.row.chartData"
        :dom-id="`${scope.row[item.chartKey]}${item.prop}`"
        :data="scope.row.chartData"
        height="50px"
      />-->

      <div
        v-else-if="item.columnTagText"
        :class="item.class"
        :style="item.style"
        @click="handleClick(scope.row,item)"
      >
        <div v-if="item.routerLink && item.routerLink(scope.row)">
          <router-link class="dy-router-link" :to="item.routerLink(scope.row)">
            <span>{{ formatter(scope.row, item) }}</span>
          </router-link>
          <div style="margin-top: -8px;">
            <el-tag
              v-if="item.columnTagText(scope.row)"
              type="danger"
              size="mini"
            >{{ item.columnTagText(scope.row) }}</el-tag>
          </div>
        </div>
        <div v-else>{{ formatter(scope.row, item) }}</div>
      </div>

      <span
        v-else
        :class="item.class"
        :style="item.style"
        @click="handleClick(scope.row,item)"
      >{{ formatter(scope.row, item) }}</span>
    </template>
  </el-table-column>
</template>

<script>
import { moneyFormat, keepDecimals, isNumber } from '@/utils/utils'
import Sortable from 'sortablejs'
// 组件
import BarLineChart from '@/components/Echarts/BarLineChart'

export default {
  components: {
    BarLineChart
  },
  props: {
    // 非必传,只有存在多个表格时,该参数作为表格的唯一标识,主要用于拖拽
    tableSign: {
      type: String,
      default: null
    },
    // 所有的列字段,用于拖拽
    schemas: {
      type: Array,
      default: () => {
        return []
      }
    },
    // 不在schemas内,并且在动态列之前的列数
    empty: {
      type: Number,
      default: 0
    },
    // 列字段
    item: {
      type: Object,
      default: () => {
        return {}
      }
    },
    // 自定义格式化方法
    format: {
      type: Function,
      default: null
    },
    // 自定义格式化方法对应的字段
    formatColumns: {
      type: Function,
      default: () => {
        return []
      }
    },
    // 表格数据
    dataList: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      lastValue: undefined // 上次的值
    }
  },
  mounted() {
    // 表格拖拽方法
    if (this.schemas.length > 0) {
      this.columnDrop()
    }
  },
  methods: {
    // 计算列宽(因为element自适应的宽度还是存在部分字段名称换行情况,所以需要重新计算)
    getCellWidth(item, dataList) {
      const { label, width: defaultWidth } = item
      if (defaultWidth) {
        return defaultWidth
      }
      if (!label) {
        return 0
      }
      let renderTextMax = ''
      if (dataList.length > 0) {
        dataList.forEach((row) => {
          // 取内容文本的最大值
          const renderText = this.formatter(row, item) ?? ''
          this.calculateWidth(renderText) >
            this.calculateWidth(renderTextMax) && (renderTextMax = renderText)
        })
      }
      // 取表头和内容文本的较大值
      this.calculateWidth(label) > this.calculateWidth(renderTextMax) &&
        (renderTextMax = label)
      let width = 0
      const html = document.createElement('span')
      html.innerText = renderTextMax
      html.className = 'getTextWidth'
      document.querySelector('body').appendChild(html)
      width = document.querySelector('.getTextWidth').offsetWidth
      document.querySelector('.getTextWidth').remove()
      if (width > 260) {
        return 280
      }
      return width + 20
    },

    // 计算字符宽度
    calculateWidth(renderText) {
      let flexWidth = 0
      for (let i = 0; i < String(renderText).length; i++) {
        const char = renderText[i]
        if ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z')) {
          // 如果是英文字符,为字符分配8个单位宽度
          flexWidth += 8
        } else if (char >= '\u4e00' && char <= '\u9fa5') {
          // 如果是中文字符,为字符分配16个单位宽度
          flexWidth += 16
        } else {
          // 其他种类字符,为字符分配8个单位宽度
          flexWidth += 8
        }
      }
      return flexWidth
    },

    // change事件,向父组件传递字段和值
    onChange(row, item) {
      row.oldValue = this.lastValue // 传选择之前的值
      this.setItemAndValue(this.$parent, row, item, 'columnChange')
    },

    // 点击事件,icon点击向父组件传递对应的字段信息
    handleClick(row, item) {
      this.setItemAndValue(this.$parent, row, item, 'columnClick')
    },

    // 失去焦点事件,icon点击向父组件传递对应的字段信息
    onBlur(row, item) {
      this.setItemAndValue(this.$parent, row, item, 'onBlur')
    },

    /**
     * @function setItemAndValue 向父组件传递相应的字段和值
     * @param {Object} parent 父组件原型
     * @param {Object} item 字段信息
     * @param {*} value 值
     * @param {String} functionType 函数类型
     */
    setItemAndValue(parent, row, item, functionType) {
      if (parent && parent[functionType]) {
        parent[functionType](row, item)
      } else {
        if (parent.$parent) {
          this.setItemAndValue(parent.$parent, row, item, functionType)
        }
      }
    },

    // 格式化
    formatter(row, item) {
      // render优先级最高
      if (item.render) {
        return item.render(row, row[item.prop])
      }
      if (
        row[item.prop] === null ||
        row[item.prop] === undefined ||
        row[item.prop] === ''
      ) {
        // value为0或者false还是照样原来显示
        return '--'
      }
      let value = row[item.prop]
      if (this.format) {
        if (
          this.formatColumns.length > 0 &&
          this.formatColumns.includes(item.prop)
        ) {
          // 指定自定义格式化的字段
          return this.format(row, item, row[item.prop])
        } else {
          // 未指定的往下走
          value = this.format(row, item, row[item.prop])
        }
      }

      if (isNumber(value)) {
        if (item.keepDecimals || item.keepDecimals === 0) {
          // 保留小数位
          value = keepDecimals(value, item.keepDecimals, item.division)
        }
        if (item.formatThousand) {
          value = moneyFormat(value, item.division) // 千分位
        }
        if (item.unit) {
          // 是否带单位,如万
          value = value + item.unit
        }
      }
      return value
    },

    /**
     * 列拖拽
     */
    columnDrop() {
      // 页面多个表格 取到对应表格dom元素
      let wrapperClass = '.el-table__header-wrapper'
      if (this.tableSign) {
        const elClass = this.$parent.$el.getAttribute('class')
        if (!elClass.includes(this.tableSign)) {
          this.$parent.$el.setAttribute('class', `${elClass} ${this.tableSign}`)
        }
        wrapperClass = `.${this.tableSign} ${wrapperClass}`
      }
      const wrapperTr = document.querySelector(`${wrapperClass} tr`)

      this.sortable = Sortable.create(wrapperTr, {
        animation: 100, // 过渡动画
        delay: 0, // 延迟多久可以拖动
        onEnd: (evt) => {
          if (evt.oldIndex === evt.newIndex) return

          const overviewColumns = [...this.schemas]
          const visbleColumns = overviewColumns.filter((v) => v.visible) // 显示的列
          const empty = this.empty // 不在schemas内,并且在动态列之前的列数

          // 注意:动态列表包含visible为false数据,需要进行特殊筛选处理
          const oldItem = visbleColumns[evt.oldIndex - empty]
          const newItem = visbleColumns[evt.newIndex - empty]

          const realOldIndex = overviewColumns.findIndex(
            (item) => item.prop === oldItem.prop
          )
          const realNewIndex = overviewColumns.findIndex(
            (item) => item.prop === newItem.prop
          )
          overviewColumns.splice(realOldIndex, 1) // 删除原来位置的数据
          overviewColumns.splice(realNewIndex, 0, oldItem) // 在新的位置插入该数据

          this.$emit('changeColumn', overviewColumns)
        }
      })
    }
  }
}
</script>
<style lang="scss" scoped>
.el-table__row .el-form-item {
  margin-bottom: 0px !important;
}

.el-input,
.el-input.is-disabled,
.el-range-editor.el-input__inner,
.el-date-editor.el-input {
  width: 100%;
}

::v-deep.el-select > .el-input {
  width: 100%;
}

::v-deep.el-input-number .el-input {
  width: 100%;
}
.el-input-number {
  width: 100%;
}

.dy-router-link {
  color: #1890ff;
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  position: relative;

  &:hover:after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border-bottom: 1px solid #1890ff;
  }
}

::v-deep .el-table__row {
  border: 1px solid #1890ff;
  .el-table .cell.el-tooltip {
    display: flex;
    align-items: center;
  }
}
</style>

你可能感兴趣的:(前端,vue,vue.js,elementui)