element ui 表格组件与分页组件的二次封装

目录

效果图 

组件封装

 parseTime函数

debounce 函数

render通用渲染模版

页面使用

【扩展】vue 函数式组件

函数式组件特点:

函数式组件的优点:

【扩展】vue中的render函数

一、初步认识render函数

二、为什么使用render函数

三、render函数的解析

【扩展】添加操作栏显示权限

结构改动

逻辑新增

组件引入使用 

【扩展 】 props与attrs

props(Properties):

attrs(Attributes):

attrs的属性 

应用场景:


使用组件需先下载 element ui  vue2

引入全局注册自定义table组件

效果图 

组件封装

这段代码是一个封装的分页表格组件。它使用了Vue.js框架,并结合了Element UI中的el-table和el-pagination组件来实现。该组件接受一系列属性作为数据源,包括列表数据(list)、表格列(columns)、操作按钮列(operates)、总数(total)、分页参数(pagination)等。

在模板中,使用el-table组件来展示表格数据,使用el-pagination组件来实现分页功能。el-table-column用来定义每列的样式和展示内容。组件中还包括其他的一些方法和事件处理函数,如handleSizeChange用于切换每页显示的数量,handleIndexChange用于切换页码,handleSelectionChange处理多行选中等。

此外,还定义了一些样式规则来美化表格的显示效果,如设置表头颜色、调整列的样式、设置操作按钮组的布局、设置筛选弹窗和表格操作弹窗的样式等。

最后,使用了scss来编写样式表,通过设置类名来控制样式的展示。

  1. <template>标签中,定义了一个表格组件,包括表格本身,列定义,按钮操作组,和分页控件。

  2. <script>标签中,导入了必要的依赖,并定义了组件的属性(props)、数据(data)、生命周期钩子(activated、beforeDestroy、deactivated、created、mounted)和方法(methods)。

  3. props属性用于接受从父组件传递进来的数据,如数据列表 list、列定义 columns、按钮操作 operates、总数 total、分页参数 pagination 等。

  4. data数据属性包括一些内部状态,如表格高度 height、监听窗口调整大小的回调函数 $_resizeHandler、当前页码 pageIndex、表格分页信息 tableCurrentPagination、多行选中数据 multipleSelection 等。

  5. 在生命周期钩子中,组件监听窗口大小调整事件,自动调整表格高度,并在适当的生命周期中初始化和销毁事件监听器。

  6. methods包括一系列方法,如计算表头宽度的方法 headSpanFit、初始化事件监听器的方法 initListener、销毁事件监听器的方法 destroyListener、调整表格高度的方法 resize、处理每页显示数量变化的方法 handleSizeChange、处理页码变化的方法 handleIndexChange、处理多行选中的方法 handleSelectionChange 等。

  7. 最后,在<style>标签中定义了一些样式,包括表格样式、分页样式、按钮样式、筛选和操作弹窗样式等。

<!--region 封装的分页 table-->
<!--region 封装的分页 table-->
<template>
  <div class="table">
    <el-table
      id="iTable"
      ref="mutipleTable"
      :row-class-name="tableRowClassName"
      :header-cell-style="getRowClass"
      v-loading="options.loading"
      :border="options.border"
      :data="list"
      :height="noStatic ? customHeight : height"
      :max-height="noStatic ? customHeight : height"
      v-bind="options"
      @selection-change="handleSelectionChange"
      @sort-change="sortChange"
    >
      <!--region 选择框-->
      <el-table-column
        v-if="options.mutiSelect"
        type="selection"
        style="width: 60px"
        :selectable="options.selectable"
      >
      </el-table-column>
      <!--endregion-->
      <!--region 序号-->
      <el-table-column
        v-if="options.numbers"
        width="60"
        type="index"
        label="序号"
        align="center"
      ></el-table-column>
      <!--endregion-->
      <!--region 数据列-->
      <template v-for="(column, index) in columns">
        <el-table-column
          v-if="isShowColumn(column)"
          :min-width="headSpanFit(column)"
          :key="index"
          :prop="column.prop"
          :label="column.label"
          :align="column.align"
          :width="column.width"
          v-bind="column.el"
        >
          <template slot-scope="scope">
            <template v-if="!column.render">
              <template v-if="column.formatter">
                <span v-html="column.formatter(scope.row, column)"></span>
              </template>
              <template v-else>
                <span>{
   
   {
                  scope.row[column.prop] === 0 ? 0 : scope.row[column.prop] || "--"
                }}</span>
              </template>
            </template>
            <template v-else>
              <expand-dom
                :column="column"
                :row="scope.row"
                :render="column.render"
                :index="index"
              ></expand-dom>
            </template>
          </template>
        </el-table-column>
      </template>
      <!--endregion-->
      <!--region 按钮操作组-->
      <el-table-column
        ref="fixedColumn"
        label="操作"
        align="center"
        :width="operates && operates.width"
        :fixed="operates && operates.fixed"
        v-if="operates && operates.list.length > 0"
      >
        <template slot-scope="scope">
          <expand-dom
            :row="scope.row"
            :render="renderOperates"
            :index="scope.$index"
          ></expand-dom>
        </template>
      </el-table-column>

      <!--endregion-->
    </el-table>
    <div style="height: 12px"></div>
    <!--region 分页-->
    <el-pagination
      style="float: none; text-align: right"
      v-if="pagination"
      :pager-count="5"
      @size-change="handleSizeChange"
      @current-change="handleIndexChange"
      :page-size.sync="pagination.pageSize"
      :page-sizes="pagination.pageSizes || pageArray"
      :current-page.sync="pagination.pageIndex"
      layout="total,sizes, prev, pager, next,jumper"
      :total="total"
    ></el-pagination>
    <!--endregion-->
  </div>
</template>
<!--endregion-->
<script>
import { debounce } from "@/utils";
import { checkPermi } from "@/utils/permission.js";
const _pageArray = [10, 20, 30, 50]; // 每页展示条数的控制集合
export default {
  props: {
    list: {
      type: Array,
      default: [], // prop:表头绑定的地段,label:表头名称,align:每列数据展示形式(left, center, right),width:列宽
    }, // 数据列表
    columns: {
      type: Array,
      default: [], // 需要展示的列 === prop:列数据对应的属性,label:列名,align:对齐方式,width:列宽
    },
    operates: {
      type: Object,
      defaultt: () => {}, // width:按钮列宽,fixed:是否固定(left,right),按钮集合 === label: 文本,type :类型(primary / success / warning / danger / info / text),show:是否显示,icon:按钮图标,plain:是否朴素按钮,disabled:是否禁用,method:回调方法
    },
    total: {
      type: Number,
      default: 0,
    }, // 总数
    pagination: {
      type: Object,
      default: null, // 分页参数 === pageSize:每页展示的条数,pageIndex:当前页,pageArray: 每页展示条数的控制集合,默认 _page_array
    },
    noStatic: false, // 是否计算表格高度
    customHeight: {
      //与noStatic一起使用
      type: Number,
      default: 320,
    },
    otherHeight: {
      type: Number,
      default: 180,
    }, // 计算表格的高度
    options: {
      type: Object,
      default: {
        stripe: false, // 是否为斑马纹 table
        loading: false, // 是否添加表格loading加载动画
        highlightCurrentRow: false, // 是否支持当前行高亮显示
        mutiSelect: false, // 是否支持列表项选中功能
        border: false, //是否显示边框
        selectable: () => {
          //是否可以选中
          return false;
        },
      },
    }, // table 表格的控制参数
  },
  components: {
    expandDom: {
      functional: true,
      props: {
        row: Object,
        render: Function,
        index: Number,
        column: {
          type: Object,
          default: null,
        },
      },
      render: (h, ctx) => {
        const params = {
          row: ctx.props.row,
          index: ctx.props.index,
        };
        if (ctx.props.column) params.column = ctx.props.column;
        return ctx.props.render(h, params);
      },
    },
  },
  data() {
    return {
      height: 250,
      $_resizeHandler: null,
      pageIndex: 1,
      pageArray: _pageArray,
      multipleSelection: [], // 多行选中
    };
  },

  activated() {
    // 通常是在使用 Vue.js 的 <keep-alive> 包装时,组件会被缓存并在重新激活时调用这个钩子函数。
    if (!this.$_resizeHandler) {
      // avoid duplication init
      this.initListener();
    }

    // when keep-alive chart activated, auto resize
    this.resize();
  },
  //当组件即将被销毁(beforeDestroy)时,会调用这个钩子函数。
  beforeDestroy() {
    this.destroyListener();
  },
  //当组件被停用(deactivated),通常也是在 <keep-alive> 包装下,组件会调用这个钩子函数。
  deactivated() {
    this.destroyListener();
  },
  created() {},
  mounted() {
    this.initListener();
  },
  methods: {
    //计算小列宽
    headSpanFit(column) {
      let labelLong = column.label.length; // 表头label长度
      let size = 20; // 根据需要定义标尺,直接使用字体大小确定就行,也可以根据需要定义
      let minWidth = labelLong * size < 100 ? 100 : labelLong * size; // 根据label长度计算该表头最终宽度
      return minWidth;
    },
    // 初始化监听器的方法。
    initListener() {
      this.$_resizeHandler = debounce(() => {
        this.resize();
      }, 200);
      window.addEventListener("resize", this.$_resizeHandler);
      this.$nextTick(() => {
        this.resize();
      });
    },
    //这是一个销毁监听器的方法。
    destroyListener() {
      window.removeEventListener("resize", this.$_resizeHandler);
      this.$_resizeHandler = null;
    },
    //计算表格的高度
    resize() {
      //  不用计算
      if (this.noStatic) {
        return;
      }

      const { mutipleTable } = this.$refs;
      let staticHeight =
        window.innerHeight - this.$refs.mutipleTable.$el.offsetTop - this.otherHeight;
      this.height = staticHeight < 250 ? 250 : staticHeight;

      console.log(this.height);
      //保表格的布局在高度调整后得以更新。
      mutipleTable && mutipleTable.doLayout();
    },
    // 切换每页显示的数量
    handleSizeChange(size) {
      if (this.pagination) {
        this.$emit("handleSizeChange", { ...this.pagination });
      }
    },
    // 切换页码
    handleIndexChange(currnet) {
      if (this.pagination) {
        this.$emit("handleIndexChange", { ...this.pagination });
      }
    },
    // 多行选中
    handleSelectionChange(val) {
      this.multipleSelection = val;
      this.$emit("handleSelectionChange", val);
    },
    // 用于渲染操作按钮的方法
    renderOperates(h, params) {
      const endArr = this.rebuildList(params); //权限验证后最终按钮数组
      let outSideBtnArr = endArr.slice(0, 2); //外部按钮 默认前两个
      let insideArr = endArr.slice(2); //下拉菜单按钮数组
      const buttonArr = []; //最终渲染数组

      outSideBtnArr.forEach((item) => {
        buttonArr.push(this.renderOutsideButton(h, item, params));
      });

      if (insideArr.length > 0) {
        buttonArr.push(this.renderDropdownButton(h, insideArr, params));
      }

      return h("div", { attrs: { class: "operate-group" } }, buttonArr);
    },

    // 渲染外部按钮
    renderOutsideButton(h, item, params) {
      return h(
        "el-button",
        {
          // 组件的属性(数据)
          props: {
            type: item.type || "text", //类型(primary / success / warning / danger / info / text)
            icon: item.icon || "", //icon:按钮图标
            plain: item.plain || false, //plain:是否朴素按钮
            size: item.size || "mini", //大小
          },
          // 组件的属性(html属性)
          attrs: {
            title: item.label, //label: 文本
          },
          // 样式
          style: {
            color: item.color,
          },
          // 按钮的点击事件处理函数
          on: {
            click: () => {
              item.method(params.index, params.row);
            },
          },
        },
        item.label //label: 文本
      );
    },
    // 渲染下拉按钮
    renderDropdownButton(h, insideArr, params) {
      return h(
        // 创建了一个 "el-dropdown" 组件
        "el-dropdown",
        // dropdown 样式
        {
          class: ["custom-dropdown"],
          // 组件的属性(数据):
          props: {
            trigger: "click", //下拉框点击触发
          },
        },
        [
          // "el-dropdown" 组件中的button
          h(
            "el-button",
            {
              // 按钮样式
              style: {
                fontSize: "18px",
              },
              // 按钮类名
              class: ["custom-text"],
              props: {
                type: "text",
                plain: false,
                size: "mini",
              },
            },
            // 在按钮的内容中,使用了一个 "i" 标签,其 class 属性为 "el-icon-more",显示一个图标。
            [h("i", { class: "el-icon-more" })]
          ),
          // 创建了一个 "el-dropdown-menu" 组件
          h(
            "el-dropdown-menu",
            // 传入slot
            {
              slot: "dropdown",
            },
            // 对传入的按钮循环渲染出el-dropdown-item"
            insideArr.map((item) => {
              return h(
                "el-dropdown-item",
                {
                  nativeOn: {
                    click: () => {
                      item.method(params.index, params.row);
                    },
                  },
                },
                item.label
              );
            })
          ),
        ]
      );
    },

    // 校验权限
    rebuildList({ row }) {
      // 检查 operates 是否为空对象
      if (!this.operates || !this.operates.list || !Array.isArray(this.operates.list)) {
        return [];
      }
      // 过滤操作项
      return this.operates.list.filter((item) => {
        // 如果定义了 show 函数,则根据该函数的返回值来决定是否显示
        if (typeof item.show === "function") {
          return item.show(row);
        }

        // 如果 show 是布尔值且为 true,则显示
        if (typeof item.show === "boolean" && item.show) {
          return true;
        }

        // 如果定义了权限要求,则检查权限
        if (item.haspermission) {
          return checkPermi(item.haspermission);
        }

        // 默认允许显示
        return true;
      });
    },
    // 判断列的数据是否显示
    isShowColumn(column) {
      if (column.show === undefined) {
        return true;
      }
      if (typeof column.show !== "function") {
        return column.show;
      }
      return column.show(column);
    },
    // 排序
    sortChange({ column, prop, order }) {
      this.$emit("sortChange", { column, prop, order });
    },
    // 每一行的样式
    tableRowClassName({ rowIndex }) {
      if (rowIndex % 2 === 0) {
        return "warning-row";
      } else if (rowIndex % 2 === 1) {
        return "success-row";
      }
      return "";
    },
    // 表头样式
    getRowClass({ rowIndex }) {
      if (rowIndex == 0) {
        return this.options.headerCellStyle || "";
      } else {
        return " ";
      }
    },
  },
};
</script>

<style lang="scss">
.table {
  height: 100%;

  .el-pagination {
    float: right;
    margin: 20px;
  }

  .el-table__header-wrapper,
  .el-table__fixed-header-wrapper {
    thead {
      tr {
        th {
          color: #333333;
        }
      }
    }
  }

  .el-table-column--selection .cell {
    padding: 0;
    text-align: center;
  }

  .el-table__fixed-right {
    bottom: 0 !important;
    right: 6px !important;
    z-index: 1004;
  }

  .operate-group {
    display: flex;
    flex-wrap: wrap;

    .item {
      margin-top: 4px;
      margin-bottom: 4px;
      display: block;
      flex: 0 0 50%;
    }
  }

  .filter-data {
    top: e("calc((100% - 100px) / 3)");
    background-color: rgba(0, 0, 0, 0.7);
  }

  .table-action {
    top: e("calc((100% - 100px) / 2)");
    background-color: rgba(0, 0, 0, 0.7);
  }

  .fix-right {
    position: absolute;
    right: 0;
    height: 100px;
    color: #ffffff;
    width: 30px;
    display: block;
    z-index: 1005;
    writing-mode: vertical-rl;
    text-align: center;
    line-height: 28px;
    border-bottom-left-radius: 6px;
    border-top-left-radius: 6px;
    cursor: pointer;
  }

  .custom-dropdown {
    color: #666;

    &:hover {
      color: inherit;
    }
  }

  .operate-group {
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    height: 100%;

    .item {
      margin-right: 2px;
      display: block;
      flex: 0 1 auto;
    }
  }

  .el-button--text.custom-text {
    span {
      vertical-align: middle;
    }

    color: #666;

    &:hover {
      color: #66b1ff;
    }
  }
}
</style>

 parseTime函数
 

用于页面使用格式化时间对象

// 日期格式化
export function parseTime(time, pattern) {
  if (arguments.length === 0 || !time) {
    return null
  }
  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
  let date
  if (typeof time === 'object') {
    date = time
  } else {
    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
      time = parseInt(time)
    } else if (typeof time === 'string') {
      time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
    }
    if ((typeof time === 'number') && (time.toString().length === 10)) {
      time = time * 1000
    }
    date = new Date(time)
  }
  const formatObj = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  }
  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
    let value = formatObj[key]
    // Note: getDay() returns 0 on Sunday
    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
    if (result.length > 0 && value < 10) {
      value = '0' + value
    }
    return value || 0
  })
  return time_str
}

debounce 函数
 

用在封装监听窗口变化的节流函数

export function debounce(func, wait, immediate) {
  let timeout, args, context, timestamp, result

  const later = function() {
    // 据上一次触发时间间隔
    const last = +new Date() - timestamp

    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last)
    } else {
      timeout = null
      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
      if (!immediate) {
        result = func.apply(context, args)
        if (!timeout) context = args = null
      }
    }
  }

  return function(...args) {
    context = this
    timestamp = +new Date()
    const callNow = immediate && !timeout
    // 如果延时不存在,重新设定延时
    if (!timeout) timeout = setTimeout(later, wait)
    if (callNow) {
      result = func.apply(context, args)
      context = args = null
    }

    return result
  }
}

render通用渲染模版

丰富表格的展现形式

// renderUtils.js
// 渲染标签
export function customRender({ h, params, fieldToCheck, textMapping }) {
  const fieldValue = params.row[fieldToCheck];
  const type = textMapping.hasOwnProperty(fieldValue)
    ? textMapping[fieldValue].type
    : "default"; // 默认类型,可以根据需要修改

  const labelText = textMapping.hasOwnProperty(fieldValue)
    ? textMapping[fieldValue].text
    : '未知'; // 默认显示字段值,可以根据需要修改

  return h("el-tag", { props: { type } }, labelText);
}

// 渲染按钮
export function renderSwitch({ h, params, size, fieldToCheck }) {
  return h("el-switch", {
    props: {
      size: size || "medium",
      value: params.row[fieldToCheck],
    },
    on: {
      change: (events) => {
        this.$set(params.row, fieldToCheck, events);
        this.changeMsgStatus(events, params);
      },
    },
  });
}

// link
export function renderLink(h, params, title) {
  return h(
    "el-link",
    {
      props: {
        type: "primary",
        underline: false,
      },
      on: {
        click: (e) => {
          this.handleDetail(params);
        },
      },
    },
    title
  );
}
// 头像
export function avatarElement({ h, params, fieldToCheck, size }) {
  return h(
    "el-avatar",
    {
      props: {
        size: size || 44,
        src: params.row[fieldToCheck],
      },
      // 图片加载失败展示默认图片
      on: {
        error: (e) => {
          return true;
        },
      },
    },
    [
      h("img", {
        attrs: {
          src: require("@/assets/images/default_avatar.png"),
        },
      }),
    ]
  );
}

页面使用

<template>
  <div class="table-page">
    <!--region table 表格-->
    <app-table :list="list" :total="total" :otherHeight="otherHeight" :options="options" :pagination="pagination"
      :columns="columns" :operates="operates" @handleSizeChange="handleSizeChange" @handleIndexChange="handleIndexChange"
      @handleSelectionChange="handleSelectionChange" @sortChange="sortChange">
    </app-table>
    <!--endregion-->
  </div>
</template>
<script>
import { parseTime } from "@/utils/ruoyi";
import {
  customRender,
  renderSwitch,
  renderLink,
  avatarElement,
} from "@/utils/renderUtils.js";
export default {
  data() {
    return {
      total: 0,
      list: [
        {
          id: 1,
          title: "标题",
          state: 2,
          author: "张三",
          phone: "12346788901",
          email: "[email protected]",
          createDate: "2023-04-23 16:11:38",
          zero: null,
          isOpend: false,
          headimgurl: 'https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png'
        },
      ],
      otherHeight: 208,
      columns: [
        {
          prop: "id",
          label: "编号",
          align: "center",
          el: {
            // element ui的一些props...
            sortable: true, //开启排序
          },
        },
        {
          prop: "title",
          label: "标题",
          align: "center",
          formatter: (row, column, cellValue) => {
            return `<span style="white-space: nowrap;color: dodgerblue;">${row.title}</span>`;
          },
        },
        {
          prop: "state",
          label: "状态",
          align: "center",
          width: "160",
          render: (h, params) => {
            const fieldToCheck = "state";
            const textMapping = {
              0: { type: "success", text: "上架" },
              1: { type: "info", text: "下架" },
              2: { type: "danger", text: "审核中" },
            };
            let data = { h, params, fieldToCheck, textMapping }
            return customRender.call(this, data);
          },
        },
        {
          prop: "switch",
          label: "开关",
          align: "center",
          width: "160",
          render: (h, params) => {
            let data = {
              h, params,
              fieldToCheck: 'isOpend',
              size: 'medium'
            }
            return renderSwitch.call(this, data);
          },
        },
        {
          prop: "headimgurl",
          label: "头像",
          align: "center",
          render: (h, params) => {
            let data = {
              h, params,
              fieldToCheck: 'headimgurl',
              size: 44
            }
            return avatarElement.call(this, data);
          },
        },
        {
          prop: "author",
          label: "作者",
          align: "center",
          width: 120,
        },
        {
          prop: "phone",
          label: "联系方式",
          align: "center",
          width: 160,
          show: false, //控制这一列是否展示
        },
        {
          prop: "zero",
          label: "邮箱",
          align: "center",
          width: 240,
        },
        {
          prop: "link",
          label: "查看",
          align: "center",
          width: "160",
          render: (h, params) => {
            return renderLink.call(this, h, params, '查看');
          },
        },
        {
          prop: "createDate",
          label: "发布时间",
          align: "center",
          width: 180,
          formatter: (row, column, cellValue) => {
            return parseTime(row.createDate);
          },
        },
      ], // 需要展示的列
      operates: {
        width: 200,
        fixed: "right",
        list: [
          {
            label: "编辑",
            type: "text",
            show: (index, row) => {
              return true;
            },
            icon: "el-icon-edit",

            disabled: false,
            method: (index, row) => {
              this.handleEdit(index, row);
            },
          },
          {
            label: "删除",
            type: "text",
            icon: "el-icon-delete",
            show: true,
            disabled: (index, row) => {
              return false;
            },
            method: (index, row) => {
              this.handleDel(index, row);
            },
          },
          {
            label: "测试下拉",
            type: "text",
            icon: "el-icon-delete",
            haspermission: ["agent:del"], //显示权限
            show: true,
            disabled: (index, row) => {
              return false;
            },
            method: (index, row) => {
              this.handleDel(index, row);
            },
          },
        ],
      }, // 操作按钮组
      pagination: {
        pageIndex: 1,
        pageSize: 20,
      }, // 分页参数
      options: {
        stripe: true, // 是否为斑马纹 table
        loading: false, // 是否添加表格loading加载动画
        highlightCurrentRow: true, // 是否支持当前行高亮显示
        mutiSelect: true, // 是否支持列表项选中功能
        border: true, //是否显示边框
        numbers: true, //是否显示序号
        selectable() {
          //禁用选中
          return false;
        },
        headerCellStyle: "background-color:#fff", //表头颜色
      }, // table 的参数
    };
  },

  mounted() { },
  methods: {
    // 切换每页显示的数量
    handleSizeChange(pagination) {
      console.log("pagination", pagination);
    },
    // 切换页码
    handleIndexChange(pagination) {
      console.log("pagination", pagination);
    },
    // 选中行
    handleSelectionChange(val) {
      console.log("val:", val);
    },
    // 编辑
    handleEdit(index, row) {
      console.log(" index:", index);
      console.log(" row:", row);
    },
    // 删除
    handleDel(index, row) {
      console.log(" index:", index);
      console.log(" row:", row);
    },
    // 排序
    sortChange(data) {
      console.log(data);
    },
    // 开关按钮
    changeMsgStatus(ev, params) {
      console.log(ev, params);
    },
    handleDetail(params) {
      console.log(params);
    },
  },
};
</script>

【扩展】vue 函数式组件

函数式组件特点:

  • 没有管理任何状态
  • 没有监听任何传递给它的状态
  • 没有生命周期方法
  • 它只是接收一些prop的函

我们将这样的组件标记为functional

  • 无状态 == 无响应式数据
  • 无实例 == 无this上下文

函数式组件的优点:

  • 渲染开销低,因为函数式组件只是函数;
 {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
}

props: 提供所有prop的对象
children:VNode 子节点的数组
slots: 一个函数,返回了包含所有插槽的对象
scoptedSlots:(2.6.0) 一个暴露传入的作用域插槽的对象,也以函数形式暴露普通插槽
data:传递个组件的整个数据对象,作为createElement的第二个参数传入组件
parent:对父组件的引用
listeners:(2.3.0+) 一个包含了:所有父组件为当前组件祖册的事件监听器对象,是data.on的一个别名
injections:(2.3.0+) 如果使用了inject选项,则改对象包含了:应当被注入的属性;

【扩展】vue中的render函数

一、初步认识render函数

import Vue from 'vue'
import App from './App'

Vue.config.productionTip = false

new Vue({
  el: '#app',
  render: h => h(App)
})

在使用脚手架创建vue项目的过程,我们很容易看到render这个函数,相对于其他标签,我们对于render还是比较陌生的,因此写下这篇文章你我共同理解。

二、为什么使用render函数


VUE推荐在绝大多数情况下使用template来创建我们的HTML。然而在一些场景中,我们真的需要JavaScript的完全编程的能力,这就是render函数,它比template更接近编译器。(这是官方的话)
简单来说,我们为什么要使用render函数呢?? 便是因为我们最经常使用的一个引入。

import Vue from "vue";

这一个引入你看似没有任何问题,但问题恰恰就是出在这。在不同版本的vue中,有vue.js和vue.runtime.xxx.js这两种js文件。其中

(1)vue.js是完整版的vue,包含核心功能+模板解析器。

(2)vue.runtime.xxx.js是运行版vue,只包含核心功能,没有模板解析器。

VUE开发者为了让我们打包的文件能尽可能小一点,在上述引入的是运行版vue。因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,这时候就需要使用render函数去接收到的createElement函数去指定具体内容,创建html模板。

三、render函数的解析


render 函数即渲染函数,它是个函数,它的参数 createElement 也是个函数。

上边的代码中 render: h => h(App) ,这是 ES6的箭头函数的写法,可以把 h 当作 createElement 的别名。所以这段代码其实相当于

render: function (createElement) {
    return createElement(App)
}

这个函数的作用就是生成一个 VNode节点,render 函数得到这个 VNode 节点之后,返回给 Vue.js 的 mount 函数,渲染成真实 DOM 节点,并挂载到根节点上。

createElement 函数的返回值是 VNode(即:虚拟节点)

createElement 函数的3个参数

  • 一个 HTML 标签字符串,组件选项对象,或者解析上述任何一种的一个 async 异步函数。类型:String | Object | Function。必需。
  • 一个包含模板相关属性的数据对象,你可以在 template 中使用这些特性。类型:Object。可选。
  • 子虚拟节点 (VNodes),由 createElement() 构建而成,也可以使用字符串来生成“文本虚拟节点”。类型:String | Array。可选
new Vue({
  el: '#app',
  render:function (createElement) {
    //1.普通用法
    // createElement(标签,{属性},[内容])
    return createElement("h2",{class:"box"},['hello',createElement("button",["按钮"])])
  }
})

同时createElement也可以传进去一个组件,因此 

render: h => h(App)

等同于 

render:function (createElement) {
    return createElement(App)
  }

【扩展】添加操作栏显示权限

结构改动

通过函数式组件渲染 操作按钮部分

    <el-table-column
        ref="fixedColumn"
        label="操作"
        align="center"
        :width="operates.width"
        :fixed="operates.fixed"
        v-if="operates.list.length > 0"
      >
        <template slot-scope="scope">
          <expand-dom
            :row="scope.row"
            :render="renderOperates"
            :index="scope.$index"
          ></expand-dom>
        </template>
      </el-table-column>

逻辑新增

这段代码是一个Vue.js组件方法,用于渲染操作按钮。让我逐步解释代码的主要部分:

1. `renderOperates` 方法:这是一个渲染操作按钮的方法。它接受两个参数 `h` 和 `params`,其中 `h` 是Vue的createElement函数,用于创建虚拟DOM,而 `params` 包含一些参数数据。

2. `const endArr = this.rebuildList(params);`:这一行调用了 `rebuildList` 方法,它会根据权限验证后的结果生成最终的按钮数组,然后将这个数组存储在 `endArr` 变量中。

3. `let outSideBtnArr = endArr.slice(0, 2);` 和 `let insideArr = endArr.slice(2);`:这两行将 `endArr` 分为两个部分,前两个按钮存储在 `outSideBtnArr` 中,其余的按钮存储在 `insideArr` 中。

4. `const buttonArr = [];`:创建一个空数组 `buttonArr`,用于存储最终要渲染的按钮。

5. `outSideBtnArr.forEach((item) => { ... });`:这是一个循环遍历 `outSideBtnArr` 的循环,对每个按钮调用 `renderOutsideButton` 方法进行渲染,然后将渲染结果添加到 `buttonArr` 中。

6. `if (insideArr.length > 0) { ... }`:这是一个条件判断,如果 `insideArr` 中有按钮,则调用 `renderDropdownButton` 方法进行渲染,然后将渲染结果添加到 `buttonArr` 中。

7. 最后,使用 `h("div", { attrs: { class: "operate-group" } }, buttonArr)` 创建一个 `<div>` 元素,设置其类名为 "operate-group",并将 `buttonArr` 中的按钮渲染到这个 `<div>` 元素中,最终返回这个 `<div>` 元素的虚拟DOM。

接下来,代码中还包含了两个方法 `renderOutsideButton` 和 `renderDropdownButton`,它们分别用于渲染外部按钮和下拉按钮。这两个方法的主要作用是创建相应的按钮元素,并设置按钮的属性、样式和点击事件处理函数。

最后,代码中还包括一个 `rebuildList` 方法,用于根据权限验证结果来生成最终的按钮数组。它会遍历操作按钮列表,根据按钮的显示权限和角色身份权限来判断是否允许显示该按钮,然后返回允许显示的按钮数组。

总的来说,这段代码用于动态生成操作按钮组件,根据权限和角色身份权限来控制按钮的显示和行为。

    // 用于渲染操作按钮的方法
    renderOperates(h, params) {
      const endArr = this.rebuildList(params); //权限验证后最终按钮数组
      let outSideBtnArr = endArr.slice(0, 2); //外部按钮 默认前两个
      let insideArr = endArr.slice(2); //下拉菜单按钮数组
      const buttonArr = []; //最终渲染数组

      outSideBtnArr.forEach((item) => {
        buttonArr.push(this.renderOutsideButton(h, item, params));
      });

      if (insideArr.length > 0) {
        buttonArr.push(this.renderDropdownButton(h, insideArr, params));
      }

      return h("div", { attrs: { class: "operate-group" } }, buttonArr);
    },

    // 渲染外部按钮
    renderOutsideButton(h, item, params) {
      return h(
        "el-button",
        {
          // 组件的属性(数据)
          props: {
            type: item.type || "text", //类型(primary / success / warning / danger / info / text)
            icon: item.icon || "", //icon:按钮图标
            plain: item.plain || false, //plain:是否朴素按钮
            size: item.size || "mini", //大小
          },
          // 组件的属性(html属性)
          attrs: {
            title: item.label, //label: 文本
          },
          // 样式
          style: {
            color: item.label == "删除" ? "#ff4057" : "",
          },
          // 按钮的点击事件处理函数
          on: {
            click: () => {
              item.method(params.index, params.row);
            },
          },
        },
        item.label //label: 文本
      );
    },
    // 渲染下拉按钮
    renderDropdownButton(h, insideArr, params) {
      return h(
        // 创建了一个 "el-dropdown" 组件
        "el-dropdown",
        // dropdown 样式
        {
          class: ["custom-dropdown"],
          // 组件的属性(数据):
          props: {
            trigger: "click", //下拉框点击触发
          },
        },
        [
          // "el-dropdown" 组件中的button
          h(
            "el-button",
            {
              // 按钮样式
              style: {
                fontSize: "18px",
              },
              // 按钮类名
              class: ["custom-text"],
              props: {
                type: "text",
                plain: false,
                size: "mini",
              },
            },
            // 在按钮的内容中,使用了一个 "i" 标签,其 class 属性为 "el-icon-more",显示一个图标。
            [h("i", { class: "el-icon-more" })]
          ),
          // 创建了一个 "el-dropdown-menu" 组件
          h(
            "el-dropdown-menu",
            // 传入slot
            {
              slot: "dropdown",
            },
            // 对传入的按钮循环渲染出el-dropdown-item"
            insideArr.map((item) => {
              return h(
                "el-dropdown-item",
                {
                  nativeOn: {
                    click: () => {
                      item.method(params.index, params.row);
                    },
                  },
                },
                item.label
              );
            })
          ),
        ]
      );
    },

    // 校验权限
    rebuildList({ row }) {
      // 验证是否显示权限和角色身份权限
      return this.operates.list.filter((item) => {
        if (typeof item.show === "function") {
          return item.show(row);
        } else if (typeof item.show === "boolean" && item.show) {
          return true;
        }

        // 如果没有显示权限要求,则继续验证角色身份权限
        if (item.haspermission) {
          return checkPermi(item.haspermission);
        }

        // 如果没有角色身份权限要求,则默认允许显示
        return true;
      });
    },

组件引入使用 

只需添加按钮对应的权限标识符字段即可

      operates: {
          fixed: "right",
          list: [
            {
              label: "编辑",
              type: "text",
              haspermission: ["agent:del"],
              icon: "el-icon-edit",
              plain: false,
              disabled: false,
              method: (index, row) => {
                this.handleEdit(index, row);
              },
            },
            {
              label: "删除",
              show: true,
              plain: false,
              disabled: (index, row) => {
                return false;
              },
              method: (index, row) => {
                this.handleDel(index, row);
              },
            },
          ],
        }, // 操作按钮组

本文用的是若依框架 上文中添加权限部分 checkPermi 函数 为若伊提供的校验按钮权限方法(组件引入haspermission字段同理 可以根据业务场景自行修改,传入haspermission权限标识符即可)

【扩展 】 props与attrs

Vue.js是一个流行的前端JavaScript框架,它提供了一种便捷的方式来构建交互性的用户界面。在Vue.js中,attrsprops是两个重要的概念,它们用于组件之间的数据传递和交互。让我们来探讨一下它们的区别和应用场景。

1、props 要先声明才能取值,attrs 不用先声明

2、props 声明过的属性,attrs 里不会再出现

3、props 不包含事件,attrs 包含

4、props 支持 string 以外的类型,attrs 只有 string 类型

props(Properties):

  1. 用途

    props是用来从父组件向子组件传递数据的一种方式。父组件可以将数据作为属性传递给子组件,子组件可以在其模板中使用这些属性。
  2. 数据流向

    数据流是单向的,从父组件到子组件。父组件通过属性设置子组件的数据。
  3. 定义方式

    在子组件中,需要明确声明接收哪些props,通常在组件的选项中使用props属性进行定义。
// 父组件
<template>
  <child-component :message="parentMessage"></child-component>
</template>

<script>
export default {
  data() {
    return {
      parentMessage: "Hello from parent!",
    };
  },
};
</script>

// 子组件
<template>
  <div>{
   
   { message }}</div>
</template>

<script>
export default {
  props: ['message'],
};
</script>

attrs(Attributes):

  1. 用途

    attrs用于从父组件向子组件传递HTML属性,而不是组件的数据。这对于将原生HTML属性传递给子组件非常有用。
  2. 数据流向

    同样是单向的,从父组件到子组件。但是,attrs传递的是HTML属性,而不是组件的属性。
  3. 定义方式

    attrs不需要在子组件中明确声明,而是在子组件的模板中直接使用。
// 父组件
<template>
  <child-component title="Child Component"></child-component>
</template>

// 子组件
<template>
  <div :title="title">This is a child component</div>
</template>

attrs的属性 

HTML 中的属性可以分为多个类别,以下是一些常见的 HTML 属性及其分类:

  1. 全局属性 (Global Attributes): 这些属性可以在 HTML 元素中的任何地方使用,它们不受特定元素的限制。例如:

    • id: 元素的唯一标识符
    • class: 元素的类名
    • style: 元素的 CSS 样式
    • data-*: 自定义数据属性
    • title: 元素的标题(通常显示为工具提示)
  2. 链接属性 (Link Attributes): 用于链接元素的属性,如 <a> 和 <link>

    • href: 指定链接的目标 URL
    • target: 定义链接如何在浏览器中打开
    • rel: 定义当前文档与链接资源之间的关系
  3. 表单属性 (Form Attributes): 用于表单元素(如 <input><form>)的属性:

    • name: 表单元素的名称
    • type: 输入字段的类型(文本、密码、单选框等)
    • value: 输入字段的默认值
    • action: 表单提交的目标 URL
    • method: 表单提交的 HTTP 方法(GET 或 POST)
    • required: 指示字段是否必填
  4. 图像属性 (Image Attributes): 用于 <img> 元素的属性:

    • src: 图像的源 URL
    • alt: 图像的替代文本
    • width 和 height: 图像的宽度和高度
  5. 多媒体属性 (Media Attributes): 用于多媒体元素,如 <audio> 和 <video>

    • controls: 显示多媒体控件(播放、暂停、音量等)
    • autoplay: 指示多媒体是否自动播放
    • loop: 指示多媒体是否循环播放
    • poster: 预览图像的 URL
  6. 元信息属性 (Meta Information Attributes): 用于 <meta> 元素,通常用于定义文档的元信息:

    • charset: 文档字符编码
    • name 和 content: 定义各种元信息,如页面描述、关键词、作者等
  7. 脚本属性 (Script Attributes): 用于 <script> 元素,用于加载和执行脚本:

    • src: 脚本文件的源 URL
    • type: 指定脚本的 MIME 类型
    • async 和 defer: 控制脚本的加载和执行方式
  8. 框架属性 (Frame Attributes): 用于 <iframe> 元素,用于嵌套其他文档:

    • src: 嵌套文档的源 URL
    • width 和 height: 框架的宽度和高度

这只是一些常见的 HTML 属性分类,还有其他特定元素的属性。不同的 HTML 元素可能具有不同的属性,具体取决于元素的类型和用途。

应用场景:

  • 使用props

    • 当你需要在父组件和子组件之间传递数据,以便子组件可以根据这些数据渲染不同的内容或执行不同的操作时,使用props是最常见的做法。
  • 使用attrs

    • 当你需要将原生HTML属性传递给子组件,例如titleclassstyle等,而不需要在子组件中显式声明它们时,可以使用attrs
    • 也可以用于将自定义属性传递给子组件,这些属性在子组件内部没有定义props,但仍然需要在子组件的模板中使用。

总之,props用于传递组件之间的数据,而attrs用于传递HTML属性。根据你的需求,选择适当的方式来实现组件之间的通信。

猜你喜欢

转载自blog.csdn.net/qq_63358859/article/details/132714435