vue递归组件具名插槽透传

305 阅读2分钟

具名插槽是vue中非常重要的一个概念,常用于我们在封装组件的时候,今天我们由浅入深来研究一下它。

1. 通过具名插槽在父组件中自定义显示列的内容

比如我们在封装表格组件时,经常会运用具名插槽在父组件中通过列名去自定义单元格内容,下面我以vxe-table表格为列。

  • 子组件
  <vxe-table
      border
      resizable
      auto-resize
      show-overflow
      column-key
      :row-id="rowId"
      :columnConfig="{ useKey: true }"
      :edit-rules="validRules"
      :size="size"
      :expandConfig="{expandRowKeys:this.expandRowKeys,reserve:true}"
      :edit-config="{ trigger: 'click', mode: 'row', showIcon: false }"
      :checkbox-config="{ checkRowKeys: checkedData, checkMethod: checkBoxMethod }"
      :data="tableData"
      max-height="500px"
      style="width:100%"
      :row-style="setRowStyle"
      :row-config="{ isHover: isHoverShow,keyField:'id' }"
      @cell-dblclick="cellClick"
      @checkbox-change="handleSelectionChange"
      @checkbox-all="handleSelectionChange"
      :show-footer="isShowFooter"
      :footer-method="footerMethod"
      @toggle-row-expand="handleExpand"
      ref="vxeTable"
      v-bind="vxeTableAttr"
    >
      <template v-for="item in tableColumntable">
        <vxe-table-colgroup v-bind="item" :key="item.field" v-if="item.children">
          <template v-for="subCol in item.children">
            <vxe-table-column v-bind="subCol" :key="subCol.field" >
              <template #defalut="{ row }">
                {{ row[subCol.field] }}
              </template>
            </vxe-table-column>
          </template>
        </vxe-table-colgroup>
        <vxe-table-column
          :key="item.field"
          v-bind="item"
          :type="item.type"
          v-else-if="item.type === 'expand'"
        >
          <template #default="{ row }">
            <span>{{ row[item.field] }}</span>
          </template>
        </vxe-table-column>
        <vxe-table-column :key="item.field" v-bind="item" v-else-if="item.type === 'seq'">
          <template #default="{ row, rowIndex }">
            <span>{{ row.footFlag ? '小计' : rowIndex + 1 }}</span>
          </template>
        </vxe-table-column>
        <vxe-table-column :key="item.field" v-bind="item" v-else>
          <template slot-scope="scope">
          //重点:插槽部分
            <template v-if="$scopedSlots[item.field]">
              <slot :name="item.field" v-bind="scope"></slot>
            </template>
            <span v-else-if="item.formatter">
              {{
                item.formatter({
                  row: scope.row,
                  column: scope.column,
                  cellValue: scope.row[item.field],
                })
              }}
            </span>
            <span v-else>
              {{ scope.row[item.field] }}
            </span>
          </template>
        </vxe-table-column>
      </template>
    </vxe-table>
</template>

<script>
export default {
  name: 'VxeChildTableView',
  props: {
    tableColumn: {
      type: Array,
      required: true,
    },
    tableData: {
      type: Array,
      required: true,
    },
    size: {
      type: String,
      default: 'mini',
    },
    showCheckBox: {
      type: Boolean,
      default: true,
    },
    // 是否支持行点击事件
    isCellCanClick: {
      type: Boolean,
      default: false,
    },
    // 是否支持行鼠标经过时高亮
    isHoverShow: {
      type: Boolean,
      default: false,
    },
   
    vxeTableAttr: {
      type: Object,
      default: () => ({}),
    },
    rowId: {
      type: String,
      default: 'id',
    },
    expandRowKeys:{
      type: Array,
      default: () => [],
    },
    checkedData: {
      type: Array,
      default: () => [],
    },
    checkBoxMethod: { type: Function },
    validRules: {
      type: Object,
      default: () => {},
    },
    moduleName: {
      type: String,
      default: 'purchase_vxeTableView',
    },
    
    editMode: {
      type: Boolean,
      default: true,
    },
    isShowFooter:{
      type: Boolean,
      default: false,
    },
    footerMethod:Function,
  },
  watch:{
    tableColumn:{
      deep:true,
      immediate:true,
      handler(val){
        let seq = [
          {
            type: 'seq',
            field: 'seq',
            title: '序号',
            width: 55,
            fixed: 'left',
            align: 'center',
          },
        ]
        let checkbox = [
          {
            type: 'checkbox',
            field: 'checkbox',
            width: 35,
            fixed: 'left',
            align: 'left',
          },
        ]
        this.tableColumntable = this.showCheckBox ? [...seq,...checkbox,...val] : [...seq,...val]
      }
    },
  },
  data() {
    return {
      selectRowIndex: 0,
      tableColumntable:[]
    };
  },
  methods: {
     handleExpand({row,expand}){
      this.$emit('handleExpand',{row,expand})
    },
    handleSelectionChange(data) {
      const checked = this.$refs.vxeTable.getCheckboxRecords();
      this.$emit('handleSelectionChange', checked, data);
      
    },
    setRowStyle({ rowIndex }) {
      if (this.isCellCanClick) {
        if (rowIndex === this.selectRowIndex) {
          return 'background-color: #F6F6FE;color: #4869F4;';
        }
      }
    },
    cellClick({ row, rowIndex, column }) {
      if (column.type === 'checkbox' || !this.isCellCanClick) return;
      this.selectRowIndex = rowIndex;
      this.$emit('currentChart', row);
    },
    setChekboxRow(data) {
      this.$refs.vxeTable.setCheckboxRow(data, true);
    },
    refreshTable(newData) {
      this.$refs.vxeTable.loadData(newData);
    },
    validateTable() {
      return new Promise((resolve, reject) => {
        // vxe表格校验
        this.$refs.vxeTable.validate(true, (err) => {
          if (err) {
            this.$nextTick(() => {
              reject();
            });
          } else {
            resolve(true);
          }
        });
      });
    },
  },
};
</script>      
  • 父组件

在父组件中我们就可以通过 <template #warehouseIds="{row,rowIndex}" >这种方式去自定义列名是warehouseIds那列的显示内容,这就是具名插槽在组件中的使用方式

          :tableData="saleOrderData"
          :tableColumn="saleOrderTableColumn"
          :editMode="false"
          :showCheckBox="false"
          :isShowFooter="true"
          :footerMethod="footerMethod"
          :footerChildMethod="footerChildMethod"
          v-loading="tableLoading"
          moduleName="sale_order_inventory_goods_table"
        >
            <template #warehouseIds="{row,rowIndex}" >
                <el-select
                  v-model="row.warehouseIds"
                  filterable
                  multiple
                  collapse-tags
                  placeholder="请选择"
                  style="width: 100%;"
                  @change="freshChildData(rowIndex)"

                >
                  <el-option
                    v-for="(item, index) in wareHouseList"
                    :key="item.id + index"
                    :label="item.name"
                    :value="item.id"
                  ></el-option>
                </el-select>
          </template>
        </VxeChildTableView>

现在需求突然变了,要求表格要显示成那种能展开的表格,并且主子表都能在父组件中自定义列的显示内容,比如下面这样的,这就是我们今天要讲的重点

微信截图_20250302115224.png

  1. 通过具名插槽在父组件中自定义显示展开表格列的内容

于是我把子组件改成递归组件,注意看下面代码块中这段内容`` <template v-for="(_, slotName) in $scopedSlots" :slot="slotName" slot-scope="scope"> <slot :name="slotName" v-bind="scope" /> </template>``这段代码正是具名插槽能随着递归组件传到最内层组件的关键

  • 改动后子组件
      border
      resizable
      auto-resize
      show-overflow
      column-key
      :row-id="rowId"
      :columnConfig="{ useKey: true }"
      :edit-rules="validRules"
      :size="size"
      :expandConfig="{expandRowKeys:this.expandRowKeys,reserve:true}"
      :edit-config="{ trigger: 'click', mode: 'row', showIcon: false }"
      :checkbox-config="{ checkRowKeys: checkedData, checkMethod: checkBoxMethod }"
      :data="tableData"
      max-height="500px"
      style="width:100%"
      :row-style="setRowStyle"
      :row-config="{ isHover: isHoverShow,keyField:'id' }"
      @cell-dblclick="cellClick"
      @checkbox-change="handleSelectionChange"
      @checkbox-all="handleSelectionChange"
      :show-footer="isShowFooter"
      :footer-method="footerMethod"
      @toggle-row-expand="handleExpand"
      :ref="dynamicRef"
      v-bind="vxeTableAttr"
    >
      <template v-for="item in tableColumntable">
        <vxe-table-colgroup v-bind="item" :key="item.field" v-if="item.children">
          <template v-for="subCol in item.children">
            <vxe-table-column v-bind="subCol" :key="subCol.field" >
              <template #defalut="{ row }">
                {{ row[subCol.field] }}
              </template>
            </vxe-table-column>
          </template>
        </vxe-table-colgroup>
        <vxe-table-column
          :key="item.field"
          v-bind="item"
          :type="item.type"
          v-else-if="item.type === 'expand'"
        >
          <template #default="{ row }">
            <span>{{ row[item.field] }}</span>
          </template>
          <template #content="{ row: childRow, rowIndex: childRowIndex }">
            <VxeChildTableView
              :tableData="childRow.childData"
              :tableColumn="expandColumn"
              :childRowIndex="childRowIndex"
              :isChild="true"
              :isShowFooter="true"
              :showTableSetting="false"
              :footerMethod="footerChildMethod"
              :checkedData="checkedChildData"
              @handleSelectionChange="(data)=>handleChildSelectionChange(data,childRowIndex)"
            >
              //重点部分:具名插槽透传
              <template v-for="(_, slotName) in $scopedSlots" :slot="slotName" slot-scope="scope">
                <slot :name="slotName" v-bind="scope" />
              </template>
            </VxeChildTableView>
          </template>
        </vxe-table-column>
        <vxe-table-column :key="item.field" v-bind="item" v-else-if="item.type === 'seq'">
          <template #default="{ row, rowIndex }">
            <span>{{ row.footFlag ? '小计' : rowIndex + 1 }}</span>
          </template>
        </vxe-table-column>
        <vxe-table-column :key="item.field" v-bind="item" v-else>
          <template slot-scope="scope">

            <template v-if="$scopedSlots[item.field]">
              <slot :name="item.field" v-bind="scope"></slot>
            </template>
            <span v-else-if="item.formatter">
              {{
                item.formatter({
                  row: scope.row,
                  column: scope.column,
                  cellValue: scope.row[item.field],
                })
              }}
            </span>
            <span v-else>
              {{ scope.row[item.field] }}
            </span>
          </template>
        </vxe-table-column>
      </template>
    </vxe-table>
    export default {
      name: 'VxeChildTableView',
      props: {
        tableColumn: {
          type: Array,
          required: true,
        },
        tableData: {
          type: Array,
          required: true,
        },
        size: {
          type: String,
          default: 'mini',
        },
        showCheckBox: {
          type: Boolean,
          default: true,
        },
        // 是否支持行点击事件
        isCellCanClick: {
          type: Boolean,
          default: false,
        },
        // 是否支持行鼠标经过时高亮
        isHoverShow: {
          type: Boolean,
          default: false,
        },
        isChild: {
          type: Boolean,
          default: false,
        },
        childRowIndex: { type: Number },
        vxeTableAttr: {
          type: Object,
          default: () => ({}),
        },
        rowId: {
          type: String,
          default: 'id',
        },
        expandRowKeys:{
          type: Array,
          default: () => [],
        },
        checkedData: {
          type: Array,
          default: () => [],
        },
        checkedChildData: {
          type: Array,
          default: () => [],
        },
        expandColumn: {
          type: Array,
          default: () => [],
        },
        checkBoxMethod: { type: Function },
        validRules: {
          type: Object,
          default: () => {},
        },
        moduleName: {
          type: String,
          default: 'purchase_vxeTableView',
        },
        showTableSetting: {
          type: Boolean,
          default: true,
        },
        editMode: {
          type: Boolean,
          default: true,
        },
        isShowFooter:{
          type: Boolean,
          default: false,
        },
        footerMethod:Function,
        footerChildMethod:Function,
      },
      computed: {
        dynamicRef() {
          return this.isChild ? `vxeTableChild${this.childRowIndex}` : 'vxeTable';
        },
        columnsConfig(){
          if(!this.showTableSetting || this.isChild) return;
          return{
            productName: 'openerp',
            moduleName: this.moduleName,
            userId: getCookie('_name_'),
            list: {
              name: 'vxeTable',
              prop: 'tableColumntable',
              hasExtendedField: true,
              methods: { dropCallBack: this.handleColumnDrop },
            },
          }
        }
      },
      watch:{
        tableColumn:{
          deep:true,
          immediate:true,
          handler(val){
            let seq = [
              {
                type: 'seq',
                field: 'seq',
                title: '序号',
                width: 55,
                fixed: 'left',
                align: 'center',
              },
            ]
            let checkbox = [
              {
                type: 'checkbox',
                field: 'checkbox',
                width: 35,
                fixed: 'left',
                align: 'left',
              },
            ]
            this.tableColumntable = this.showCheckBox ? [...seq,...checkbox,...val] : [...seq,...val]
          }
        },
      },
      data() {
        return {
          selectRowIndex: 0,
          tableColumntable:[],
        };
      },
      methods: {
        handleExpand({row,expand}){
          this.$emit('handleExpand',{row,expand})
        },
        handleSelectionChange(data) {
          if (this.isChild) {
            this.$emit(
              'handleSelectionChange',
              this.$refs[`vxeTableChild${this.childRowIndex}`].getCheckboxRecords(),
            );
          } else {
            const checked = this.$refs.vxeTable.getCheckboxRecords();
            this.$emit('handleSelectionChange', checked, data);
          }
        },
        handleChildSelectionChange(data,index){
          this.$emit('handleChildSelectionChange',data,index)
        },
        setRowStyle({ rowIndex }) {
          if (this.isCellCanClick) {
            if (rowIndex === this.selectRowIndex) {
              return 'background-color: #F6F6FE;color: #4869F4;';
            }
          }
        },
        cellClick({ row, rowIndex, column }) {
          if (column.type === 'checkbox' || !this.isCellCanClick) return;
          this.selectRowIndex = rowIndex;
          this.$emit('currentChart', row);
        },
        setChekboxRow(data) {
          this.$refs.vxeTable.setCheckboxRow(data, true);
        },
        setChildChekboxRow(data){
          console.log(38,data)
          this.$refs[this.dynamicRef].setCheckboxRow(data, true);
        },
        refreshTable(newData) {
          this.$refs.vxeTable.loadData(newData);
        },
        scrollToPosition(position){
          this.$refs[this.dynamicRef].scrollTo(position)
        },
        validateTable() {
          return new Promise((resolve, reject) => {
            // vxe表格校验
            this.$refs.vxeTable.validate(true, (err) => {
              if (err) {
                this.$nextTick(() => {
                  reject();
                });
              } else {
                resolve(true);
              }
            });
          });
        },
      },
    };
  • 改动后父组件调用
        <VxeChildTableView
          :tableData="saleOrderData"
          :tableColumn="saleOrderTableColumn"
          :expandColumn="saleOrderExpandColumn"
          :editMode="false"
          :showCheckBox="false"
          :expandRowKeys="expandRowKeys"
          :isShowFooter="true"
          :footerMethod="footerMethod"
          :footerChildMethod="footerChildMethod"
          :checkedChildData="checkRowKeys"
          @handleExpand="handleExpand"
          v-loading="tableLoading"
          moduleName="sale_order_inventory_goods_table"
          ref="vxeTable"
          @handleChildSelectionChange="handleSelectInventory"
        >
        //主表插槽传入的自定义内容
          <template #warehouseIds="{row,rowIndex}" >
            <el-select
              v-model="row.warehouseIds"
              filterable
              multiple
              collapse-tags
              placeholder="请选择"
              style="width: 100%;"
              @change="freshChildData(rowIndex)"

            >
              <el-option
                v-for="(item, index) in wareHouseList"
                :key="item.id + index"
                :label="item.name"
                :value="item.id"
              ></el-option>
            </el-select>
          </template>
          // 展开表格插槽传入的自定义内容
          <template #spotOrderChildWeight="{row}" >
            <el-input type="number" placeholder="请输入" @change="(val)=>handleSpotOrderChildWeight(row,val)" v-model="row.spotOrderChildWeight" clearable />
          </template>
          <template #spotOrderChildNum="{row}" >
            <el-input type="number" placeholder="请输入" @change="(val)=>handleSpotOrderChildNum(row,val)"  v-model="row.spotOrderChildNum" clearable />
          </template>
        </VxeChildTableView>

说明:在vue2中可以通过$scopedSlots获取所有父组件中传来的插槽,在vue3中则是通过$slots获取