el-table合并表头、动态合并列、合并尾部合计

在有些情况下,我们会有合并表头、合并列、合并尾部合计的需求,这篇文章是为了记录,如何进行合并,方便日后翻阅。

效果图

el-table合并表头

el-table合并列(动态合并)

el-table合并尾部合计

el-table合并表头的实现

这个地方是用的两个表格实现的,即两个el-table,上方的全选、规格、购买时长等属于一个表,下面的阿里云以及数据属于另一个表,将两个表的列宽设为一致,即可实现。

javascript 复制代码
// 主表
<template>
    <div class="pdlr20 h-100" v-if="state.goodsList.length > 0">
      <div class="sticky-top">
      <el-table class="shopping-cart-table" //最上面的表头,需要把表身隐藏
        style="width: 99.9%;"
        :header-row-style="{border: 'none'}"
        :header-cell-style="{border: 'none',height:'60px',fontSize: '14px',fontWeight: 600,color: '#333333',background:'#FFFFFF'}"
        :data="[]">
        <el-table-column width="32px" label="">
          <template #header>
            <el-checkbox
              v-model="state.checkAll"
              :disabled="state.goodsList.length === 0"
              :indeterminate="state.isIndeterminate">
            </el-checkbox>
          </template>
        </el-table-column>
        <el-table-column width="268px" label="全选"></el-table-column>
        <el-table-column width="180px" label="规格"></el-table-column>
        <el-table-column label="购买时长" align="center"></el-table-column>
        <el-table-column width="100px" align="center" label="单价"></el-table-column>
        <el-table-column width="150px" align="center" label="台数"></el-table-column>
        <el-table-column width="120px" align="center" label="小计"></el-table-column>
        <el-table-column width="190px" align="center" label="操作"></el-table-column>
      </el-table>
      </div>
      <div v-for="(item, index) of state.cloudProvider" :key="index"> // 表身中一个个的表
        <cloud-provider-merchant
          v-if="state.goodsClassify[item].length > 0" // 判断子表中的商品长度是否大于0
          :cloudProvider="item" //用于判断当前表是'ali', 'tencent', 'huawei', 'ct', 'baidu', 'jd', 'ks'中的哪一个
          :checkAll="state.checkAll"//是否全选
          :index="index"
          @selection-change="handleSelectionChange"> // 用于计算选中项的预付金额和按需金额
        </cloud-provider-merchant>
      </div>
    </div>
    <el-empty v-if="state.goodsList.length === 0" description="暂无云资源" :image="state.emptyImage" :image-size="240"></el-empty>
  <div class="c6 shopping-cart-footer pdl30r20 font-size-normal font-weight-400 d-flex align-items-center justify-content-between">
    <div class="d-flex align-items-center">
      <el-checkbox
        v-model="state.checkAll"
        :disabled="state.goodsList.length === 0"
        :indeterminate="state.isIndeterminate">全选
      </el-checkbox>
      <el-button
        class="ml50 font-size-normal font-weight-400 c6 pad0 op1"
        type="text"
        @click="deleteCheckedGoods">删除
      </el-button>
    </div>

    <div class="d-flex align-items-center">
      <div class="mr40 d-flex align-items-center" v-if="[].concat(...Object.values(state.checkedGoods)).length > 0">
        <div class="mr20">总计:</div>
        <div class="d-flex text-amount font-size-mini">
          <div class="mr30" v-if="state.reservedTotalPrice > 0">
            <span class="c3 mr6">预付:</span>
            <span class="text-amount">{{ state.reservedTotalPrice.toFixed(2) }} 元</span>
          </div>
          <div v-if="state.onDemandTotalPrice > 0">
            <span class="c3 mr6">按需:</span>
            <span class="text-amount">{{ state.onDemandTotalPrice.toFixed(2) }} 元/小时</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { useStore } from 'vuex'
import { reactive, onMounted, getCurrentInstance, watch } from 'vue'

import CloudProviderMerchant from './CloudProviderMerchant'

const store = useStore()
const { proxy } = getCurrentInstance()

const goodsClassifyInitData = {
  ali: [],
  tencent: [],
  huawei: [],
  ct: [],
  baidu: [],
  jd: [],
  ks: []
}

const state = reactive({
  checkAll: false,
  isIndeterminate: false,
  goodsList: [],
  goodsClassify: JSON.parse(JSON.stringify(goodsClassifyInitData)),
  cloudProvider: ['ali', 'tencent', 'huawei', 'ct', 'baidu', 'jd', 'ks'],
  reservedTotalPrice: 0,
  onDemandTotalPrice: 0,
  emptyImage: require('@assets/images/no-data.png'),
  shoppingCartLoading: false,
  checkedGoods: JSON.parse(JSON.stringify(goodsClassifyInitData))
})

onMounted(() => {
  getGoodsList()
  getTotalPrice()
})

watch(() => store.state.shoppingCartChange, () => {
  getGoodsList()
  getTotalPrice()
  getCheckAllStatus()
})

watch(state.checkedGoods, () => {
  getCheckAllStatus()
})

const getCheckAllStatus = () => {
  if (state.goodsList.length === 0) {
    state.checkAll = false
    state.isIndeterminate = false
    return
  }

  const checkedNum = Object.values(state.checkedGoods).map(item => item.length).reduce((pre, val) => {
    return pre + val
  }, 0)

  if (checkedNum === state.goodsList.length) {
    state.checkAll = true
    state.isIndeterminate = false
  } else if (checkedNum > 0 && checkedNum < state.goodsList.length) {
    state.isIndeterminate = true
  } else {
    state.checkAll = false
    state.isIndeterminate = false
  }
}

const getGoodsList = () => {
  const goodsClassify = {
    ali: [],
    tencent: [],
    huawei: [],
    baidu: [],
    ct: [],
    jd: [],
    ks: []
  }
  state.goodsList = JSON.parse(localStorage.getItem('goodsList')) || []
  state.goodsList.forEach(goods => {
    goodsClassify[goods.cloudProvider].push(goods)
  })
  state.goodsClassify = goodsClassify
}

const getTotalPrice = () => {
  const checkedGoods = [].concat(...Object.values(state.checkedGoods)) // Object.values()返回一个数组,其元素是在对象上找到的可枚举属性值。属性的顺序与通过手动循环对象的属性值所给出的顺序相同,此处是用于找到选中的商品
  const filteredList = state.goodsList.filter(goods => {
    return checkedGoods.find(goodsHash => { // 从商品列表中,筛选出选中的商品
      return goodsHash === goods.goodsHash
    })
  })
  state.reservedTotalPrice = formatFloat(filteredList.filter(item => { //选中商品计算预付金额
    return item.pricingType === 'reserved'
  }).reduce((pre, item) => {
    return pre + (item.goodsNum * item.price)
  }, 0), 2)

  state.onDemandTotalPrice = formatFloat(filteredList.filter(item => { //选中商品计算按需金额
    return item.pricingType === 'onDemand'
  }).reduce((pre, item) => {
    return pre + (item.goodsNum * item.price)
  }, 0), 2)
}

const formatFloat = (num, pos = 2) => {
  return parseFloat(num * (pos * 10), 10) / (pos * 10) // parseFloat() 函数可解析一个字符串,并返回一个浮点数。
}

const deleteCheckedGoods = () => { // 删除已选的厂家
  if ([].concat(...Object.values(state.checkedGoods)).length === 0) {
    proxy.$notify.error({
      title: '错误',
      message: '没有选中的云资源'
    })
    return
  }
  proxy.$confirm('确定要删除云资源吗?', '删除云资源').then(result => {
    if (result === 'confirm') {
      const checkedGoods = [].concat(...Object.values(state.checkedGoods))
      const filteredList = state.goodsList.filter(goods => {
        return !checkedGoods.find(goodsHash => {
          return goodsHash === goods.goodsHash
        })
      })
      state.checkedGoods = JSON.parse(JSON.stringify(goodsClassifyInitData))
      updateGoodsList(filteredList)
    }
  })
}

const updateGoodsList = (goodsList) => { // 删除已选商家时更新商品列表
  goodsList.forEach((item) => {
    item.input = ''
    item.detailsInput = ''
  })
  localStorage.setItem('goodsList', JSON.stringify(goodsList))
  state.goodsList = goodsList
  proxy.$store.commit('setShoppingCartChange')
}

const handleSelectionChange = (cloudProvider, val) => { // 子表调用这方法计算已选商品金额
  state.checkedGoods[cloudProvider] = val
  getTotalPrice()
}
</script>

<style scoped lang="scss">
@import "../../assets/styles/vendor/element-variables";

.shopping-cart-footer {
  width: 100%;
  height: 80px;
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 9999;
  background: $color-white;
}

::v-deep .el-input__inner {
    height: 32px !important;
    line-height: 32px !important;
    padding: 0;
    border: 1px solid #dcdfe6;
  }

::v-deep.el-input__inner:focus {
  background-color: #fff !important;
}
.sticky-top{
  position: sticky;
  top: 0;
  z-index: 99;
}
.shop-plan-btn{
  width: 124px;
  height: 34px;
  line-height: 34px;
  background: #4C66CE;
  border-radius: 17px;
  color: #FFFFFF;
  font-size: 14px;
}
</style>
javascript 复制代码
// 子表
<template>
  <div class="mb10 goods-widget">
    <el-collapse-transition name="el-fade-in">
      <div class="goods-widget-body">
        <!-- 购物车表格内部内容 -->
        <el-table
          style="width: width: 99.9%;"
          :row-style="{height:'38px',fontSize: '12px',color: '#666666',fontWeight: 400}"
          :header-cell-style="handerMethod" //用于合并表头的方法
          row-key="goodsHash"
          :key="index"
          ref="goods-list-table"
          class="goods-widget-table"
          :class="{'goods-widget-body': !state.goodsDetailVisible}"
          :data="state.cloudProviderGoodsList"
          @selection-change="handleSelectionChange">
          <el-table-column width="32px" type="selection" :reserve-selection="true"/>
          <el-table-column width="268px">
            <template #header>
              <div class="d-flex align-items-center">
                <div class="d-flex align-items-center justify-content-between">
                  <div v-if="state.cloudProvider === 'ali'" class="text-amount d-flex align-items-center">
                    <svg-icon class="mr-2" data="@icon/ali_cloud_logo.svg"></svg-icon>
                    <span>阿里云</span>
                  </div>
                  <div v-if="state.cloudProvider === 'tencent'" class="text-primary-blue d-flex align-items-center">
                    <svg-icon class="mr-2" data="@icon/tencent_cloud_logo.svg"></svg-icon>
                    <span>腾讯云</span>
                  </div>
                  <div v-if="state.cloudProvider === 'huawei'" class="text-danger d-flex align-items-center">
                    <svg-icon class="mr-2" data="@icon/huawei_logo.svg"></svg-icon>
                    <span>华为云</span>
                  </div>
                  <div v-if="state.cloudProvider === 'ct'" class="text-ct d-flex align-items-center">
                    <svg-icon class="mr-2" data="@icon/tianyi_cloud_logo.svg"></svg-icon>
                    <span>天翼云</span>
                  </div>
                  <div v-if="state.cloudProvider === 'baidu'" class="d-flex align-items-center">
                    <el-image class="mr-2" :src="require('@assets/images/baidu_logo.png')" style="width: 16px;height: 16px;"/>
                    <span>百度云</span>
                  </div>
                  <div v-if="state.cloudProvider === 'jd'" class="text-ct d-flex align-items-center">
                    <svg-icon class="mr-2" data="@icon/jd_cloud_logo.svg"></svg-icon>
                    <span>京东云</span>
                  </div>
                  <div v-if="state.cloudProvider === 'ks'" class="text-ct d-flex align-items-center">
                    <svg-icon class="mr-2" data="@icon/ks_cloud_logo.svg"></svg-icon>
                    <span>金山云</span>
                  </div>
                  <div>(共 {{ goodsTotalNum }} 台)</div>
                </div>
              </div>
            </template>
            <template #default="scope">
              <el-row>
                {{ pricingTypeMap[scope.row.pricingType] }},{{ scope.row.cpu }}核 {{ scope.row.mem }}GiB,{{ scope.row.zoneName }}
              </el-row>
              <el-row>
                操作系统:{{ scope.row.systemImage }}
              </el-row>
              <el-row>
                流量带宽:{{ scope.row.netBrandWidth > 0 ?  `${scope.row.netBrandWidth}Mbps` : '--'}}
              </el-row>
            </template>
          </el-table-column>
          <el-table-column width="180px">
            <template #default="scope">
              <el-row>
                系统盘:{{ scope.row.systemDisk ? getSystemDiskDescription(scope.row.systemDisk) : '--'}}
              </el-row>
              <el-row>
                数据盘:
                <span v-if="scope.row.dataDisk.length === 0"> -- </span>
                <span v-else-if="scope.row.dataDisk.length === 1">{{ getDataDiskDescription(scope.row.dataDisk)[0] }}</span>
                <div v-else-if="scope.row.dataDisk.length > 1">
                  {{ getDataDiskSize(scope.row) }}
                  <el-popover
                    class="data-disk-popover"
                    effect="dark"
                    placement="right"
                    :width="90"
                    trigger="click"
                  >
                    <template #reference>
                      <el-button class="data-disk-btn" type="text">详情</el-button>
                    </template>
                    <div v-for="(item, index) of getDataDiskDescription(scope.row.dataDisk)" :key="index">{{ item }}</div>
                  </el-popover>
                </div>

              </el-row>
              <el-row class="data-disk-blank">-</el-row>
            </template>
          </el-table-column>
          <el-table-column align="center">
            <template #default="scope">
              <span class="mr-1">{{ scope.row.duration }}</span>
              <span class="" v-if="scope.row.durationUnit === 'Year'">年</span>
              <span class="" v-else-if="scope.row.durationUnit === 'Month'">个月</span>
              <span class="" v-else>小时</span>
            </template>
          </el-table-column>
          <el-table-column width="100px" align="center">
            <template #default="scope">
              <div v-if="scope.row.price">
                <div v-if="scope.row.pricingType === 'onDemand'">
                  <span class="c3">{{ priceDataFormatter(scope.row.price) }}元/小时</span>
                </div>
                <div v-else>
                  <span class="c3">{{ priceDataFormatter(scope.row.price) }}元</span>
                </div>
              </div>
              <div v-else>--</div>
            </template>
          </el-table-column>
          <el-table-column width="150px" align="center">
            <template #default="scope">
              <el-tooltip content="可选范围 1 ~ 999" placement="top-start">
                <el-input-number
                  class="input-number-box c6"
                  v-model="scope.row.goodsNum"
                  :min="1"
                  :max="999"
                  style="width: 130px;border-radius: 4px;"
                  @change="goodsNumChange(scope.row)">
                </el-input-number>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column width="120px" align="center">
            <template #default="scope">
              <div class="text-amount">{{ getTotalPrice(scope.row) }}</div>
            </template>
          </el-table-column>
          <el-table-column width="190px" align="center">
            <template #header>
              <div class="d-flex justify-content-end align-items-center">
                <div class="d-flex mr20" v-if="reservedTotalPrice > 0">
                  <div class="mr4">预付:</div>
                  <div class="text-amount">{{ reservedTotalPrice }}元</div>
                </div>
                <div class="d-flex ml28 mr20" v-if="onDemandTotalPrice > 0">
                  <div class="mr4">按需:</div>
                  <div class="text-amount">{{ onDemandTotalPrice }}元/小时</div>
                </div>
                <el-tooltip content="展开/收起" placement="top" :enterable="false">
                  <el-button type="text" @click="goodsDetailVisibleToggle">
                    <svg-icon v-if="state.goodsDetailVisible" data="@icon/unfold.svg" style="width: 14px; height: 14px;"></svg-icon>
                    <svg-icon v-else data="@icon/unfold.svg" style="width: 14px; height: 14px;"></svg-icon>
                  </el-button>
                </el-tooltip>
              </div>
            </template>
            <template #default="scope">
              <el-button class="el-button-operate" type="primary" @click="buyNow(scope.row)">立即购买</el-button>
              <el-button class="el-button-del" type="info" @click="deleteGoods(scope.row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-collapse-transition>
  </div>
</template>

<script setup>
import { useStore } from 'vuex'
import { reactive, defineProps, defineEmits, getCurrentInstance, ref, onMounted, watch, computed } from 'vue'
const store = useStore()
const { proxy } = getCurrentInstance()
const emit = defineEmits(['selection-change'])

const props = defineProps({
  checkAll: Boolean,
  cloudProvider: String,
  index: Number
})

const state = reactive({
  cloudProvider: props.cloudProvider,
  goodsList: [],
  cloudProviderGoodsList: [],
  checkedGoodsItem: [],
  goodsDetailVisible: true,
  multipleSelection: []
})

const reservedTotalPrice = computed(() => {
  return getTotalPricingTypePrice('reserved')
})

const onDemandTotalPrice = computed(() => {
  return getTotalPricingTypePrice('onDemand')
})

const goodsTotalNum = computed(() => {
  return state.cloudProviderGoodsList.map(item => item.goodsNum).reduce((pre, val) => {
    return pre + val
  }, 0)
})

watch(() => store.state.shoppingCartChange, () => {
  getGoodsList()
})

watch(() => store.state.shoppingCartDeleteAction, () => {
  proxy.$refs['goods-list-table'].clearSelection()
})

watch(() => props.checkAll, (val) => {
  if (val) {
    checkAllAction()
  } else {
    clearChecked()
  }
})

onMounted(() => {
  getGoodsList()
})

const getTotalPricingTypePrice = (pricingType) => {
  return state.cloudProviderGoodsList.filter(item => {
    return item.pricingType === pricingType
  }).map(goods => {
    return Number(goods.price).floatMul(goods.goodsNum)
  }).reduce((pre, val) => { // reduce() 方法对数组中的每个元素执行一个由您提供的reduce函数(升序执行),将其结果汇总为单个返回值。reduce方法可做的事情特别多,就是循环遍历能做的,reduce都可以做,比如数组求和、数组求积、数组中元素出现的次数、数组去重等等。
    return pre.floatAdd(val) // 相加,计算单个商品小计的总额
  }, 0)
}

const pricingTypeMap = ref({
  reserved: '预付实例',
  onDemand: '按需实例'
})

const diskTypeMap = {
  standard: '标准性能',
  efficient: '高性能'
}

const deleteGoods = (goodsItem) => { //删除某项商品
  proxy.$confirm('确定要删除云资源吗?', '删除云资源').then(result => {
    if (result === 'confirm') {
      const index = state.goodsList.findIndex(item => {
        return item.goodsHash === goodsItem.goodsHash
      })

      state.goodsList.splice(index, 1)
      updateGoodsList()
      proxy.$message.success({
        message: '成功删除云资源'
      })
    }
  })
}

const getGoodsList = () => {
  state.goodsList = JSON.parse(localStorage.getItem('goodsList')) || []
  state.cloudProviderGoodsList = state.goodsList.filter(goods => {
    return goods.cloudProvider === props.cloudProvider
  })
}

const updateGoodsList = () => { //改变父组件中价格
  localStorage.setItem('goodsList', JSON.stringify(state.goodsList))
  proxy.$store.commit('setShoppingCartChange')
}

const goodsNumChange = (goodsItem) => { //操作台数时,父表中价格做相应计算
  state.goodsList.forEach(item => {
    if (item.goodsHash === goodsItem.goodsHash && item.goodsNum !== goodsItem.goodsNum) { //只对选中的商品价格做相应计算
      item.goodsNum = goodsItem.goodsNum
    }
  })

  updateGoodsList()
}

const getSystemDiskDescription = ({ type, size }) => {
  return `${ diskTypeMap[type] } | ${ size }GB`
}

const getDataDiskDescription = (dataDisks) => {
  return dataDisks?.map(item => {
    return `${ diskTypeMap[item.type] } | ${ item.size }GB`
  })
}

const getDataDiskSize = (dataDisks) => { //计算数据盘大小
  let size = 0
  dataDisks.dataDisk.map(item => {
    size += item.size
  })
  return `共 ${size} G`
}

const priceDataFormatter = (price) => { //单价保留两位小数
  return Number(price).toFixed(2)
}

const getTotalPrice = (item) => { /单价保留两位小数,floatMul是防止精度丢失的问题
  return `${ Number(item.price).floatMul(item.goodsNum, 2) }${ (item.pricingType === 'reserved' ? '元' : '元/小时') }`
}

const handleSelectionChange = (val) => { // 点击选中/取消选中时调父表中的计算金额的方法
  state.multipleSelection = val
  emit('selection-change', props.cloudProvider, state.multipleSelection.map(item => item.goodsHash))
}

const goodsDetailVisibleToggle = () => { //展开和收起
  state.goodsDetailVisible = !state.goodsDetailVisible
  proxy.$nextTick(() => {
    proxy.$refs['goods-list-table'].doLayout() // 对 Table 进行重新布局。当 Table 或其祖先元素由隐藏切换为显示时,可能需要调用此方法
  })
}

const checkAllAction = () => { // 全选
  state.cloudProviderGoodsList.forEach(item => {
    proxy.$refs['goods-list-table'].toggleRowSelection(item, true) // 用于多选表格,切换某一行的选中状态,如果使用了第二个参数,则是设置这一行选中与否(selected 为 true 则选中)
  })
}

const clearChecked = () => { // 用于多选表格,清空用户的选择
  proxy.$refs['goods-list-table'].clearSelection()
}

// 立即购买
const buyNow = (row) => {
}

const handerMethod = ({ row, column, rowIndex, columnIndex }) => { //合并表头
      if (row[0].level == 1) { // //这里有个非常坑的bug 必须是row[0]=0 row[1]=2才会生效
        row[4].colSpan = 0 // 表头索引为4、5、6时合并到7
        row[5].colSpan = 0
        row[6].colSpan = 0
        row[7].colSpan = 4
        if (columnIndex === 4 || columnIndex === 5 || columnIndex === 6) { // columnIndex 代表列号/列索引,隐藏
          return { display: 'none' }
        }
      }
}

</script>

<style scoped lang="scss">
@import "../../assets/styles/vendor/element-variables";
.el-table td.el-table__cell div {
  line-height: 32px;

  .data-disk-btn {
    color: #409EFF;
  }

  .data-disk-blank {
    color: #FFF;
  }
}

.text-ct{
  color: $color-tianyi;
}

.el-checkbox {
  --el-checkbox-checked-background-color: #fd852d;
  --el-checkbox-checked-input-border-color: #fd852d;
  --el-checkbox-input-border-color-hover: #fd852d;
}

.goods-widget {
  background: #fff;
  border-radius: 4px;

  &-footer, &-body {
    box-sizing: border-box;
    border-top: 1px solid var(--el-border-color-base);
  }

  ::v-deep(.el-form-item) {
    margin-bottom: 8px;
  }
}
::v-deep .input-number-box{
  height: 32px;
  border: 1px solid #EAEBEF;
  .el-input-number__increase, .el-input-number__decrease{
    font-size: 12px;
    font-weight: 400;
  }
  .el-input__inner{
    height: 28px !important;
    line-height: 28px !important;
    font-size: 12px;
    border: none;
  }
}
::v-deep.el-button-operate{
  width: 80px;
  height: 32px;
  line-height: 32px;
  background-color: #EBEFFB;
  color: #4C66CE;
  font-size: 12px;
  border-radius: 4px;
  border: none;
  &:hover{
    background-color: #4C66CE !important;
    color: #FFFFFF;
  }
}
::v-deep.el-button-del{
  width: 52px;
  height: 32px;
  line-height: 32px;
  background-color: #F2F2F4;
  color: #666666;
  font-size: 12px;
  border-radius: 4px;
  border: none;
  &:hover{
    background-color: #F2F2F4 !important;
    color: #666666;
  }
}
</style>

el-table合并列(动态合并)的实现

javascript 复制代码
<template>
     <el-table
              class="procurement-plan-table procurement-plan-table-noborder"
              :row-style="{height: '48px',fontSize: '12px',color: '#666666'}"
              :header-cell-style="{height: '48px',background: '#F6F6F8',fontSize: '14px',color: '#333333',fontWeight: 400}"
              :cell-class-name="cellClassName" //因为设计稿上是只有首位才需要左右边框,合并的单元格需要去除右侧的边框
              :data="state.tempGoodList"
              :span-method="objectSpanMethod" //合并单元格
              ref="table"
              border
              :summary-method="getSummaries"
              show-summary
              :style="{borderColor: '#E6E6E6'}"
            >
              <el-table-column label="云厂商" width="120px" align="center" prop="cloudProvider">
                <template #default="scope">
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'ali'">
                    <div class="text-center">阿里云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'tencent'">
                    <div class="text-center">腾讯云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'huawei'">
                    <div class="text-center">华为云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'ct'">
                    <div class="text-center">天翼云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'baidu'">
                    <div class="text-center">百度云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'jd'">
                    <div class="text-center">京东云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'ks'">
                    <div class="text-center">金山云</div>
                  </div>
                </template>
              </el-table-column>
              <el-table-column label="类型" width="120px" align="center">
                  云服务器
              </el-table-column>
              <el-table-column label="付费方式" width="110px" align="center">
                <template #default="scope">
                  {{ scope.row.pricingType === 'reserved' ? '预付' :  '按需'}}
                </template>
              </el-table-column>
              <el-table-column label="数量" width="110px" align="center">
                <template #default="scope">
                  {{ scope.row.goodsNum }}
                </template>
              </el-table-column>
              <el-table-column label="小计" width="130px">
                <template #default="scope">
                  <div>
                    <span>{{  scope.row.price * scope.row.goodsNum }}</span>
                    <span v-if="scope.row.pricingType === 'reserved'">元</span>
                    <span v-else>元/小时</span>
                  </div>
                </template>
              </el-table-column>
              <el-table-column label="备注" align="center">
                <template #default="scope">
                  <el-input class="procurement-plan-table-input" v-model="scope.row.input" type="textarea" placeholder="请输入备注" @blur="saveMemo" />
                </template>
              </el-table-column>
     </el-table>
</template>
<script setup>
import { reactive, onMounted, getCurrentInstance } from 'vue'
import { ArrowLeft } from '@element-plus/icons-vue'
import transform from './common/toExcel.js'
import moment from 'moment'

import AppMenuChange from '@component/AppMenuChange'

const { proxy } = getCurrentInstance()

onMounted(() => {
  getGoodsList()
  getCollectTableData()
})

const diskTypeMap = {
  standard: '标准性能',
  efficient: '高性能'
}

const durationUnitMap = {
  Hour: '小时',
  Month: '月',
  Year: '年'
}

const state = reactive({
  cloudProvider: {
    ali: [],
    tencent: [],
    huawei: [],
    ct: [],
    baidu: [],
    jd: [],
    ks: []
  },
  goodsList: [],
  collectTableData: [
    { cloudProvider: '', reserved: { num: 0, price: 0 }, onDemand: { num: 0, price: 0 } }
  ],
  tableData: [],
  purchasePurpose: '部署高可用的云上网站架构,支持业务流量跨可用区进行并发,并具备跨可用区故障容灾能力。' || JSON.parse(localStorage.getItem('purchasePurpose')),
  printObj: {
    id: 'pdf',
    popTitle: '',
    // extraHead: '打印', // 最上方的头部文字,附加在head标签上的额外标签,使用逗号分割
    preview: false, // 是否启动预览模式,默认是false
    previewTitle: ' ', // 打印预览的标题
    extraCss: '',
    extraHead: '<meta http-equiv="Content-Language"content="zh-cn"/>,<style> #pdf { width: 100%; height: auto !important; } <style>'
  },
  detailsForm: [],
  tempStr: '',
  cloudProviderInfo: [
    { id: 'ali', name: '阿里云' },
    { id: 'baidu', name: '百度云' },
    { id: 'huawei', name: '华为云' },
    { id: 'ct', name: '天翼云' },
    { id: 'tencent', name: '腾讯云' },
    { id: 'ks', name: '金山云' },
    { id: 'jd', name: '京东' }
  ],
  tempCloudName: null,
  tempGoodList: []
})

const typeNameArr = []
let typeNamePos = 0

// 导出为excel
const toExcel = () => {
  state.detailsForm = []
  state.tempStr = ''
  state.goodsList.forEach((item) => {
    let tempCloudName = ''
    state.cloudProviderInfo.filter((subitem) => {
      if (subitem.id === item.cloudProvider) {
        tempCloudName = subitem.name
        return subitem.name
      }
    })
    if (item.dataDisk) {
      state.tempStr = getDataDiskDescription(item.dataDisk)
    }

    state.detailsForm.push(
      {
        cloudProvider: tempCloudName,
        standardID: item.standardID,
        info: `${item.cpu}核${item.mem}GiB, ${item.zoneName}, 系统盘:${item.systemDisk ? getSystemDiskDescription(item.systemDisk) : '--'}, 数据盘:${state.tempStr},固定带宽:${item.netBrandWidth}M`,
        time: `${getDuration(item) ? getDuration(item) : '按需'}`,
        price: `${item.price} ${item.pricingType === 'reserved' ? '元' : '元/小时'}`,
        memo: item.detailsInput ? item.detailsInput : ''
      }
    )
  })
  transform(state.detailsForm, '云服务器采购清单')
}

// 合并单元格
const objectSpanMethod = ({
  row,
  column,
  rowIndex,
  columnIndex
}) => {
  if (columnIndex === 0) {
    const _row = typeNameArr[rowIndex]
    const _col = _row > 0 ? 1 : 0
    return {
      rowspan: _row,
      colspan: _col
    }
  }
  proxy.$nextTick(() => {
    if (proxy.$refs.table.$el) {
      const current = proxy.$refs.table.$el.querySelector('.el-table__footer-wrapper').querySelector('.el-table__footer')
      const cell = current.rows[0].cells
      cell[1].style.display = 'none'
      cell[2].classList.remove('is-left')
      cell[2].colSpan = '2'
      cell[3].style.display = 'none'
      cell[4].classList.remove('is-left')
      cell[4].colSpan = '2'
    }
  })
}

// 设置cell样式
const cellClassName = ({
  row,
  column,
  rowIndex,
  columnIndex
}) => {
  if (columnIndex !== 0) {
    return 'noRightBorderClass'
  }
}

// 在本地存储里获取购物清单
const getGoodsList = () => {
  state.goodsList = JSON.parse(localStorage.getItem('checkedGoodsList')) || []

  const tempGoodObject = {
    ali: [],
    tencent: [],
    huawei: [],
    baidu: [],
    ks: [],
    ct: [],
    jd: []
  }
  // tempGoodList.push(state.goodsList[0])

  state.goodsList.forEach((item) => {
    if (item.cloudProvider === 'ali') tempGoodObject.ali.push(item)
    if (item.cloudProvider === 'tencent') tempGoodObject.tencent.push(item)
    if (item.cloudProvider === 'huawei') tempGoodObject.huawei.push(item)
    if (item.cloudProvider === 'baidu') tempGoodObject.baidu.push(item)
    if (item.cloudProvider === 'ks') tempGoodObject.ks.push(item)
    if (item.cloudProvider === 'ct') tempGoodObject.ct.push(item)
    if (item.cloudProvider === 'jd') tempGoodObject.jd.push(item)
  })
  state.tempGoodList = [
    ...tempGoodObject.ali,
    ...tempGoodObject.tencent,
    ...tempGoodObject.huawei,
    ...tempGoodObject.baidu,
    ...tempGoodObject.ks,
    ...tempGoodObject.ct,
    ...tempGoodObject.jd
  ]

  for (let i = 0; i < state.tempGoodList.length; i += 1) {
    if (i === 0) {
      typeNameArr.push(1)
      typeNamePos = 0
    } else {
      if (state.tempGoodList[i].cloudProvider === state.tempGoodList[i - 1].cloudProvider) {
        typeNameArr[typeNamePos] += 1
        typeNameArr.push(0)
      } else {
        typeNameArr.push(1)
        typeNamePos = i
      }
    }
  }
}
</script>

el-table合并尾部合计的实现

javascript 复制代码
<template>
     <el-table
              class="procurement-plan-table procurement-plan-table-noborder"
              :row-style="{height: '48px',fontSize: '12px',color: '#666666'}"
              :header-cell-style="{height: '48px',background: '#F6F6F8',fontSize: '14px',color: '#333333',fontWeight: 400}"
              :cell-class-name="cellClassName" //因为设计稿上是只有首位才需要左右边框,合并的单元格需要去除右侧的边框
              :data="state.tempGoodList"
              :span-method="objectSpanMethod" //合并单元格
              ref="table"
              border
              :summary-method="getSummaries" //底部合计行
              show-summary
              :style="{borderColor: '#E6E6E6'}"
            >
              <el-table-column label="云厂商" width="120px" align="center" prop="cloudProvider">
                <template #default="scope">
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'ali'">
                    <div class="text-center">阿里云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'tencent'">
                    <div class="text-center">腾讯云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'huawei'">
                    <div class="text-center">华为云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'ct'">
                    <div class="text-center">天翼云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'baidu'">
                    <div class="text-center">百度云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'jd'">
                    <div class="text-center">京东云</div>
                  </div>
                  <div class="d-flex align-items-center justify-content-center" v-if="scope.row.cloudProvider === 'ks'">
                    <div class="text-center">金山云</div>
                  </div>
                </template>
              </el-table-column>
              <el-table-column label="类型" width="120px" align="center">
                  云服务器
              </el-table-column>
              <el-table-column label="付费方式" width="110px" align="center">
                <template #default="scope">
                  {{ scope.row.pricingType === 'reserved' ? '预付' :  '按需'}}
                </template>
              </el-table-column>
              <el-table-column label="数量" width="110px" align="center">
                <template #default="scope">
                  {{ scope.row.goodsNum }}
                </template>
              </el-table-column>
              <el-table-column label="小计" width="130px">
                <template #default="scope">
                  <div>
                    <span>{{  scope.row.price * scope.row.goodsNum }}</span>
                    <span v-if="scope.row.pricingType === 'reserved'">元</span>
                    <span v-else>元/小时</span>
                  </div>
                </template>
              </el-table-column>
              <el-table-column label="备注" align="center">
                <template #default="scope">
                  <el-input class="procurement-plan-table-input" v-model="scope.row.input" type="textarea" placeholder="请输入备注" @blur="saveMemo" />
                </template>
              </el-table-column>
     </el-table>
</template>
<script setup>
import { reactive, onMounted, getCurrentInstance } from 'vue'
import { ArrowLeft } from '@element-plus/icons-vue'
import transform from './common/toExcel.js'
import moment from 'moment'

import AppMenuChange from '@component/AppMenuChange'

const { proxy } = getCurrentInstance()

onMounted(() => {
  getGoodsList()
  getCollectTableData()
})

const diskTypeMap = {
  standard: '标准性能',
  efficient: '高性能'
}

const durationUnitMap = {
  Hour: '小时',
  Month: '月',
  Year: '年'
}

const state = reactive({
  cloudProvider: {
    ali: [],
    tencent: [],
    huawei: [],
    ct: [],
    baidu: [],
    jd: [],
    ks: []
  },
  goodsList: [],
  collectTableData: [
    { cloudProvider: '', reserved: { num: 0, price: 0 }, onDemand: { num: 0, price: 0 } }
  ],
  tableData: [],
  purchasePurpose: '部署高可用的云上网站架构,支持业务流量跨可用区进行并发,并具备跨可用区故障容灾能力。' || JSON.parse(localStorage.getItem('purchasePurpose')),
  printObj: {
    id: 'pdf',
    popTitle: '',
    // extraHead: '打印', // 最上方的头部文字,附加在head标签上的额外标签,使用逗号分割
    preview: false, // 是否启动预览模式,默认是false
    previewTitle: ' ', // 打印预览的标题
    extraCss: '',
    extraHead: '<meta http-equiv="Content-Language"content="zh-cn"/>,<style> #pdf { width: 100%; height: auto !important; } <style>'
  },
  detailsForm: [],
  tempStr: '',
  cloudProviderInfo: [
    { id: 'ali', name: '阿里云' },
    { id: 'baidu', name: '百度云' },
    { id: 'huawei', name: '华为云' },
    { id: 'ct', name: '天翼云' },
    { id: 'tencent', name: '腾讯云' },
    { id: 'ks', name: '金山云' },
    { id: 'jd', name: '京东' }
  ],
  tempCloudName: null,
  tempGoodList: []
})

const typeNameArr = []
let typeNamePos = 0

// 导出为excel
const toExcel = () => {
  state.detailsForm = []
  state.tempStr = ''
  state.goodsList.forEach((item) => {
    let tempCloudName = ''
    state.cloudProviderInfo.filter((subitem) => {
      if (subitem.id === item.cloudProvider) {
        tempCloudName = subitem.name
        return subitem.name
      }
    })
    if (item.dataDisk) {
      state.tempStr = getDataDiskDescription(item.dataDisk)
    }

    state.detailsForm.push(
      {
        cloudProvider: tempCloudName,
        standardID: item.standardID,
        info: `${item.cpu}核${item.mem}GiB, ${item.zoneName}, 系统盘:${item.systemDisk ? getSystemDiskDescription(item.systemDisk) : '--'}, 数据盘:${state.tempStr},固定带宽:${item.netBrandWidth}M`,
        time: `${getDuration(item) ? getDuration(item) : '按需'}`,
        price: `${item.price} ${item.pricingType === 'reserved' ? '元' : '元/小时'}`,
        memo: item.detailsInput ? item.detailsInput : ''
      }
    )
  })
  transform(state.detailsForm, '云服务器采购清单')
}

// 合并单元格
const objectSpanMethod = ({
  row,
  column,
  rowIndex,
  columnIndex
}) => {
  if (columnIndex === 0) {
    const _row = typeNameArr[rowIndex]
    const _col = _row > 0 ? 1 : 0
    return {
      rowspan: _row,
      colspan: _col
    }
  }
  proxy.$nextTick(() => {
    if (proxy.$refs.table.$el) {
      const current = proxy.$refs.table.$el.querySelector('.el-table__footer-wrapper').querySelector('.el-table__footer')
      const cell = current.rows[0].cells
      cell[1].style.display = 'none'
      cell[2].classList.remove('is-left')
      cell[2].colSpan = '2'
      cell[3].style.display = 'none'
      cell[4].classList.remove('is-left')
      cell[4].colSpan = '2'
    }
  })
}

// 设置cell样式
const cellClassName = ({
  row,
  column,
  rowIndex,
  columnIndex
}) => {
  if (columnIndex !== 0) {
    return 'noRightBorderClass'
  }
}

// 在本地存储里获取购物清单
const getGoodsList = () => {
  state.goodsList = JSON.parse(localStorage.getItem('checkedGoodsList')) || []

  const tempGoodObject = {
    ali: [],
    tencent: [],
    huawei: [],
    baidu: [],
    ks: [],
    ct: [],
    jd: []
  }
  // tempGoodList.push(state.goodsList[0])

  state.goodsList.forEach((item) => {
    if (item.cloudProvider === 'ali') tempGoodObject.ali.push(item)
    if (item.cloudProvider === 'tencent') tempGoodObject.tencent.push(item)
    if (item.cloudProvider === 'huawei') tempGoodObject.huawei.push(item)
    if (item.cloudProvider === 'baidu') tempGoodObject.baidu.push(item)
    if (item.cloudProvider === 'ks') tempGoodObject.ks.push(item)
    if (item.cloudProvider === 'ct') tempGoodObject.ct.push(item)
    if (item.cloudProvider === 'jd') tempGoodObject.jd.push(item)
  })
  state.tempGoodList = [
    ...tempGoodObject.ali,
    ...tempGoodObject.tencent,
    ...tempGoodObject.huawei,
    ...tempGoodObject.baidu,
    ...tempGoodObject.ks,
    ...tempGoodObject.ct,
    ...tempGoodObject.jd
  ]

  for (let i = 0; i < state.tempGoodList.length; i += 1) {
    if (i === 0) {
      typeNameArr.push(1)
      typeNamePos = 0
    } else {
      if (state.tempGoodList[i].cloudProvider === state.tempGoodList[i - 1].cloudProvider) {
        typeNameArr[typeNamePos] += 1
        typeNameArr.push(0)
      } else {
        typeNameArr.push(1)
        typeNamePos = i
      }
    }
  }
}

const getSummaries = () => { // 底部合计行
  const reservedNum = getSummariesNum('reserved')
  const reservedPrice = getSummariesPrice('reserved')
  const onDemandNum = getSummariesNum('onDemand')
  return ['合计', '', `按需实例: ${onDemandNum}台,预付实例: ${reservedNum}台`, '', `预付: ${reservedPrice}元`, '按需实例为后付费,云账户有一定与余额即可']
}

const getSummariesNum = (type) => { // 计算按需/预付多少台
  return state.collectTableData.map(item => {
    return item[type].num
  }).reduce((pre, value) => pre + value)
}

const getSummariesPrice = (type) => { // 计算预付价格
  return state.collectTableData.map(item => {
    return item[type].price
  }).reduce((pre, value) => {
    return (parseInt((pre + value) * 100, 10) / 100)
  })
}
</script>
相关推荐
Boilermaker199221 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子33 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构