这个需求是根据点击左侧的箭头部分,右侧图表切换,左侧选中数据高亮(图片用的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)//平均值flag
const industryAvg=ref('')//平均值
const formatTime=$formatTime
const lineOpt = ref({});
const chooseList=ref([0,1])//选中id
const 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=index
ele.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.flowRank
trendList.value=res.data.trends
industryAvg.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 null
if (num === '--') return '--'
if (!(!isNaN(Number(num)) && typeof Number(num) === 'number')) {
return '0'
}
return (num || 0).toString().replace(/\d+/, function(n) {
const len = n.length
if (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 = value
const 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,
},