vue2使用el-cascader在table中下拉框不跟随滚动问题

在vue2中 ,使用 element 中的 级联选择器的时候, 当把级联选择器当插槽放入table中的时候,且这个组件是出现在 el-drawer 抽屉里面的情况下, 这时候如果有纵向滚动条,滚动的时候, el-cascader下拉框不跟随滚动, 这个问题解决了好久,使用别的库的下拉框, 点击的时候下拉框要么有层级问题,要么不显示。针对这一问题进行一次总结。

场景

  • 步长: 点击按钮, 打开 抽屉, 抽屉中显示一个 el-table 表格, 表格中 动态添加 el-cascader 级联选择器

代码

  • 代码中主要看 el-cascader 组件即可, v-model 绑定的值还没写,属于后续内容,这里只展示这个问题的解决方案,这里主要利用的就是在子组件上 配置 ref mySelect2, 父组件侦听滚动事件。 解决方案代码在下面。
js 复制代码
<template>
  <div>
    <el-table
      :data="tableData"
      :span-method="objectSpanMethod"
      border
      stripe
      class="compare-table"
      style="margin-top: 20px"
      tooltip-effect="dark"
      :cell-style="cellStyleHandler"
      :cell-class-name="cellStyleHandler2"
      >
      <el-table-column
        v-for="item in columns"
        :key="item.label"
        :prop="item.prop"
        :width="item?.width"
        min-width="230"
        :label="item.label"
        show-overflow-tooltip
        align="center"
      >
        <template slot-scope="scope">
          <template v-if="renderOtherSlot(scope, item)">
            <div>
              {{  scope.row[item.prop].value ? '否' : '是'}}
            </div>
          </template>
          <template v-else-if="renderSlot(scope) === 'RENDER'">
            <template v-if="scope.row.slot">
                <el-select ref="mySelect1" @change="(value)=>changeHandler(scope, item,value)" v-model="scope.row[scope.column.property].source[scope.row.slot]" multiple collapse-tags placeholder="请选择">
                  <el-option
                    v-for="item in returnOptions(scope, item)"
                    :key="item.label"
                    :label="item.value"
                    :value="item.label">
                  </el-option>
                </el-select>
            </template>
            <div class="slot-class" v-else>
              <div>
                <el-tooltip effect="dark" content="1%" placement="top" :disabled="true">
                  <i class="el-icon--left" v-if="renderCompare(scope,2)" ><svg-icon icon-class="compare_big" /></i>
                  <i class="el-icon--left" v-if="renderCompare(scope,1)"><svg-icon icon-class="compare_small" /></i>
                </el-tooltip>
              </div>
              <div class="ellipsis-reset">
                <template>
                  {{renderCompareValue(scope, item) ?? '-'}}
                </template>
              </div>
            </div>
          </template>
          <template v-else>
            <template v-if="decideSlot(scope, item)">
                <el-cascader 
                ref="mySelect2" 
                :options="returnOptions(scope, item)"
                collapse-tags
                size="mini"
                filterable
                :props="{ multiple: true }"
                @change="cascaderChangeHandler"
                @visible-change="visibleChange"
                clearable/>
            </template>
            <template v-else>
              {{renderSlot(scope, item)}}
            </template>
          </template>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
import { UNIT_ENUM } from '@/views/flow-direction/const';

export default {
  props: {
    columns: {
      type: Array,
      default: () => []
    },
    basisData: {
      type: Array,
      default: () => []
    },
    getAnalysisReuslt: {
      type: Function,
      default: () => {}
    }
  },
  data() {
    return {
        form: {
        },
        rules: {
        },
        arrSort: [],
        tableData: [
          // compareType 类别, content 对比内容 , effect 对比结果
          {compareType: '结果指标',type:'segmentNames',content:'管段名称',  slot: 'selectSegmentCodes'},
          {compareType: '结果指标',type:'stationNames',content:'站场名称',  slot: 'selectStationCodes'},
          {compareType: '结果指标',type:'segmentNum',content:'建管段数量'},
          {compareType: '结果指标',type:'segmentUseLength',content:'管道投产里程' + `(${UNIT_ENUM.KM})`},
          // {compareType: '结果指标',type:'totalDemand',content:'总需求'},
          {compareType: '结果指标',type:'stationNum',content:'建站数量'},
          {compareType: '结果指标',type:'loadRatio',content:'平均负荷率'},
          {compareType: '结果指标',type:'power',content:'年功率' + `(${UNIT_ENUM.POWER})` },
          {compareType: '结果指标',type:'stationInvestment',content:'站场投资'  + `(${UNIT_ENUM.TEM_PRICE})` },
          {compareType: '结果指标',type:'segmentInvestment',content:'管道投资' + `(${UNIT_ENUM.TEM_PRICE})` },
          {compareType: '结果指标',type:'buildCost',content:'建设费用'  + `(${UNIT_ENUM.TEM_PRICE})` },
          {compareType: '结果指标',type:'operatingCost',content:'运行费用'  + `(${UNIT_ENUM.TEM_PRICE})` },
          {compareType: '结果指标',type:'presentCost',content:'费用现值'  + `(${UNIT_ENUM.TEM_PRICE})` },
          {compareType: '结果指标',type:'turnoverEnergyConsumption',content:'单位周转量能耗' + `(${UNIT_ENUM.UNIT_REVOLVE})`},

          {compareType: '全局约束条件',type:'loadRationRelaxMax',content:'负荷率松弛上限值' + `(${UNIT_ENUM.PERCENT})`},
          {compareType: '全局约束条件',type:'energyTarget',content:'能耗目标值' + `(${UNIT_ENUM.COST})`},
          {compareType: '全局约束条件',type:'turnoverTarget',content:'周转量目标值' + `(${UNIT_ENUM.REVOLVE}/天)`},
          {compareType: '全局约束条件',type:'costTarget',content:'供气成本目标值'  + `(${UNIT_ENUM.TEM_PRICE})` },

          {compareType: '优化目标权重',type:'minimumTotalTurnover',content:'最小总周转量' + `(${UNIT_ENUM.PERCENT})`},
          {compareType: '优化目标权重',type:'optimalLoadRatio',content:'最优负荷率' + `(${UNIT_ENUM.PERCENT})`},
          {compareType: '优化目标权重',type:'minimumReceptionCost',content:'最小接气成本' + `(${UNIT_ENUM.PERCENT})`},
          {compareType: '优化目标权重',type:'minimumEnergy',content:'最低能耗' + `(${UNIT_ENUM.PERCENT})`},

          {compareType: '模型计算配置',type:'maxRunTime',content:'计算终止时长(s)'},
          {compareType: '模型计算配置',type:'convergencePrecision',content:'压力误差(%)'},
          {compareType: '模型计算配置',type:'isWaterConservancy',content:'模型计算配置'},
          
          {compareType: '推荐排序',type:'sort',content:'经济评价指标'},
        ]
    };
  },
  // 数据重置
  created() {
    this.arrSort = []
    // 改造数据源
    this.basisData.forEach((item)=>{
      const { modelName, constraints, executeConfig, optimizeTargetWeight, basIndicators} = item 
      this.arrSort.push(modelName) 
      const currentObj = { ...constraints, ...executeConfig, ...optimizeTargetWeight, ...basIndicators}
      Object.keys(currentObj).forEach((big)=>{
        // 循环,对照数据,找到 tableData 展示 数据中的对应项, 进行赋值。
        const showCurrent = this.tableData.find((small)=>small.type === big)
        if(showCurrent){
          // 响应式复制, 给当前列 复制 value 对象
          this.$set(showCurrent, modelName, { value: currentObj[big], source: currentObj})
        }
      })
    })
    const denominator = this.tableData.find(item=>item.type === 'presentCost')[this.arrSort[0]].value
    // 第一层循环当前数据、第二层 找到当前列对应的数据, 与第一列对应的数据进行对比, 给当前列数据显示对象 复制对比结果
    this.tableData.forEach((item)=>{
      this.arrSort.map((it,index)=>{
        const compare = this.arrSort[index]
        const compareValue = item[compare]?.value ?? null  // 当前列的值
        const currentValue = item[this.arrSort[0]]?.value ?? null  // 第一列需要对比的值
        // 如果 需要对比的值为 null  或者当前列的值为 null  则不对比
        // 找到第一列的 费用现值, 需要用作分母的, 这里判定 如果分母是 0 , 则不渲染箭头
        if(item.type === 'sort' && denominator === 0){
          item[compare].effect = 0
        }else if(currentValue === null || compareValue === null){ item[compare].effect = 0 }  
        else{
          item[compare].effect = compareValue > currentValue ? 2 : compareValue < currentValue ? 1 : 0;
        }
      })
    })
  },
  mounted() {
  },
  methods: {
    changeHandler(scope, item, selectValue){
      const { row, column } = scope
      const { property } = column
      const { type } = row
      const current = {  modelId: item.modelId }
      if(type === "stationNames"){
        console.log('ceasfd',row[property].source.selectSegmentCodes)
        current.selectStationCodes = selectValue
        current.selectSegmentCodes = row[property].source.selectSegmentCodes
      }
      if(type === "segmentNames"){
        current.selectSegmentCodes = selectValue
      }
      // 触发页面更新
      this.getAnalysisReuslt(current)
    },
    cascaderChangeHandler(value){
      console.log('value',value);
    },
    visibleChange(value){
      console.log('visibleChange',value);
      if(!value) return
      const container = document.querySelector('.el-cascader__dropdown');

        if (container) {
          console.log('83',container);
          container.style.opacity = 1; 
        }
    },
    // 渲染对比的值
    renderCompareValue(data, item){
      const { row, column:{ label } } = data 
      const { type, slot } = row
      const value = row[label]?.value
      if(type === 'sort' &&  value){
        const current = this.tableData.find(item=>item.type === 'presentCost')
        // 计算百分比 使用当前列的费用现值 / 第一列的费用现值 * 100%   第一列的 label 为 this.arrSort[0] 当前 label 为 lable
        // 如果 分母是 0 , 则不计算
        const denominator = current[this.arrSort[0]].value   // 分母
        const molecule = current[label].value
        if(denominator === 0) return '-'
        return value + `(${(molecule / denominator * 100).toFixed(2)}%)` 
      }
      if(slot){
        return this.renderNames(row[item.label].source, slot, value)
      }
      return value ?? '-'
    },
    renderCompare(data,num){
      // 渲染箭头方向
      const { row, column:{ label } } = data 
      return row[label]?.effect === num
    },
    // 判定第一列的模型内容
    decideSlot(scope, item){
      const { row, column } = scope
      const { property } = column
      if(row.slot && typeof row[property] === 'object' && item){
        return true
      }
      return false
    },
    // 返回下拉列表 
    returnOptions(scope, item){
      const { row, column } = scope
      const { property } = column
      const { type } = row
      if(type === "stationNames") return row[property].source['stations']
      if(type === "segmentNames") return row[property].source['segments']
    },
    // 渲染绑定的值
    returnSelectValue(scope, item){
      const { row, column } = scope
      const { property } = column
      const { type } = row
      if(type === "stationNames") return row[property].source['selectStationCodes']
      if(type === "segmentNames") return row[property].source['selectSegmentCodes']
    },
    renderSlot(scope, item){
      const { row, column } = scope
      const { property } = column
      // 排除第一列, 第二列之后的使用 箭头插槽渲染
      let arr = [...this.arrSort].slice(1)
      if(arr.includes(property)){
        return 'RENDER'
      }
      // 值是对象的,标识不是类别 和 对比内容, 直接返回 value 
      const value = row[property]?.value
      if(row.type === 'sort' && typeof row[property] === 'object') return value ? `${value}(100%)` : '-' // 渲染第一列, 经济评价指标的百分比
      if(row.slot && typeof row[property] === 'object' && item){
        return this.renderNames(row[item.label].source,row.slot, value)
      }
      if(typeof row[property] === 'object') return value ?? '-'
      return row[property]  // 返回前两列的固定label
    },
    // 渲染不需要对比的值
    renderOtherSlot(scope, item){
      const { row, column: { property } } = scope
      if(row.type === "isWaterConservancy" && property !== 'compareType' && property !== 'content') return true
      return false
    },
    // 合并渲染管段数量以及站场数量
    renderNames(obj,key,value){
      if(value === null || value === undefined) return '-'
      if(value === 0) return value
      // console.log('obj',obj[key]);
      return `${value}(${obj[key]})`
    },
    // 设置斑马线样式
    cellStyleHandler2({row, column, rowIndex, columnIndex}){
      if(columnIndex === 0) return 'cell-reset'
    },
    cellStyleHandler({row, column, rowIndex, columnIndex}){
      // 设置第一列的样式
      if(columnIndex === 0){
        return {
          backgroundColor: "#fff"
        }
      }
    },
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      // columnIndex 为当前列的索引, 我们只合并第一列, compareType 为当前列的值 rowIndex 为当前列的行数,  rowspan 是我们要合并多少行, 这里拿第一个 switch 的条件距离,rowspan 11 表示合并11行, colspan 表示 显示1列。 那么我们在想要合并11行的话, 就在第 11 行 合并即可, 其他 10行的 行跨度 和 列跨度 都返回0 即可。
      const { compareType, content } = row
        if (columnIndex === 0) {
          switch(compareType){
            case "结果指标":
              // console.log('data', row, column, rowIndex, rowIndex % 11, '---',columnIndex);
              if (rowIndex % 13 === 0) {
                  return {
                    rowspan: 13, // 行跨度
                    colspan: 1 // 列跨度
                  };
                } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            case "全局约束条件":
              if (rowIndex % 13 === 0) {
                return {
                  rowspan: 4,
                  colspan: 1
                };
              } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            case "优化目标权重": 
              if (rowIndex % 17 === 0) {
                  return {
                    rowspan: 4,
                    colspan: 1
                  };
              } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            case "模型计算配置":
              // console.log('data', row, column, rowIndex, rowIndex % 20, '---',columnIndex);
              if (rowIndex % 21 === 0) {
                    return {
                      rowspan: 3,
                      colspan: 1
                    };
              } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            default: 
              return {
                rowspan: 1,
                colspan: 1
              }
          }
        }
      }
  },
};
</script>
<style lang="scss" scoped>
@import "@/views/flow-direction/model-manage/compare-model/compare.scss";
</style>

问题

页面当中高度不够, 内部容器出现了滚动条 滚动的时候下拉框并没有跟随滚动

解决方案

  • 父组件, 需要滚动的容器上添加 scroll 事件, 滚动的时候通过ref 获取子组件当中的 级联选择器, 配置每一个选择器的 item.dropDownVisible = false; 属性 , 这样就能完成在滚动的时候动态隐藏下拉框了。
  • 这里面还有一个是 mySelect1 的 ref, 这个是 select 下拉框, 他也有同样的问题, 后续我业务上会把这个也替换成级联选择器。 select 这个问题的解决方案就是使用 item?.blur() 方法即可, 滚动的时候, 控制器失焦,让其隐藏即可。
js 复制代码
<template>
  <el-drawer
    :visible.sync="visible"
    title="结果对比"
    :size="820"
    ref="drawer"
    class="flow-drawer"
    @close="() => { visible = false }"
  >
    <div class="drawer-container"  v-loading="loading" @scroll="scrollHandler">
      <template v-if="basisData.length !== 0">
        <AnalysisCompare  ref="AnalysisCompare" :key="analysisKey" :getAnalysisReuslt="getAnalysisReuslt" v-if="modelType === 3" :columns="analysisColumns" :basisData="basisData"/>
        <BasisCompare v-else :columns="basisColumns" :basisData="basisData"/>
      </template>
      <el-empty v-else description="暂无数据"></el-empty>
    </div>
    <div class="demo-drawer__footer">
      <el-button v-if="basisColumns.length !== 0"  :disabled="loading" type="primary" @click="exportHandler" :loading="btnLoading">导出</el-button>
      <el-button @click="()=>{ visible = false }" >关闭</el-button>
    </div>
  </el-drawer>
</template>

<script>
import BasisCompare from './BasisCompare'
import AnalysisCompare from './AnalysisCompare.vue'
import { modelContrastApi, exportModelContrastApi, exportModelContrastAnalsisApi } from '@/flow-api/model'
import { downloadFile } from '@/views/flow-direction/basic-data/utils'
import { testData } from '@/建设方案结果对比返回结果格式'
export default {
  components: {
    BasisCompare,
    AnalysisCompare
  },
  props:{
    // 需要对比的两条数据
    row: {
      type: Array,
      default: () => []
    },
    analysisCatchList: {
      type: Map,
      default: () => new Map()
    },
    setAnalysisCatchList: {
      type: Function,
      default: () => {}
    }
  },
  data() {
    return {
      visible:false,
      loading: false,
      btnLoading: false,
      basisData: [],
      analysisKey: 1,
      modelType: 0,
      basisColumns: [
        { label: '类别', prop: 'compareType', width: 140 },
        { label: '对比内容', prop: 'content', width: 220 },
      ],
      analysisColumns: [],
      analysisSelects: undefined // 当前选中的 站场和管段队列
    };
  },
  watch:{
  },
  mounted() {
  },
  async created() {
    // 建设方案场景单独处理
    this.modelType = Number(this.row[0].modelType)
    if(this.modelType === 3){
      this.initAnalysisReuslt()
    }else{
      this.getBasicResult()
    }
  },
  methods: {
    scrollHandler(){
      if(this.modelType !== 3) return
      // 滚动的时候 隐藏下拉框, 防止下拉框不跟随滚动
      const { mySelect1 = [], mySelect2 = [] } = this.$refs.AnalysisCompare.$refs || {}
      // if(mySelect1.length !== 0){
      //   mySelect1.map((item)=>{item?.blur()})
      // }
      if(mySelect1.length !== 0){
        mySelect1.map((item)=>{
          item.dropDownVisible = false;
        })
      }
      if(mySelect2.length !== 0){
        mySelect2.map((item)=>{
          item.dropDownVisible = false;
        })
      }
    },  
    async exportHandler(){
      this.warningTip()
      this.btnLoading = true
      let result = {}
      if(this.modelType === 3){
        // 建设方案
        result = await exportModelContrastAnalsisApi({
          modelIds: this.row.map(item=>item.modelId),
          modelSelects: this.analysisSelects
        })
      }else{
        result = await exportModelContrastApi({
          modelId: this.row[0].modelId,
          contrastId: this.row[1].modelId
        })
      }
      if(result?.status === 200){
        downloadFile(result.data, '结果对比数据')
      }
      this.btnLoading = false
    },
    // 子组件触发父组件更新, 子组件下拉框选择以后从新拉取数据
    async getAnalysisReuslt(res = undefined){
      if(res){
        // 将选中项加入缓存
        this.setAnalysisCatchList(res)
        // 从选中的站场队列当中, 拿出当前需要更新的这一列模型的数据, 从新赋值选中项
        const obj = this.analysisSelects.find(item=>item.modelId === res.modelId)
        Object.assign(obj, res)
        const { selectSegmentCodes, selectStationCodes} = obj 
        const arr1 = selectSegmentCodes || []
        const arr2 = selectStationCodes || []
        if(arr1.length === 0 && arr2.length === 0){
          this.$message({
            type: 'warning',
            message: '请至少选择一个站场或管段'
          })
          return
        }
      }
      this.loading = true
      // 结果对比的时候 还是把全部的模型都传过去
      const result = await modelContrastApi({
        modelIds: this.row.map(item=>item.modelId),
        modelSelects: this.analysisSelects || []
      })
      this.loading = false
      const { data, code  } = result
      if(code === 200){
        this.basisData = data
        this.analysisColumns = [...this.basisColumns].concat(data.map(item=>{
          const { modelName,modelId }  =  item
          return {
            label: modelName,
            modelId: modelId,
            prop: modelName,
          }
        }))
        this.analysisKey++
      }
    },
    async initAnalysisReuslt(){
      console.log('this.',this.analysisCatchList)
      // 如果 缓存当中 analysisCatchList 的值都是 null 的话, 则默认选择全部, 不需要携带 modelSelects 参数, 如果有的话 则 1 是携带参数 2 是同步子组件中 下拉框的选中项
      this.loading = true
      const result = await modelContrastApi({
        modelIds: this.row.map(item=>item.modelId)
      })
      this.loading = false
      const { data, code  } = testData
      if(code === 200){
        this.basisData = data
        // 初始化选中项, 存储的队列中没有的话, 则默认选择全部
        // this.basisData[0].basIndicators.segments = [{key:'ABC', value:'测试'}]
        // this.basisData[0].basIndicators.selectSegmentCodes = ['ABC']
        this.analysisSelects = data.map(item=>{
          const { modelId, basIndicators: {selectSegmentCodes, selectStationCodes} } = item
          return {
            modelId,
            selectSegmentCodes,
            selectStationCodes
          }
        })
        this.analysisColumns = [...this.basisColumns].concat(data.map(item=>{
          const { modelName,modelId }  =  item
          return {
            label: modelName,
            modelId: modelId,
            prop: modelName,
          }
        }))
      }
    },
    async getBasicResult(){
      this.loading = true
      // 请求拿到数据以后, 处理表头 modelContrastApi
      const result = await modelContrastApi({
        modelId: this.row[0].modelId,
        contrastId: this.row[1].modelId
      })
      this.loading = false
      const { code , data } = result
      if(code === 200){
        this.basisData = data
        this.basisColumns = this.basisColumns.concat(data.map(item=>{
          const { modelName,modelId }  =  item
          return {
            label: modelName,
            modelId: modelId,
            prop: modelName,
          }
        }))
      }else{
        this.basisColumns = []
      }
    }
  },
};
</script>

<style lang="scss" scoped>
.drawer-container{
  max-height: calc( 100vh - 120px );
  box-sizing: border-box;
  overflow-y: auto;
  background-color: #FFF;
  padding: 0px 20px 20px 20px;
}
.demo-drawer__footer{
  height: 64px;
  border-top: 1px solid #E8E8E8;
  box-sizing: border-box;
  padding-right: 20px;
  padding-top: 16px;
  display: inline-flex;
  justify-content: end;
  position: absolute;
  width: 100%;
  bottom: 0;
}
.form-container{
  width:100%;
  padding: 0 20px;
  box-sizing: border-box;
}
::v-deep .el-drawer__header{
  height:56px;
  margin-bottom: 0;
  padding: 12px 20px;
  font-size: 16px;
  border-bottom: 1px solid #E8E8E8;
  box-sizing: border-box;
  font-style: normal;
  font-weight: 500;
  line-height: 22px;
}
::v-deep .el-form--label-top .el-form-item__label{
  padding: 0
}
::v-deep  input::-webkit-outer-spin-button,
::v-deep  input::-webkit-inner-spin-button {
        -webkit-appearance: none;
}
::v-deep  input[type="number"]{
    -moz-appearance: textfield;
}
::v-deep button{
  height:32px
}
</style>
相关推荐
2401_878454532 小时前
Themeleaf复用功能
前端·学习
葡萄城技术团队3 小时前
基于前端技术的QR码API开发实战:从原理到部署
前端
八了个戒5 小时前
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
开发语言·前端·javascript·数据可视化
noravinsc5 小时前
html页面打开后中文乱码
前端·html
胚芽鞘6816 小时前
vue + element-plus自定义表单验证(修改密码业务)
javascript·vue.js·elementui
小满zs6 小时前
React-router v7 第四章(路由传参)
前端·react.js
小陈同学呦6 小时前
聊聊双列瀑布流
前端·javascript·面试
键指江湖7 小时前
React 在组件间共享状态
前端·javascript·react.js
诸葛亮的芭蕉扇7 小时前
D3路网图技术文档
前端·javascript·vue.js·microsoft
小离a_a7 小时前
小程序css实现容器内 数据滚动 无缝衔接 点击暂停
前端·css·小程序