效果
1. 无编辑权限:显示普通表格
2. 有编辑权限:根据配置显示编辑控件
3. 可以动态新增行,也可以动态新增列
核心代码
无权限情况的核心代码
<!-- 无编辑权限时显示普通表格 -->
<el-table
v-if="!hasEditPermission"
ref="table"
:border="border"
:key="tablekey"
fit
v-bind="$attrs"
:element-loading-text="loadingText"
:data="tableData"
:header-cell-style="headerCellStyles"
:cell-style="cellStyles"
v-on="$listeners"
>
<!-- 复选框 -->
<el-table-column
v-if="showSelection"
column-key="selection"
v-bind="$attrs"
type="selection"
width="35"
align="center"
fixed
/>
<!-- 序号 -->
<el-table-column
v-if="showIndex"
column-key="index"
label="序号"
type="index"
width="55"
align="left"
fixed
/>
<!-- 所有列可扩展 -->
<el-table-column
v-if="showExpand"
type="expand"
width="25"
align="center"
fixed
>
<slot
slot-scope="scope"
name="expand"
:elCol="scope.column"
:row="scope.row"
:rowIndex="scope.$index"
/>
</el-table-column>
<!-- 表头数据 -->
<el-table-column
v-for="(column, cIndex) in tableColumns"
:key="cIndex"
:align="column.align || 'center'"
:prop="column.prop"
:label="column.label"
:width="column.width"
:sortable="column.sortable || false"
:type="column.type"
:fixed="column.fixed || (cIndex === 0 && firstColumnFixed)"
v-bind="column.attrs"
v-on="column.listeners"
>
<!-- 表头的自定义插槽, 使用 slotHeaderName 和 slotHeaderIcon 定义-->
<template
v-if="column.slotHeaderName || column.slotHeaderIcon"
#header
>
<!-- 表头自定义插槽-->
<slot
v-if="column.slotHeaderName"
:name="column.slotHeaderName"
:row="column"
/>
<span v-if="column.slotHeaderIcon">{{ column.label }}</span>
<!-- 表头图标插槽:使用表头图标,默认添加弹出层,弹出层内容插槽名称为:表头插槽名称 + PopContent -->
<el-popover
v-if="column.slotHeaderIcon"
class="header-slot-popup"
placement="bottom"
:title="column.slotHeaderIconPopTitle || column.label"
:width="column.slotHeaderPopWidth"
trigger="click"
>
<div class="filter-content">
<slot
v-if="column.slotHeaderIcon"
class="filter-slot"
:name="column.slotHeaderPopName + 'PopContent'"
/>
</div>
<!-- 列对象中添加 slotHeaderIcon 属性,表示使用头部图标插槽-->
<svg-icon
v-if="column.slotHeaderIcon"
slot="reference"
class="header-icon-slot"
:class="column.slotHeaderIconClass"
:icon-class="column.slotHeaderIcon"
@click="headerIconClick(column)"
/>
</el-popover>
</template>
<!-- 插入自定义模板到列中 -->
<slot
v-if="column.textSlot"
slot-scope="scope"
:name="column.textSlotName || 'slotTextCol'"
:colConf="column"
:elCol="scope.column"
:row="scope.row"
:rowIndex="scope.$index"
:colIndex="cIndex"
:rowspan="column.rowspan"
/>
<!-- 扩展列插槽 -->
<template v-else-if="column.expandSlot">
<slot
v-if="column.expandSlot"
:name="column.expandSlot || 'slotExpend'"
:colConf="column"
:elCol="scope.column"
:row="scope.row"
:rowIndex="scope.$index"
:colIndex="cIndex"
:rowspan="column.rowspan"
/>
</template>
<!--列内容插槽 -->
<template v-else slot-scope="scope">
<div class="content">
<el-checkbox
v-if="column.checkbox"
:value="checked(scope.row, column)"
@change="checkboxChange($event, scope.row, column)"
>{{ scope.row[column.prop] }}</el-checkbox
>
<el-tooltip
v-else
class="tootip-container"
effect="dark"
placement="top"
:disabled="!column.tooltip"
>
<template slot="content">
<!-- 字符串长度大于 80 格式化 -->
<span
v-if="String(scope.row[column.prop]).length > 80"
:class="{ 'cursor-pointer hoveTitle': column.tooltip }"
v-html="formateTooltip(String(scope.row[column.prop]))"
/>
<span v-else>{{ String(scope.row[column.prop]) }}</span>
</template>
<p>
<span
:class="{ 'cursor-pointer hoveTitle': column.tooltip }"
>{{
column.formatter(
scope.row,
scope.column,
scope.row[column.prop],
scope.$index
)
}}</span
>
</p>
</el-tooltip>
</div>
</template>
</el-table-column>
<!-- 固定列数据 -->
<el-table-column
v-if="showButtons"
column-key="buttons"
:label="fixButConfig.label"
:align="fixButConfig.align"
:fixed="fixButConfig.fixed"
:width="fixButConfig.width"
>
<slot
slot="default"
slot-scope="scope"
name="buttons"
:$index="scope.$index"
:row="scope.row"
:column="scope.column"
/>
</el-table-column>
<!-- 默认插槽 -->
<slot />
</el-table>
有权限情况的核心代码
<!-- 有编辑权限时显示可编辑表格 -->
<el-form v-else :model="formModel" :rules="rules" :ref="formRef">
<el-table
ref="table"
:border="border"
:key="tablekey"
fit
v-bind="$attrs"
:element-loading-text="loadingText"
:data="tableData"
:header-cell-style="headerCellStyles"
:cell-style="cellStyles"
v-on="$listeners"
>
<!-- 复选框 -->
<el-table-column
v-if="showSelection"
column-key="selection"
v-bind="$attrs"
type="selection"
width="35"
align="center"
fixed
/>
<!-- 序号 -->
<el-table-column
v-if="showIndex"
column-key="index"
label="序号"
type="index"
width="55"
align="left"
fixed
/>
<!-- 删除行 -->
<el-table-column
v-if="hasEditPermission && showRemoveRow"
label=""
width="50"
align="center"
fixed
>
<template slot-scope="scope">
<el-button
icon="el-icon-remove-outline"
size="medium"
type="text"
circle
:disabled="data.length <= 1"
@click="removeRow(scope.$index)"
></el-button>
</template>
</el-table-column>
<!-- 可编辑列 -->
<el-table-column
v-for="(column, cIndex) in tableColumns"
:key="cIndex"
:align="column.align || 'center'"
:prop="column.prop"
:label="column.label"
:width="column.width"
v-bind="column.attrs"
>
<!-- 表头的自定义插槽, 使用 slotHeaderName 和 slotHeaderIcon 定义-->
<template
v-if="column.slotHeaderName || column.slotHeaderIcon"
#header
>
<!-- 表头自定义插槽-->
<slot
v-if="column.slotHeaderName"
:name="column.slotHeaderName"
:row="column"
/>
<span v-if="column.slotHeaderIcon">{{ column.label }}</span>
<!-- 表头图标插槽:使用表头图标,默认添加弹出层,弹出层内容插槽名称为:表头插槽名称 + PopContent -->
<el-popover
v-if="column.slotHeaderIcon"
class="header-slot-popup"
placement="bottom"
:title="column.slotHeaderIconPopTitle || column.label"
:width="column.slotHeaderPopWidth"
trigger="click"
>
<div class="filter-content">
<slot
v-if="column.slotHeaderIcon"
class="filter-slot"
:name="column.slotHeaderPopName + 'PopContent'"
/>
</div>
<!-- 列对象中添加 slotHeaderIcon 属性,表示使用头部图标插槽-->
<svg-icon
v-if="column.slotHeaderIcon"
slot="reference"
class="header-icon-slot"
:class="column.slotHeaderIconClass"
:icon-class="column.slotHeaderIcon"
@click="headerIconClick(column)"
/>
</el-popover>
</template>
<template #header v-else>
<span :class="{ 'required-label': isRequiredColumn(column) }">
{{ column.label }}
</span>
</template>
<template slot-scope="scope">
<el-form-item
:prop="`[${scope.$index}].${column.prop}`"
:rules="column.rules"
:style="column.style"
:class="column.className"
>
<!-- 只读字段 -->
<template v-if="!column.type && !column.slot">
<slot
v-if="column.textSlot"
:name="column.textSlotName || 'slotTextCol'"
:colConf="column"
:elCol="scope.column"
:row="scope.row"
:rowIndex="scope.$index"
:colIndex="cIndex"
:rowspan="column.rowspan"
/>
<el-tooltip
v-else
v-bind="column.attrs"
class="tootip-container"
effect="dark"
placement="top"
:disabled="!column.tooltip"
>
<template slot="content">
<!-- 字符串长度大于 80 格式化 -->
<span
v-if="String(scope.row[column.prop]).length > 80"
:class="{ 'cursor-pointer hoveTitle': column.tooltip }"
v-html="formateTooltip(String(scope.row[column.prop]))"
/>
<span v-else>{{ String(scope.row[column.prop]) }}</span>
</template>
<p>
<span
:class="{ 'cursor-pointer hoveTitle': column.tooltip }"
>{{
column.formatter(
scope.row,
scope.column,
scope.row[column.prop],
scope.$index
)
}}</span
>
</p>
</el-tooltip>
</template>
<!-- 可编辑字段-根据列类型渲染不同表单控件 -->
<template v-else>
<el-input
v-if="column.type === $const.DialogCompType.input"
v-model="data[scope.$index][column.prop]"
class="item-inputs"
:placeholder="placeholderFormate(column)"
:style="{ width: column.width || '100%' }"
v-bind="column.attrs"
v-on="column.listeners"
/>
<remote-search-selector
v-else-if="
column.type === $const.DialogCompType.remoteSearchSelector
"
:init-value="data[scope.$index][column.prop]"
class="item-inputs"
:placeholder="placeholderFormate(column)"
v-bind="column.attrs"
:options="column.optionList"
:show-value="column.showValue"
:remote-methods="column.remoteMethod"
:style="{ width: column.width || '100%' }"
v-on="column.listeners"
@valueChange="
remoteSearchValueChage($event, data[scope.$index], column)
"
/>
<el-input-number
v-else-if="column.type === $const.DialogCompType.number"
v-model="data[scope.$index][column.prop]"
class="item-inputs"
:style="{ width: column.width || '100%' }"
:placeholder="placeholderFormate(column)"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-input
v-else-if="column.type === $const.DialogCompType.textarea"
v-model="data[scope.$index][column.prop]"
class="item-inputs"
type="textarea"
:style="{ width: column.width || '100%' }"
:placeholder="placeholderFormate(column)"
v-bind="column.attrs"
:autosize="
column.attrs && column.attrs.autosize
? column.attrs.autosize
: true
"
v-on="column.listeners"
/>
<el-date-picker
v-else-if="column.type === $const.DialogCompType.datePicker"
v-model="data[scope.$index][column.prop]"
class="item-inputs"
:type="(column.attrs && column.attrs.type) || 'date'"
:placeholder="placeholderFormate(column)"
:style="{ width: column.width || '100%' }"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-date-picker
v-else-if="column.type === $const.DialogCompType.dateTime"
v-model="data[scope.$index][column.prop]"
class="item-inputs"
type="datetime"
:placeholder="placeholderFormate(column)"
:style="{ width: column.width || '100%' }"
v-bind="column.attrs"
v-on="column.listeners"
/>
<date-picker-wrap
v-else-if="
column.type === $const.DialogCompType.datePickerColor
"
:date="data[scope.$index][column.prop]"
class="item-inputs"
:placeholder="placeholderFormate(column)"
:style="{ width: column.width || '100%' }"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-date-picker
v-else-if="column.type === $const.FormCompType.daterange"
v-model="data[scope.$index][column.prop]"
:type="(column.attrs && column.attrs.type) || 'daterange'"
:placeholder="placeholderFormate(column)"
:style="{ width: column.width || '100%' }"
:picker-options="{ disabledDate: daterangeDisabledDate }"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-date-picker
v-else-if="
column.type === $const.FormCompType.datetimerange
"
v-model="data[scope.$index][column.prop]"
type="datetimerange"
:placeholder="placeholderFormate(column)"
:style="{ width: column.width || '100%' }"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-switch
v-else-if="column.type === $const.DialogCompType.switch"
v-model="data[scope.$index][column.prop]"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-dropdown
v-else-if="column.type === $const.DialogCompType.dropdown"
v-bind="column.attrs"
v-on="column.listeners"
>
<el-button v-bind="column.dropdownButtonsAttrs">
{{ column.dropdownLabel }}
<i class="el-icon-arrow-down el-icon--right" />
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="dItem in column.dropdownItem"
:key="dItem.label"
v-bind="dItem.attrs"
v-on="dItem.listeners"
>
{{ dItem.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<selector-wrap
v-else-if="column.type === $const.DialogCompType.select"
:item="column"
:data="data[scope.$index]"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-radio-group
v-else-if="column.type === $const.DialogCompType.radio"
v-model="data[scope.$index][column.prop]"
class="item-inputs"
:style="{ width: column.width || '100%' }"
v-bind="column.attrs"
v-on="column.listeners"
>
<el-radio
v-for="option in column.optionList"
:key="option.id || option.value"
:label="option.value"
v-bind="option.attrs"
v-on="option.listeners"
>
{{ option.label }}
</el-radio>
</el-radio-group>
<el-slider
v-else-if="column.type === $const.DialogCompType.slider"
v-model="data[scope.$index][column.prop]"
class="item-inputs"
v-bind="column.attrs"
v-on="column.listeners"
/>
<el-alert
v-else-if="column.type === $const.DialogCompType.alert"
v-bind="column.attrs"
/>
<el-divider
v-else-if="column.type === $const.DialogCompType.divider"
v-bind="column.attrs"
>
<span v-if="column.tips">{{ column.tips }}</span>
</el-divider>
<file-upload
v-else-if="column.type === $const.DialogCompType.files"
class="item-inputs"
v-bind="column.attrs"
v-on="column.listeners"
/>
<div
v-else-if="column.type === $const.DialogCompType.buttons"
class="dialog-buttons"
:style="{
display: 'flex',
'justify-content':
buttonsAlign[column.align] || 'flex-end',
}"
>
<el-button
v-for="but in column.buttons"
:key="but.label"
:type="but.type"
v-bind="but.attrs"
size="small"
@click="
submitValidate(but.validate, but.reset, but.event)
"
>
{{ but.label }}
</el-button>
</div>
<tinymce
v-else-if="column.type === $const.DialogCompType.editor"
v-model="data[scope.$index][column.prop]"
:height="column.height"
/>
<slot
v-else-if="column.slot"
:name="column.slotName || 'slotCol'"
:colConf="column"
:elCol="scope.column"
:row="scope.row"
:rowIndex="scope.$index"
:colIndex="cIndex"
:rowspan="column.rowspan"
/>
</template>
</el-form-item>
</template>
</el-table-column>
<!-- 增加列 -->
<el-table-column
v-if="hasEditPermission && showAddColumn"
label=""
width="45"
align="center"
fixed='right'
>
<template #header>
<el-button
icon="el-icon-circle-plus-outline"
size="medium"
type="text"
circle
@click="addColumn"
></el-button>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column
v-if="showButtons"
column-key="buttons"
:label="fixButConfig.label"
:align="fixButConfig.align"
:fixed="fixButConfig.fixed"
:width="fixButConfig.width"
>
<slot
slot="default"
slot-scope="scope"
name="buttons"
:$index="scope.$index"
:row="scope.row"
:column="scope.column"
/>
</el-table-column>
</el-table>
</el-form>
<!-- 在表格下方添加操作按钮 -->
<div v-if="hasEditPermission" class="row-actions">
<el-button
v-if="showAddRow"
type="text"
size="medium "
@click="addRow"
icon="el-icon-circle-plus-outline"
>增加行</el-button>
<el-button
v-if="showSave"
type="text"
size="medium "
@click="saveData"
icon="el-icon-circle-check"
>保存</el-button>
</div>
增减行列核心方法
// 增加行
addRow() {
const newRow = {};
this.columns.forEach((col) => {
newRow[col.prop] = ""; // 初始化空值
});
this.tempData = [...this.tempData, newRow]; // 更新 tempData
this.$emit("update:data", this.tempData);
// 不再强制刷新整个表格
// this.tablekey += 1;
},
// 验证表单
validateForm() {
return new Promise((resolve) => {
this.$refs[this.formRef].validate((valid) => {
if (valid) {
resolve(true);
} else {
this.$message.error("请检查表单填写是否正确");
resolve(false);
}
});
});
},
// 删除行
removeRow(index) {
this.tempData.splice(index, 1);
this.$emit("update:data", [...this.tempData]);
// 不再强制刷新整个表格
// this.tablekey += 1;
},
// 保存数据
async saveData() {
const isValid = await this.validateForm();
this.$emit("update:data", [...this.tempData]);
return isValid;
},
// 比较列配置是否相等
isColumnsEqual(newColumns, oldColumns) {
if (newColumns.length !== oldColumns.length) return false;
return newColumns.every((newCol, index) => {
const oldCol = oldColumns[index];
return JSON.stringify(newCol) === JSON.stringify(oldCol);
});
},
// 新增列
async addColumn() {
try {
this.$emit('add-column', this.columns);
this.$nextTick(() => {
if (this.$refs.table) {
this.$refs.table.doLayout();
}
});
} catch (error) {
this.$message.error('新增列操作出错,请稍后重试');
}
},