在后台管理系统中,表格的使用频率非常高,统一封装表格动态列
组件并全局注册
使用,可大大提升代码的复用性
和可维护性
。
import columns from './columns/index'
export default {
install(Vue) {
// 动态列
Vue.prototype.$columns = columns
}
}
import plugins from './plugins' // plugins
// 表格动态列组件
import DynamicColumn from '@/components/DynamicColumn'
Vue.component('DynamicColumn', DynamicColumn)
Vue.use(plugins)
<DynamicColumn
v-for="(ite, index) in columns"
:key="index"
:item="ite"
:data-list="infoList"
/>
infoList: [],
columns: this.$columns.getColumns('investmentDecision_list'),
Vue注册组件相关内容可见我的另外一篇博客:Vue - 组件注册及其原理
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
<!--动态列-->
<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>