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)中使用
<DynamicColumnv-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].listconst newColumn = []list.forEach((v, i) => {newColumn.push({...v,visible: v.visible === undefined ? true : v.visible, // 是否显示字段prop: v.prop, // 字段keylabel: 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, // 是否需要除数,如格式化万则需要除10000class: v.class, // 样式style: v.style, // 样式disabled: v.disabled, // 禁用controls: v.controls,tagTypeStyle: v.tagTypeStyle, // tag标签风格,函数类型,返回一个字符串,参考值为:default | primary | success | info | warning | dangerrender: 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-columnv-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-tooltipv-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-inputv-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-numberv-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-selectv-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-optionv-for="ite in scope.row[item.prop + 'list'] || []":key="ite.value":label="ite.label":value="ite.value":disabled="ite.disabled"/></el-select><el-date-pickerv-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-switchv-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-groupv-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-radiov-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-checkboxv-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-tagv-else-if="item.tagTypeStyle":disable-transitions="true":type="item.tagTypeStyle(scope.row,scope.row[item.prop])||'primary'">{{ formatter(scope.row, item) }}</el-tag><router-linkv-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><BarLineChartv-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"/><!-- <GridsBarLineChartv-else-if="item.chartType==2 && scope.row.chartData":dom-id="`${scope.row[item.chartKey]}${item.prop}`":data="scope.row.chartData"height="50px"/>--><divv-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-tagv-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><spanv-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 } = itemif (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 = 0const html = document.createElement('span')html.innerText = renderTextMaxhtml.className = 'getTextWidth'document.querySelector('body').appendChild(html)width = document.querySelector('.getTextWidth').offsetWidthdocument.querySelector('.getTextWidth').remove()if (width > 260) {return 280}return width + 20},// 计算字符宽度calculateWidth(renderText) {let flexWidth = 0for (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) returnconst 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>