二次封装el-table实现表格行内编辑

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情

近期在做项目的时候遇到需要再表格内编辑的需求。考虑对el-table进行二次封装以实现上述需求。记录一下封装过程中遇到的问题及解决办法。

下面是效果图: wQBMIe.gif

组件封装

常规的做法

实现表格内边编辑的常规做法就是使用 el-table-column 的slot,自定义单元格中的内容。给每一行数据添加 isEditing 字段用于控制其是否使其处以编辑状态。

比如:

<el-table-column
    label="培训经历"
    :show-overflow-tooltip="true"
    >
    <template slot-scope="{ row }">
        <template v-if="row.isEditing">
            <el-input
            v-model="row.trainingInfo"
            placeholder="请输入培训经历"
            size="small"
            />
        </template>
        <span v-else>{{ row.trainingInfo }}</span>
    </template>
</el-table-column>
复制代码

当需要编辑的字段比较少的时候,这么做没什么问题,当字段很多的时候,这么写就比较繁琐了,尤其是,单元格内需要编辑的内容,不仅仅只需要输入框,还可能是时间选择器,下拉框,文件上传。

封装 el-table-cilumn

现考虑对 el-table-column 进行封装,做到传入一个类型使单元格处于不同的输入状态。

在封装中主要使用了 v-bind="$attrs" v-on="$listeners"

  • v-bind="$attrs" v-on="$listeners" 用于当vue中有多层组件嵌套, 且多层组件间需要相互传递数据。

  • v-bind="$attrs" 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。也就是说在父组件 TlTableColumn 中可以书写书写 el-table-column 的属性。

  • inheritAttrs: false 的作用是父组件中的 attribute 在子组件props中如果没有接受,使其不会作为根元素的属性节点。

  • v-on="$listener 使孙子组件接收爷爷组件传递的事件

下面是具体的封装的代码: TlTableColumn.vue

<template>
    <el-table-column v-bind="$attrs" v-on="$listeners">
        <template slot-scope="{ row }">
            <!-- 输入框 -->
            <template v-if="editType === 'input'">
                <el-input
                    v-if="row.isEditing"
                    v-model="row[prop]"
                    placeholder="请输入备注"
                    size="small"
                />
                <span v-else>{{ row[prop] }}</span>
            </template>
            <!-- 下拉框 -->
            <template v-else-if="editType === 'select'">
                <el-select
                    v-if="row.isEditing"
                    v-model="row[prop]"
                    filterable
                    placeholder="请选择"
                    size="mini"
                >
                    <el-option
                        v-for="item in options"
                        :key="item.ctCode"
                        :label="item.ctName"
                        :value="item.ctName"
                    />
                </el-select>
                <span v-else>{{ row[prop] }}</span>
            </template>
            <!-- 开始时间 -->
            <template v-else-if="editType === 'startTime'">
                <el-date-picker
                    v-if="row.isEditing"
                    v-model="row[prop]"
                    size="small"
                    type="month"
                    class="date-picker"
                    placeholder="开始时间"
                    format="yyyy.MM"
                    value-format="yyyy.MM"
                    :picker-options="startPicker(row)"
                />
                <span v-else>{{ row[prop] }}</span>
            </template>
            <!-- 结束时间 -->
            <template v-else-if="editType === 'endTime'">
                <el-date-picker
                    v-if="row.isEditing"
                    v-model="row[prop]"
                    size="small"
                    type="month"
                    class="date-picker"
                    placeholder="结束时间"
                    format="yyyy.MM"
                    value-format="yyyy.MM"
                    :picker-options="endPicker(row)"
                />
                <span v-else>{{ row[prop] }}</span>
            </template>
            <!-- 普通显示 -->
            <template v-else>
                <span>{{ row[prop] }}</span>
            </template>
        </template>
    </el-table-column>
</template><script>
export default {
    inheritAttrs: false,
    props: {
        prop: {
            type: String,
            default: '',
        },
        editType: {
            type: String,
            default: '',
        },
        options: {
            type: Array,
            default: () => [],
        },
    },
    methods: {
        //开始时间验证
        startPicker(row) {
            return {
                disabledDate(time) {
                    return time.getTime() > new Date(row.endTime);
                },
            };
        },
        //结束时间验证
        endPicker(row) {
            return {
                disabledDate(time) {
                    return time.getTime() < new Date(row.startTime);
                },
            };
        },
    },
};
</script><style></style>复制代码

1.v-bind="$attrs"v-on="$listeners"使el-table-column接收未被父组件应用的参数,与事件。入:父组件传入的label能被el-table-column获取到。

2.inheritAttrs:false,是为了防止一些不必要的问题发生。如果不加这行代码,我在父组件调用子组件时传了一个参数 type=‘idnex’。子组件没有应用它,而恰好type属性又是孙子组件(这里也就是el-table-column)的一个属性,那么孙子组件就会被覆盖。如果孙子组件的type=‘selection',那他就会被替换成index。

参数 描述
prop 必填,数据项
editType 必填:处于编辑状态的类型。可选值:input(输入框)
label 可填:表头
其他 el-table-column的原有参数均可接收
options 当editType为select时必填,用作下拉框的数据项

注意:表格数据tableData的每一行需要添加一个属性:isEditing:false

用法

import TlTableColumn from '@/components/TlTableColumn/index';
​
<el-table>
    <el-table-column type="index" width="50" label="序号" />
    
    <tl-table-column
        label="培训经历"
        prop="trainingInfo"
        :show-overflow-tooltip="true"
        editType="input"
    />
​
    <tl-table-column
        label="结束时间"
        prop="endTime"
        editType="endTime"
        min-width="115"
    />
</el-table
​
复制代码

对比常规的做法是不是简单了不少呢。

二次封装el-table组件

在如上组件的基础上进行el-table组件的封装。

效果:

wQjucj.gif

封装 TlTable.vue

为了最大程度的简化代码,按照上面的思路可以对el-table进行封装。

<template>
    <!-- 以下属性可以在父组件中直接调用 -->
    <!-- v-loading="loading"
        :data="tableData"
        fit
        border
        highlight-current-row
        @selection-change="handleSelectionChange" -->
    <el-table
        v-bind="$attrs"
        v-on="$listeners"
        style="width: 100%;"
        :header-cell-style="{
            background: '#eef1f6',
            color: '#606266',
            'text-align': 'center',
        }"
        :cell-style="{ 'text-align': 'center' }"
    >
        <el-table-column type="selection" width="40" />
        <el-table-column type="index" width="50" label="序号" />
        <template v-for="(th, key) in tableHeader">
            <!-- 可编辑列 -->
            <tl-table-column
                :key="key"
                v-if="th.editType"
                :prop="th.prop"
                :label="th.label"
                :fixed="th.fixed"
                :width="th.width"
                :min-width="th.minWidth"
                :editType="th.editType"
                :options="th.options"
            />
            <el-table-column
                :key="key"
                v-else
                :prop="th.prop"
                :label="th.label"
                :fixed="th.fixed"
                :width="th.width"
                :min-width="th.minWidth"
            />
        </template>
    </el-table>
</template><script>
import TlTableColumn from '@/components/TlTableColumn/index';
export default {
    inheritAttrs: false,
    components: { TlTableColumn },
    props: {
        tableHeader: {
            type: Array,
            default: () => [],
        },
    },
};
</script><style></style>复制代码

如何使用TlTable组件

<template>
    <div class="majorIssues">
        <options-bar
            :editFlag="editFlag"
            @handleDelete="handleDelete"
            @handleSubmit="handleSubmit"
            @handleCancle="handleCancle"
            @handleCreate="handleCreate"
            @handleEdit="handleEdit"
        />
        <tl-table
            v-loading="loading"
            :data="tableData"
            :tableHeader="tableHeader"
            fit
            border
            highlight-current-row
            ref="dragTable"
            @selection-change="handleSelectionChange"
        />
    </div>
</template><script>
import TlTable from '@/components/TlTable/index';
import OptionsBar from '../common/OptionsBar';
export default {
    components: { TlTable, OptionsBar },
    data() {
        return {
            userId: '',
            loading: false,
            tableData: [],
            multipleSelection: [],
            editFlag: false,
            currentRow: {},
            // ? 根据表格内容填写
            rowTemplate: {
                type: '',
                time: '',
                name: '',
                content: '',
                isEditing: true,
                isCreate: true,
            },
            tableHeader: [
                {
                    prop: 'type',
                    label: '类别',
                    editType: 'select',
                    options: [
                        { ctName: '留学', ctCode: '0' },
                        { ctName: '定居', ctCode: '1' },
                    ],
                },
​
                {
                    prop: 'time',
                    label: '时间',
                    editType: 'startTime',
                    options: [],
                },
​
                {
                    prop: 'name',
                    label: '名称',
                    editType: 'input',
                    options: [],
                },
​
                {
                    prop: 'content',
                    label: '内容',
                    editType: 'input',
                    options: [],
                },
            ],
        };
    },
    created() {
        this.getTableData();
    },
    methods: {
        handleSelectionChange(arr) {
            this.multipleSelection = arr;
        },
        // todo
        getTableData() {
            this.tableData = [];
            for (let i = 0; i < 5; i++) {
                let row = {
                    id: i,
                    type: String(i),
                    time: '2020.4.5',
                    name: 'niu' + i,
                    content: String(i),
                    isEditing: false,
                };
                this.tableData.push(row);
            }
        },
        handleEdit() {
            if (this.multipleSelection.length !== 1) {
                this.$message.warning('请选择一条数据');
                return;
            }
​
            if (this.multipleSelection[0].isLock === '1') {
                this.$message.warning('该条数据流程已发起,暂时不允许操作');
                return;
            }
            this.currentRow = this.multipleSelection[0];
            this.editFlag = true;
​
            let rowIndex = -1;
            this.tableData.forEach((v, index) => {
                if (v.id === this.currentRow.id) {
                    rowIndex = index;
                }
            });
​
            if (rowIndex !== -1) {
                this.tableData[rowIndex].isEditing = true;
                this.currentRow['index'] = rowIndex;
            }
        },
        handleCancle() {
            // 新增
            if (this.currentRow.isCreate) {
                this.tableData.pop();
            } else {
                // 编辑
                this.getTableData();
            }
            this.editFlag = false;
        },
        handleCreate() {
            if (this.editFlag) {
                this.$message({
                    type: 'warning',
                    message: '请取消编辑状态',
                });
                return;
            }
            this.tableData.push(this.rowTemplate);
            this.currentRow = this.rowTemplate;
            this.editFlag = true;
        },
        handleSubmit() {
            // 新增
            if (this.currentRow.isCreate) {
                // this.add(this.currentRow);
                // this.operationProcess(this.currentRow, '01'); // 新增流程
                console.log(this.currentRow);
            } else {
                // 编辑
                let index = this.currentRow.index;
                let row = this.tableData[index];
                let data = { ...row };
                console.log(data);
                // this.update(data);
                // this.operationProcess(data, '03'); // 编辑流程
            }
            this.editFlag = false;
        },
        handleDelete() {
            if (this.multipleSelection.length !== 1) {
                this.$message({
                    type: 'warning',
                    message: '请选中一条数据',
                });
                return;
            }
            if (this.multipleSelection[0].isLock === '1') {
                this.$message.warning('该条数据流程已发起,暂时不允许操作');
                return;
            }
            let id = this.multipleSelection[0].id;
​
            this.$confirm('删除不可恢复, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning',
            })
                .then(() => {
                    // this.deleteById(id);
                    // this.operationProcess({ id: id }, '02'); // 流程删除
                    // todo
                    console.log('删除操作', id);
                })
                .catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除',
                    });
                });
        },
    },
};
</script><style lang="less" scoped>
.majorIssues {
    overflow: auto;
    margin-right: 10px;
}
</style>复制代码

封装 OptionsBar.vue

这里把上边的操作按钮也封装了一下。

<template>
    <div class="optionBar">
        <el-button type="text" icon="el-icon-delete" @click="handleDelete">
            删除
        </el-button>
        <span v-if="editFlag">
            <el-button
                type="text"
                icon="el-icon-circle-check"
                @click="handleSubmit"
            >
                确认
            </el-button>
            <el-button
                type="text"
                icon="el-icon-refresh"
                class="cancel-btn"
                @click="handleCancle"
            >
                取消
            </el-button>
        </span>
        <span v-else>
            <el-button type="text" icon="el-icon-plus" @click="handleCreate">
                新增
            </el-button>
            <el-button type="text" icon="el-icon-edit" @click="handleEdit">
                编辑
            </el-button>
        </span>
    </div>
</template><script>
export default {
    props: {
        editFlag: {
            type: Boolean,
            default: false,
        },
    },
    methods: {
        handleDelete() {
            this.$emit('handleDelete');
        },
        handleSubmit() {
            this.$emit('handleSubmit');
        },
        handleCancle() {
            this.$emit('handleCancle');
        },
        handleCreate() {
            this.$emit('handleCreate');
        },
        handleEdit() {
            this.$emit('handleEdit');
        },
    },
};
</script><style lang="less" scoped>
.optionBar {
    display: flex;
    flex-direction: row-reverse;
    padding-right: 10px;
}
</style>复制代码

添加表格拖拽排序功能。

由于业务需求,还需要添加对表格的拖动排序功能。

表格拖拽排序使用了 sortablejs 插件。

import Sortable from 'sortablejs';
复制代码

添加参数

<tl-table
          v-loading="loading"
          :data="tableData"
          :tableHeader="tableHeader"
          fit
          border
          highlight-current-row
          show-index
          @selection-change="handleSelectionChange"
​
          // 添加如下参数      
          is-sort
          ref="dragTable"
          :isSortChange="isSortChange"
          @updateSort="updateSort"
/>
复制代码

添加表格拖拽排序功能

        // 回调
        updateSort() {
            console.log('updateSort');
        },
        // 初始化    
        setSort() {
            const el = this.$refs.dragTable.$el.querySelectorAll(
                '.el-table__body-wrapper > table > tbody'
            )[0];
            this.sortable = Sortable.create(el, {
                ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
                setData: function(dataTransfer) {
                    dataTransfer.setData('Text', '');
                },
                onEnd: evt => {
                    // 数据已经改变,不必手动更新了
                    // const targetRow = this.tableData.splice(evt.oldIndex, 1)[0];
                    // this.tableData.splice(evt.newIndex, 0, targetRow);
                    // const tempArr = this.tableData;
                    // this.tableData = [];
                    // this.tableData = tempArr;
                    
                    // for show the changes, you can delete in you code
                    const tempIndex = this.newList.splice(evt.oldIndex, 1)[0];
                    this.newList.splice(evt.newIndex, 0, tempIndex);
                    // console.log(this.oldList, this.newList);
                },
            });
        },
        
        getTableData() {
            this.loading = true;
            const params = {
                mainInfoId: this.userId,
                type: 30,
            };
            query(params).then(res => {
                this.loading = false;
                const list = res.data;
                if (Array.isArray(list) && list.length > 0) {
                    this.tableData = list.map(v => {
                        v.startTime = this.myformatDate(v.startTime);
                        v.endTime = this.myformatDate(v.endTime);
                        v.isEditing = false;
                        return v;
                    });
​
                    this.oldList = this.tableData.map(v => v.id);
                    this.newList = this.oldList.slice();
                }
            });
​
            this.$nextTick(() => {
                this.setSort();
            });
        },            
复制代码

猜你喜欢

转载自juejin.im/post/7110186128551968776