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>
相关推荐
月巴月巴白勺合鸟月半23 分钟前
工作记录 2017-02-03
前端·c#·健康医疗
伟笑43 分钟前
npm 报错 unable to resolve dependency tree
前端·npm·node.js
beibeibeiooo1 小时前
【ES6】04-对象 + 类 + 模板字符串 + 解构 + 字符串
前端·javascript·es6
imkaifan1 小时前
7、vue3做了什么
javascript·vue.js·ecmascript
冴羽1 小时前
SvelteKit 最新中文文档教程(6)—— 状态管理
前端·javascript·svelte
徐小黑ACG1 小时前
个人blog系统 前后端分离 前端js后端go
开发语言·前端·javascript·vue.js·golang
拉不动的猪2 小时前
刷刷题39(同一组件中的不同的标签页如何实现通信)
前端·javascript·面试
拉不动的猪2 小时前
刷刷题37(vue3的优化点)
前端·javascript·面试
家里有只小肥猫3 小时前
关于新奇的css
前端·css
南雨北斗3 小时前
jquery ajax 返回TP6错误信息的调试方法
前端·后端