美文网首页前端工程化
el-table动态渲染列、可编辑单元格、虚拟无缝滚动

el-table动态渲染列、可编辑单元格、虚拟无缝滚动

作者: z_hboot | 来源:发表于2022-05-01 15:42 被阅读0次

    针对日常开发的组件二次封装、方案设计实现。包括对el-table的动态渲染、单元格编辑;对于无缝滚动的实现,优化大数据量下的页面卡顿问题。

    1. el-table实现动态渲染列

    常规使用el-table

    <template>
      <el-table
        ref="multipleTable"
        :data="data"
      >
        <el-table-column prop="family_name" label="姓名" align="center">
        </el-table-column>
        <el-table-column label="操作" align="center">
          <template slot-scope="scope">
            <el-button
              @click="handleEdit(scope.row)"
              type="text"
              size="small"
              >用户信息</el-button
            >
          </template>
        </el-table-column>
      </el-table>
    </template>
    <script>
      export default {
        data(){
          return {
            data:[]
          }
        },
        methods:{
          handleEdit(){}
        }
      }
    </script>
    

    表格比较长时,需要些好多的el-table-column;所以想通过动态渲染的方式循环渲染出列项。

    官方给出的formatter格式化列项输出的方法只能格式化文本。无法渲染VNode。

    尝试通过v-html绑定,报错h is not a function

      // ...
      <el-table-column label="操作" align="center">
        <template slot-scope="scope">
          <div v-html="item.render(scope)"></div>
        </template>
      </el-table-column>
    

    解决办法,通过render方法提供CreateElement函数。新创建一个组件RenderColumn

    RenderColumn.vue

    <script>
      export default {
        props:{
          render:{
            type:Function,
            default:()=>()=>{}
          },
          scope:{
            type:Object,
            default:()=>{}
          }
        },
        render(h){
          const { row, column, $index } = this.scope
          return this.render(h,row,column,$index)
        }
      }
    </script>
    

    在渲染表格时调用,主要在于需要给render方法传入CreateElement方法。

    <template>
      <el-table
        ref="multipleTable"
        :data="data"
      >
        <el-table-column v-for="item in columns" :label="item.lable" :prop="item.prop">
          <template slot-scope="scope">
            <render-column v-if="item.render" :render="item.render" :scope="scope" />
            <span v-else>{{scope.row[item.prop]}}</span>
          </template>
        </el-table-column>
      </el-table>
    </template>
    <script>
      export default {
        data(){
          let $this = this
          return {
            data:[],
            columns:[
              {
                label:'姓名',
                prop:'name'
              },
              {
                label:'操作',
                render(h,row,column){
                  return <el-button
                          onClick={$this.handleEdit(row)}
                          type="text"
                          size="small"
                          >用户信息</el-button>
                }
              }
            ]
          }
        },
        methods:{
          handleEdit(){}
        }
      }
    </script>
    

    vue-cli脚手架已经继承了JSX的语法,可以直接书写。

    2. el-table实现单元格的编辑

    实现单元格的编辑,实现编辑组件EditCell.vue

    逻辑的核心点:

    1. 非编辑状态下,展示当前列项值,通过点击事件,单元格进入可编辑状态。并可通过this.$refs.input.focus()聚焦

    2. 数据el-input主要在于处理完成输入、enter键后完成编辑状态。

    3. 当完成编辑时,如果传入了校验函数。则需调用函数进行校验。并通过el-popover展示。

    <template>
      <div class="edit-cell">
        <el-popover :value="validateMsg !== ''" trigger="manual">
          <div slot="reference">
            <span v-if="!editable" @click="handleEditable"
              >{{ editValue }}
              <i class="el-icon-edit"></i>
            </span>
            <el-input
              ref="input"
              autofocus
              v-else
              v-model="editValue"
              @change="handleEditable"
              @blur="handleEditable"
            />
          </div>
          <span style="color: #f5222d">{{ validateMsg }}</span>
        </el-popover>
      </div>
    </template>
    <script>
    export default {
      name: "edit-cell",
      props: {
        value: String,
        validator: {
          type: Function,
          default: () => null
        }
      },
      data() {
        return {
          editValue: "",
          editable: false,
          validateMsg: ""
        };
      },
      mounted() {
        this.editValue = this.value;
      },
      methods: {
        handleEditable() {
          if (this.editable && typeof this.validator === "function") {
            const err = this.validator(this.editValue);
            if (err) {
              this.validateMsg = err;
              return;
            }
            this.validateMsg = "";
          }
          this.editable = !this.editable;
          if (this.editable) {
            this.$nextTick(() => {
              this.$refs.input.focus();
            });
          }
          this.$emit("change", this.editValue);
        }
      }
    };
    </script>
    

    如果要实现整行row的编辑,可给每一行数据追加属性editable,整合编辑时更改属性,切换为编辑状态。

    切入编辑状态el-input本来想通过autofocus获取焦点的。但没有用,使用了ref组件内部的方法。

    3. 实现虚拟无缝滚动seamlessScroll

    使用过vue-seamless-scroll,可实现数据的无缝滚动。但当数据量超过大几千时,页面就会变的很卡。通过看源代码实现,加入5000的数据量,需要渲染10000个DOM节点。

    通过之前虚拟列表的思想,实现一个虚拟无缝滚动组件

    实现滚动的主要API

    1. transform:translate(0px,0px),在水平、垂直方向上进行平移数据列表

    2. window.requestAnimationFrame(()=>{}) 在浏览器下次重绘时调用回调函数,通常为60次/s

    实现的主要逻辑:

    • 组件挂载或者数据data变化时进行数据初始化init()

    • init方法用于调用数据切割滚动方法。其中一个参数virtual用于显示控制如果数据量不大时,就没必要虚拟滚动了。

    • move方法中,通过每一帧的渲染更新,回调函数处理this.translateY -= this.speed平移数据列表。

    • splitData中则处理数据切割,判断如果不需要虚拟滚动时,则加载展示所有的数据。

    • 随后监听了translateY的变化,用于处理虚拟列表的滚动分页逻辑

        /**
         * 如果平移的距离大于分页*每项的长度,进行数据滚动重置
         **/
        handleDataScorll() {
          if (
            Math.abs(this.translateY) <
            this.pageOptions.pageSize * this.itemWidth
          ) {
            return;
          }
          // this.stop();
          // 第一页已滚动完成
          if (this.virtual) {
            this.splitData();
          }
          this.translateY = 0;
        },
      

    核心的JS逻辑,实现的相关方法。

    export default {
      // ...
      mounted() {
        // 复制数据,数据仓
        this.copyData = [...this.data];
        // 切割数据
        this.init();
      },
      computed: {
        boxStyle() {
          return {
            transform: `translate(0, ${this.translateY}px )`,
          };
        },
        total() {
          return this.data.length;
        },
      },
      watch: {
        data(newData) {
          this.copyData = [...newData];
          this.init();
        },
        translateY() {
          this.handleDataScorll();
        },
      },
      methods: {
        init() {
          if (!this.virtual) {
            // 非虚拟列表管滚动,则直接展示所有数据
            this.pageOptions.pageSize = this.total;
          }
          if (this.total > 0) {
            this.splitData();
            this.move();
          }
        },
        move() {
          this.stop();
          this.animationFrame = requestAnimationFrame(() => {
            if (this.total > 0) {
              this.translateY -= this.speed;
            }
    
            this.move();
          });
        },
        splitData() {
          if (!this.virtual) {
            this.preData = [...this.copyData];
            this.nextData = [...this.copyData];
            return;
          }
          // 只有在虚拟列表时,才调换数据位置
          this.copyData = [...this.copyData, ...this.preData];
          // pre
          this.preData = this.copyData.splice(0, this.pageOptions.pageSize);
          // next
          this.nextData = this.copyData.slice(0, this.pageOptions.pageSize);
        },
        /**
         * 监听滚动的距离
         */
        handleDataScorll() {
          if (
            Math.abs(this.translateY) <
            this.pageOptions.pageSize * this.itemWidth
          ) {
            return;
          }
          // this.stop();
          // 第一页已滚动完成
          if (this.virtual) {
            this.splitData();
          }
          this.translateY = 0;
        },
        stop() {
          if (this.animationFrame) {
            cancelAnimationFrame(this.animationFrame);
          }
        }
      },
    };
    

    示例中仅实现竖向滚动,横向滚动后续会追加props属性mode进行逻辑处理。

    4. 通过el-select实现联级选择

    Element提供的Cascader,但设计师可能需要的是并排的多个下拉,进行控制。

    主要的实现逻辑:

    1. 通过level指定联动选择的层级数量。通过循环渲染出el-select,

    2. 还有最关键的实现分级数据, 从data中分级出每一级level数据。视图中则通过optionsData[index]获取数据

      optionsData: function () {
        let arr = [[...this.data]]
        for (let id of this.selectedData) {
          if (!id) {
            arr.push([])
            break
          }
          let data = arr[arr.length - 1].find((item) => item.id === id)
          if (!data) {
            arr.push([])
            break
          }
          arr.push(data.children || [])
        }
        return arr
      }
    
    1. 最重要的是保证selectedData为层级深度长度的数组,这样才能渲染出正确数量的el-select

    2. 每一层级的事件change通过index来更新选中的数据selelctData

    <template>
      <div class="cascade-select-city">
        <el-select
          placeholder="请选择"
          v-for="(val, index) in selectedData"
          :key="index"
          :value="selectedData[index]"
          @change="handleSelect($event, index)"
        >
          <el-option value="">请选择</el-option>
          <el-option
            v-for="item in optionsData[index]"
            :key="item.id"
            :label="item.name"
            :value="item.name"
          />
        </el-select>
      </div>
    </template>
    <script>
    export default {
      name: 'cascade-select',
      props: {
        /**
         * 用于自定义级联数据
         */
        data: {
          type: Array,
          default: () => []
        },
        /**
         * 联动层级数量
         */
        level: {
          type: Number,
          default: 1
        },
        /**
         * 绑定数据
         */
        value: {
          type: Array,
          default: () => []
        }
      },
      data () {
        return {
          selectedData: new Array(this.level).fill('')
        }
      },
      mounted () {
      },
      watch: {
        value (val, oldVal) {
          if (JSON.stringify([val]) !== JSON.stringify([this.selectedData])) {
            //
            this.selectedData = [...val]
          }
        }
      },
      computed: {
        /**
         * 处理层级数据
         */
        optionsData: function () {
          let arr = [[...this.data]]
          for (let id of this.selectedData) {
            if (!id) {
              arr.push([])
              break
            }
            let data = arr[arr.length - 1].find((item) => item.AreaId === id)
            if (!data) {
              arr.push([])
              break
            }
            arr.push(data.children || [])
          }
          return arr
        }
      },
      methods: {
        /**
         * 处理联动的select
         */
        handleSelect (selected, level) {
          // 更新值
          this.selectedData = this.selectedData.map((val, index) => {
            if (index < level) {
              return val
            }
            return index === level ? selected : ''
          })
          this.$emit('change', this.selectedData)
        }
      }
    }
    </script>
    

    组件仅实现了data为静态数据时的逻辑处理,如果数据是动态的呢,比如异步加载联动数据。

    后续会开放一些日常开发封装的公共组件。地址暂时不可访问Fun-UI

    相关文章

      网友评论

        本文标题:el-table动态渲染列、可编辑单元格、虚拟无缝滚动

        本文链接:https://www.haomeiwen.com/subject/xnlpyrtx.html