在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>