这个需求是根据点击左侧的箭头部分,右侧图表切换,左侧选中数据高亮(图片用的svg)
一、效果图
二、vue组件
<template><div class="funnel_wrap"><div class="flex_between"><div class="sec_title">测试</div><!-- <el-checkbox label="平均值" v-model="averageCheck" @change="changeAverage"></el-checkbox> --></div><div class="flex_center funnel_con" v-if="flowList.items&&flowList.items.length>0"><div class="flow_con"><div class="left_position pointer"><svg-icon icon-class="arrow" :style="`color:${chooseId==5||chooseId==6||chooseId==7?'green':'#DFE1EB'};position:absolute;right:0;bottom:-3px;width: 6px;height: 6px;`"></svg-icon><div @click="changeFunnel(7)" :class="['left_top',chooseId==7?'leftactive':'']"><div class="left_title">{{radioList[6]}}%</div></div><div @click="changeFunnel(6)" :class="['left_cen',chooseId==6?'leftactive':'']" :style="`${chooseId==7?'border-bottom:0':''}`"><div class="left_title">{{radioList[5]}}%</div></div><div @click="changeFunnel(5)" :class="['left_bot',chooseId==5?'leftactive':'']" :style="`${chooseId==7||chooseId==6?'border-bottom:0':''}`"><div class="left_title">{{radioList[4]}}%</div></div></div><div :class="['flow_item',chooseList.includes(item.id)?'active':''] " :style="'width:'+(255-(12*index))+'px'" v-for="(item,index) in flowList.items" :key="index"><div class="item_lef">{{item.title}}</div><div class="item_rig" >{{item.newValue}}<div class="funnle"></div></div><div class="svg_box" v-if="index!==4" :style="`color:${chooseId==index?'green':'#DFE1EB'};right:-${(15+12*index)}px`" ><svg-icon @click="changeFunnel(index)" class="pointer" :style="'height:42px;width:'+(26+index*12)+'px'" :icon-class="'funnel'+index"></svg-icon><div class="title" :style="`left:${(30+index*12)}px;color:${chooseId==index?'green':'#212848'}`">{{radioList[index]}}%</div></div></div></div><div class="flow_echart"><line-vue v-if="lineOpt.id" :opt="lineOpt" :heightNum="300"></line-vue></div></div><div v-else class="none">暂无数据</div></div>
</template><script>
import { defineComponent, onMounted, computed,reactive,ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { roomflow } from "@/api/analyze/index.js";
import lineVue from "@/components/echartsChange/lineVue.vue";
import { thousandthis, valueTransfer,processingData,$formatTime,millionTransfer } from "@/utils/utils";
export default defineComponent({components:{lineVue},props: {opt: {type: Object,default: () => {return {roomId:'',userId:''};},},optIds: {type: Object,default: () => {return {lineId: "funnel_data_id", // echarts图表默认id 同一个页面多次引用当前组件 id不能相同};},},},setup(props,context) {const router = useRouter(),route = useRoute()let averageCheck=ref(false)//平均值flagconst industryAvg=ref('')//平均值const formatTime=$formatTimeconst lineOpt = ref({});const chooseList=ref([0,1])//选中idconst chooseListArr=ref([])//选中趋势图const flowList=ref([])//漏斗列表const trendList=ref([])//曲线图列表const chooseId=ref('0')let radioList=ref([])//占比let colors = ["#4CAF50","#556FFD","#91CC75","#EA8533","#283E81","#097C38","#48D9D9","#93BEFF",];let renderColors = colors;const initData = (res,arrline) => {let data = res;if (data && data.length > 0) {let xList = [];let seriesList = [];let maxArr=[]data.forEach((element, index) => {element.points=element.points||[]maxArr.push(element.points.length)let arrnew=[]// if(element.points.length>0){arrnew = element.points.map((obj) => {return obj.value;}).join(",").split(",");// } seriesList.push({name: element.title,type: "line",showSymbol: false,symbolSize: 6,seriesLayoutBy: "row",emphasis: { focus: "series" },data: [...arrnew],markLine : {symbol: ['none'],data : arrline?arrline:[],emphasis: {lineStyle: {width: 1, // hover时的折线宽度}},},lineStyle: {width: 1,}, });});let max=Math.max(...maxArr)let maxIndex=maxArr.map(item => item).indexOf(max)xList = data[maxIndex].points.map((item) => {return formatTime(item.date,'HH:mm');});lineOpt.value = {id: props.optIds.lineId,resize:true,options: {color: renderColors,title: {text: "",},legend: {icon: "circle",// selectedMode:'single',itemHeight: 6,itemWidth: 6,left: "0px",itemGap: 24,// top:'bottom',textStyle: {//图例文字的样式color: "#596076",fontSize: 14,padding: [0, 0, 0, 0], //文字与图形之间的左右间距},// data: ["签约汇总", "计划招募", "计划孵化"],},tooltip: {// 鼠标移入的展示trigger: "axis",// axisPointer: {// type: "cross",// label: {// backgroundColor: "#6a7985",// },// },formatter: function (params) {let res = params[0].name+'分析数据\n';for (let i = 0; i < params.length; i++) {res += `<div style="margin-top: 4px;font-size: 14px;line-height: 22px;color: #596076;">${params[i].marker} ${params[i].seriesName}:${thousandthis(params[i].data)}</div>`;}return res;},backgroundColor: "rgba(255,255,255,.9)",borderColor: "#E2E6F5",borderWidth: 1,padding: [12, 16, 16, 16],},grid: {// 图表距离容器的距离left: "1%",right: '4%',bottom: "3%",top:'22%',containLabel: true, // 是否显示刻度,默认不显示},xAxis: [{type: "category",boundaryGap: false,axisLabel: {color: "#9095A7",fontSize: 12,margin: 13,},axisLine: {lineStyle: {color: "#DFE1EB",},},axisTick: {show: false,},data: xList,},],yAxis: [{type: "value",// min: 0,// max: function (value) {// return value.max < 400 ? 400 : value.max;// },// interval: 1000,// splitNumber: 4,axisLabel: {color: "#9095A7",formatter(v) {return valueTransfer(Math.abs(v), 0, "w", true);},},splitLine: {lineStyle: {type: "dashed", //虚线},},},],series: seriesList,},};}};//格式化选中线条const initChoosrArr=(arrList)=>{let arr=[]chooseList.value.forEach((ele)=>{arr.push(arrList[ele])})return arr}//获取漏斗列表const getList = () => {let param = {userId:props.opt.userId,roomId: props.opt.roomId,};// roomflow(param).then((res) => { let res= {"code": 200,"msg": "ok","data": {"flowRank": {"name": "flowRank","title": "测试数据","items": [{"title": "数据1","ratio": 1.0,"value": 2833543},{"title": "数据2","ratio": 0.12883587790974055,"value": 365062},{"title": "数据3","ratio": 0.85563822035709,"value": 312361},{"title": "数据4","ratio": 0.09972755881816232,"value": 31151},{"title": "数据5","ratio": 0.016532374562614364,"value": 515}],},"trends": [{"title": "数据1","points": [{"value": 30000,"date": "2023-07-02 09:25:00"},{"value": 35000,"date": "2023-07-02 09:30:00"},{"value": 50000,"date": "2023-07-02 09:35:00"},{"value": 100000,"date": "2023-07-02 09:40:00"},{"value": 130003,"date": "2023-07-02 09:45:00"},{"value": 190000,"date": "2023-07-02 09:50:00"},{"value": 230000,"date": "2023-07-02 09:55:00"},{"value": 250000,"date": "2023-07-02 10:00:00"},]},{"title": "数据2","points": [{"value": 6000,"date": "2023-07-02 09:25:00"},{"value": 7000,"date": "2023-07-02 09:30:00"},{"value": 8000,"date": "2023-07-02 09:35:00"},{"value": 9000,"date": "2023-07-02 09:40:00"},{"value": 10000,"date": "2023-07-02 09:45:00"},{"value": 11000,"date": "2023-07-02 09:50:00"},{"value": 12000,"date": "2023-07-02 09:55:00"},{"value": 21810,"date": "2023-07-02 10:00:00"},]},{"title": "数据3","points": [{"value": 4500,"date": "2023-07-02 09:25:00"},{"value": 4700,"date": "2023-07-02 09:30:00"},{"value": 10000,"date": "2023-07-02 09:35:00"},{"value": 10214,"date": "2023-07-02 09:40:00"},{"value": 12000,"date": "2023-07-02 09:45:00"},{"value": 13000,"date": "2023-07-02 09:50:00"},{"value": 14000,"date": "2023-07-02 09:55:00"},{"value": 15000,"date": "2023-07-02 10:00:00"},]},{"title": "数据4","points": [{"value": 400,"date": "2023-07-02 09:25:00"},{"value": 800,"date": "2023-07-02 09:30:00"},{"value": 1100,"date": "2023-07-02 09:35:00"},{"value": 1200,"date": "2023-07-02 09:40:00"},{"value": 1400,"date": "2023-07-02 09:45:00"},{"value": 1600,"date": "2023-07-02 09:50:00"},{"value": 1800,"date": "2023-07-02 09:55:00"},{"value": 2000,"date": "2023-07-02 10:00:00"},]},{"title": "数据5","points": [{"value": 0,"date": "2023-07-02 09:25:00"},{"value": 2,"date": "2023-07-02 09:30:00"},{"value": 13,"date": "2023-07-02 09:35:00"},{"value": 14,"date": "2023-07-02 09:40:00"},{"value": 34,"date": "2023-07-02 09:45:00"},{"value": 40,"date": "2023-07-02 09:50:00"},{"value": 53,"date": "2023-07-02 09:55:00"},{"value": 63,"date": "2023-07-02 10:00:00"},]}],"industryAvg": 100}
}if (res.data) {if(res.data.flowRank.items){radioList.value=[]//格式漏斗右侧返回占比res.data.flowRank.items.forEach((ele,index) => {ele.id=indexele.newValue=millionTransfer(ele.value)if(index!==0){radioList.value.push(processingData(ele.ratio*100,2))}});// 漏斗左侧百分比计算radioList.value.push(processingData((res.data.flowRank.items[4].value/res.data.flowRank.items[2].value)*100,2))radioList.value.push(processingData((res.data.flowRank.items[4].value/res.data.flowRank.items[1].value)*100,2)) radioList.value.push(processingData((res.data.flowRank.items[4].value/res.data.flowRank.items[0].value)*100,2))}//绘制图表if(res.data.trends){chooseListArr.value=initChoosrArr(res.data.trends)initData(chooseListArr.value)}flowList.value=res.data.flowRanktrendList.value=res.data.trendsindustryAvg.value=res.data.industryAvg}// });};//点击漏斗const changeFunnel=(val)=>{chooseId.value=val;if(val<5){chooseList.value=[val,val+1]}else if(val==5){chooseList.value=[2,4]}if(val==6){chooseList.value=[1,4]}if(val==7){chooseList.value=[0,4]}chooseListArr.value=initChoosrArr(trendList.value)// 先判断是否有平均线再重绘图表changeAverage(averageCheck.value)}//点击平均值 val=true有平均线const changeAverage=(val)=>{if(val){let arrline=[{symbol: "none",silent:false, //鼠标悬停事件 true没有,false有lineStyle:{ //警戒线的样式 ,虚实 颜色type:"dashed", //样式 ‘solid’和'dotted'color:"#E98433",width: 1 //宽度},label:{show:false,color:"#E98433",position:'middle',// padding: ['0', '0', '0',tableWidth.value],formatter: function (params) {let res = "";res += `${params.name}:${params.value}`;return res;},},name:'平均值',yAxis:industryAvg.value}]initData(chooseListArr.value,arrline)}else{initData(chooseListArr.value)}}watch(props,(newValue) => {console.log(newValue);if (newValue && newValue.opt && newValue.opt.roomId) {getList()}},{ deep: true });onMounted(()=>{// getList()})return {flowList,chooseList,changeFunnel,chooseId,radioList,trendList,lineOpt,averageCheck,changeAverage}}
})
</script><style scoped lang="scss">
.funnel_wrap{margin-top: 24px;padding: 24px;color: #212848;font-size: 14px;background-color: #fff;.sec_title{font-size: 18px;font-weight: 500;}.funnel_con{padding: 24px;}.flow_con{position: relative;padding-left: 100px;padding-right: 112px;.left_position{position: absolute;top: 30px;left:0;.left_top{width: 100px;height: 198px;color:#DFE1EB;border:1px solid #DFE1EB;border-right:0;}.left_cen{width: 72px;height: 146px;position:absolute;left:24px;bottom:-0.5px;color:#DFE1EB;border:1px solid #DFE1EB;border-right:0;}.left_bot{width: 25px;height: 92px;position:absolute;left:72px;color:#DFE1EB;bottom:0px;border:1px solid #DFE1EB;border-right:0;}.left_title{position: absolute;line-height: 20px;top: -24px;right: 0; color: #212848;}.leftactive{border:1px solid green;border-right:0;color: green;.left_title{color: green;}}}.flow_item{background-color:#F8F9FB ;margin-bottom: 12px;height: 40px;line-height: 40px;display: flex;align-items: center;position: relative;.item_lef{width: 116px;text-align: center;box-sizing: border-box;}.item_rig{padding-left: 16px;position: relative;flex: 1;.funnle{position: absolute;border-bottom:40px solid #fff;border-left: 12px solid transparent;right: 0;top: 0;}}}.active{.item_lef{background-color: green;color: #fff;}.item_rig{background-color: #EEF1FF;}}.svg_box{position: absolute;top: 25px;right: -10px;.title{position: absolute;left:0;top: 0;}}}.flow_echart{flex: 1;}.none{margin-top: 12px;color: #9095A7;text-align: center;}
}
// .svg-icon {
// height: 3em;
// }
</style>
三、utils.js方法
export function millionTransfer(value,digits = 4,unit = "w",decimal = 2,removeZero = false
) {// unit = unit || "w"const valueNum = Number(value)const transferNum = Math.pow(10, digits)if (!isNaN(valueNum)) {if (valueNum < transferNum && valueNum >= 0) {return value}const num = floatDivideMethod(valueNum, transferNum)if (removeZero) {return `${parseFloat(num.toFixed(decimal))}${unit}`}return `${num.toFixed(decimal)}${unit}`}return value
}export function thousandthis(num) {if (!num && num !== 0) return nullif (num === '--') return '--'if (!(!isNaN(Number(num)) && typeof Number(num) === 'number')) {return '0'}return (num || 0).toString().replace(/\d+/, function(n) {const len = n.lengthif (len % 3 === 0) {return n.replace(/(\d{3})/g, ',$1').slice(1)}return n.slice(0, len % 3) + n.slice(len % 3).replace(/(\d{3})/g, ',$1')})
}/* 最早的数据没有亿,只有万,兼容之前数据,后面转换万和亿的数据用这个方法 */
export function valueTransfer(value, decimal = 2, unit = "万", removeZero = false) {let outputVal = valueconst valueNum = Number(value)const transferNum1 = Math.pow(10, 4)const transferNum2 = Math.pow(10, 8)if (!isNaN(valueNum)) {if (valueNum < transferNum1) {outputVal = value} else if (valueNum >= transferNum1 && valueNum < transferNum2) {outputVal = millionTransfer(value, 4, unit, decimal, removeZero)} else {outputVal = millionTransfer(value, 8, "亿", decimal, removeZero)}}return outputVal
}//保留两位小数
export function processingData(data,length){data=Number(data);data=Number((parseInt(data * 100) / 100).toFixed((length!=undefined?length:2)))data=data+''return data
}import moment from "moment"
export function $formatTime (time, format = "YYYY-MM-DD HH:mm:ss") {if (time && time !== "--") {if (format === "timestamp") {return Number(moment(time).utcOffset(8).format("x"))}return moment(time).format(format)}return time
}
四、父组件调用
import flowFunnel from "./components/flowFunnel.vue";components:{flowFunnel,},