需求描述
在vue项目中实现由自定义块元素组成的连线图。
效果图
实现思路
Leader-Line 是一个用于 Web 的轻量级 JavaScript 库,专为创建从一个元素指向另一个元素的引导线而设计。它提供了高度自定义的能力,使得开发者能够轻松地在网页上实现各种指引用例,如表单验证指示、界面导航辅助等。该库支持现代浏览器,且不依赖任何大型框架,保证了其灵活性和性能。
利用Leader-Line组件可以实现把不同dom元素进行连接,且连线及箭头可以灵活自定义。
具体步骤
1、环境版本:
Node版本:16.20.2
Leader-Line版本: 1.0.8
2、使用npm或yarn安装leader-line依赖包
npm install leader-line 或 yarn add leader-line
3、找到装好的依赖包,把leader-line.min.js文件复制到自己的拓展工具类函数文件夹下面
例如放置路径:src\plugins\leader-line.min.js
4、在使用的地方引入leader-line.min.js文件
import LeaderLine from "@/plugins/leader-line.min.js";
5、创建连线
完整代码
<!-- 经营全景看板-产业链经营全景 -->
<template>
<div class="chain-chart" :class="theme">
<TabHeader title="产业链经营全景">
<template #leftBtn>
<div class="right-select">
<template>
<el-select
class="common-simple-select"
v-model="searchForm.keyProduct"
placeholder="选择重点产品"
:popper-append-to-body="true"
popper-class="common-search-popper"
clearable
>
<el-option
v-for="item in keyProductOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select>
</template>
</div>
</template>
<div class="content-container">
<!-- 销售计划 -->
<div ref="box1" class="box sales-plan-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">计划执行</div>
<div class="title">销售计划</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>本年计划</div>
<div class="text-box">
<span class="blue-text">5400</span
><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>较去年实际</div>
<div class="arrow">
<img src="@/assets/icons/up_arrow.png" alt="" />
<img src="@/assets/icons/down_arrow.png" alt="" v-if="false" />
3.24%
</div>
</div>
<div class="stat-row">
<div>上期计划准确率</div>
<div class="arrow">
<img src="@/assets/icons/up_arrow.png" alt="" />
<img src="@/assets/icons/down_arrow.png" alt="" v-if="false" />
84.22%
</div>
</div>
</div>
<div class="switch-btns">
<span :class="{ active: switchBtnState.salesPlan === 'quantity' }" @click="toggleSwitchBtn('salesPlan', 'quantity')">量</span>
<span :class="{ active: switchBtnState.salesPlan === 'amount' }" @click="toggleSwitchBtn('salesPlan', 'amount')">额</span>
</div>
</div>
<!-- 销售目标 -->
<div ref="box2" class="box sales-target-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">计划执行</div>
<div class="title">销售目标</div>
<div class="sub-title" @click="toPath('/strategeAnalysis/pages/842?catalogId=8')">专题分析</div>
</div>
<div class="stats">
<div class="stat-row">
<div>本年达成率</div>
<div class="text-box">
<span class="red-text">75%</span>
<!-- <img
src="@/assets/icons/tips_icon.png"
alt=""
class="tips-icon"
/> -->
<i class="el-icon-warning tips-icon"></i>
</div>
</div>
<div class="stat-row">
<div>本年实际量</div>
<div class="text-box">
<span class="blue-text">5400</span
><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>同比</div>
<div class="arrow">
<img src="@/assets/icons/up_arrow.png" alt="" v-if="false" />
<img src="@/assets/icons/down_arrow.png" alt="" />
4.22%
</div>
</div>
</div>
<div class="switch-btns">
<span :class="{ active: switchBtnState.salesTarget === 'quantity' }" @click="toggleSwitchBtn('salesTarget', 'quantity')">量</span>
<span :class="{ active: switchBtnState.salesTarget === 'amount' }" @click="toggleSwitchBtn('salesTarget', 'amount')">额</span>
</div>
</div>
<!-- 供应商履约 -->
<div ref="box3" class="box supplier-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">计划执行</div>
<div class="title">供应商履约</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>准时交付率</div>
<div class="text-box">
<span class="blue-text">85%</span>
</div>
</div>
<div class="stat-row">
<div>质量合格率</div>
<div class="text-box">
<span class="blue-text">90%</span>
</div>
</div>
<div class="stat-row">
<div>原料总用量</div>
<div class="text-box">
<span>5400</span><span class="unit">万只</span>
</div>
</div>
</div>
</div>
<!-- 协议签订 -->
<div ref="box4" class="box order-sign-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">计划执行</div>
<div class="title">协议签订</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>本年履约率</div>
<div class="text-box">
<span class="blue-text">75%</span>
</div>
</div>
<div class="stat-row">
<div>本年协议量</div>
<div class="text-box">
<span>4400</span><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>本年实际量</div>
<div class="text-box">
<span>3400</span><span class="unit">万只</span>
</div>
</div>
</div>
<div class="switch-btns">
<span :class="{ active: switchBtnState.orderSign === 'quantity' }" @click="toggleSwitchBtn('orderSign', 'quantity')">量</span>
<span :class="{ active: switchBtnState.orderSign === 'amount' }" @click="toggleSwitchBtn('orderSign', 'amount')">额</span>
</div>
</div>
<!-- 生产计划 -->
<div ref="box5" class="box prod-plan-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">计划执行</div>
<div class="title">生产计划</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>生产成本</div>
<div class="text-box">
<span class="blue-text">XX万元</span>
</div>
</div>
<div class="stat-row">
<div>生产计划量</div>
<div class="text-box">
<span>600</span><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>排产准确率</div>
<div class="text-box">
<span>100%</span>
</div>
</div>
</div>
</div>
<!-- 订单情况 -->
<div ref="box6" class="box order-detail-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">销售执行</div>
<div class="title">订单情况</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>订单满足率</div>
<div class="text-box">
<span class="blue-text">75%</span>
</div>
</div>
<div class="stat-row">
<div>紧急订单占比</div>
<div class="text-box">
<span class="red-text">40%</span>
</div>
</div>
<div class="stat-row">
<div>未满足订单量</div>
<div class="text-box">
<span>3400</span><span class="unit">万只</span>
</div>
</div>
</div>
</div>
<!-- 采购合同 -->
<div ref="box7" class="box purchase-contract-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">采购执行</div>
<div class="title">采购合同</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>生产采购成本</div>
<div class="text-box">
<span class="blue-text">XX万元</span>
</div>
</div>
<div class="stat-row">
<div>采购成本同比</div>
<div class="text-box">
<span>4.22%</span>
</div>
</div>
<div class="stat-row">
<div>采购成本环比</div>
<div class="text-box">
<span>2.31%</span>
</div>
</div>
</div>
</div>
<!-- 回款情况 -->
<div ref="box8" class="box collect-situation-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">销售执行</div>
<div class="title">回款情况</div>
<div class="sub-title" @click="toPath('/strategeAnalysis/pages/856?catalogId=25')">专题分析</div>
</div>
<div class="stats">
<div class="stat-row">
<div>应收账款逾期率</div>
<div class="text-box">
<span class="red-text">20%</span>
</div>
</div>
<div class="stat-row">
<div>回款周期</div>
<div class="text-box">
<span>85天</span>
</div>
</div>
<div class="stat-row">
<div>预期总金额</div>
<div class="text-box">
<span>5000万</span>
</div>
</div>
</div>
</div>
<!-- 发货情况 -->
<div ref="box9" class="box send-situation-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">物流执行</div>
<div class="title">发货情况</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>准时发货率</div>
<div class="text-box">
<span class="blue-text">90%</span>
</div>
</div>
<div class="stat-row">
<div>已发货总量</div>
<div class="text-box">
<span>11</span><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>已发货总金额</div>
<div class="text-box">
<span>500万</span>
</div>
</div>
</div>
</div>
<!-- 库存管理 -->
<div ref="box10" class="box inventory-manage-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">物流执行</div>
<div class="title">库存管理</div>
<div class="sub-title" @click="toPath('/strategeAnalysis/pages/863?catalogId=41')">专题分析</div>
</div>
<div class="stats">
<div class="stat-row">
<div>可用库存可销月</div>
<div class="text-box">
<span class="blue-text">3个月</span>
</div>
</div>
<div class="stat-row">
<div>库存周转天数</div>
<div class="text-box">
<span>80天</span>
</div>
</div>
<div class="stat-row">
<div>半年内近效期库存占比</div>
<div class="text-box">
<span class="red-text">10%</span>
</div>
</div>
</div>
</div>
<!-- 生产执行 -->
<div ref="box11" class="box prod-execution-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">生产执行</div>
<div class="title">生产执行</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>生产批次合格率</div>
<div class="text-box">
<span class="blue-text">100%</span>
</div>
</div>
<div class="stat-row">
<div>已生产总量</div>
<div class="text-box">
<span>10</span><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>紧急生产占比</div>
<div class="text-box">
<span>10%</span>
</div>
</div>
</div>
</div>
<!-- 商业库存 -->
<div ref="box12" class="box business-inventory-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">商业流通</div>
<div class="title">商业库存</div>
<div class="sub-title"></div>
</div>
<div class="stats">
<div class="stat-row">
<div>商业库存总量</div>
<div class="text-box">
<span class="blue-text">50</span><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>库存过低</div>
<div class="text-box">
<span class="red-text">16家</span>
</div>
</div>
<div class="stat-row">
<div>库存过高</div>
<div class="text-box">
<span>13家</span>
</div>
</div>
</div>
</div>
<!-- 终端覆盖 -->
<div ref="box13" class="box terminal-coverage-box common-tab-header-border">
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="header">
<div class="type">终端分析</div>
<div class="title">终端覆盖</div>
<div class="sub-title" @click="toPath()">专题分析</div>
</div>
<div class="stats">
<div class="stat-row">
<div>目标终端覆盖率</div>
<div class="text-box">
<span class="blue-text">75%</span>
</div>
</div>
<div class="stat-row">
<div>纯销总量</div>
<div class="text-box">
<span>10</span><span class="unit">万只</span>
</div>
</div>
<div class="stat-row">
<div>未开发终端总量</div>
<div class="text-box">
<span>XX家</span>
</div>
</div>
</div>
</div>
</div>
</TabHeader>
</div>
</template>
<script>
import TabHeader from "@/components/common_components/tabHeader.vue";
import LeaderLine from "@/plugins/leader-line.min.js";
import { Notification } from 'element-ui'
export default {
components: {
TabHeader,
},
props: {
width: {
type: Number,
default: null,
},
height: {
type: Number,
default: null,
},
theme: {
type: String,
default: "theme-dark",
},
},
data() {
return {
lines: [],
switchBtnState: {
salesPlan: 'quantity', // 销售计划
salesTarget: 'quantity', // 销售目标
orderSign: 'quantity', // 协议签订
},
keyProductOptions: [
{
label: "重点产品1",
value: "1",
},
{
label: "重点产品2",
value: "2",
},
{
label: "重点产品3",
value: "3",
},
],
searchForm: {
keyProduct: ''
}
};
},
computed: {},
mounted() {
// id为当前页id的时候才做初始化查询
if (this.$route.params.id == 865) {
this.$nextTick(() => {
this.initLines();
});
}
// 添加窗口大小变化的监听
window.addEventListener('resize', this.debounceResize);
},
beforeDestroy() {
// 组件销毁前清除所有连线
this.lines.forEach((line) => line.remove());
// 移除窗口大小变化的监听
window.removeEventListener('resize', this.debounceResize);
},
methods: {
toggleSwitchBtn(boxName, value) {
this.switchBtnState[boxName] = value;
},
// 防抖处理
debounceResize: function() {
if (this.resizeTimer) clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
this.handleResize();
}, 200);
},
// 处理窗口大小变化
handleResize() {
// 清除现有的连线
if (this.lines && this.lines.length > 0) {
this.lines.forEach((line) => {
if (line && typeof line.remove === 'function') {
line.remove();
}
});
this.lines = [];
}
// 重新初始化连线
this.$nextTick(() => {
this.initLines();
});
},
initLines() {
// 确保所有ref都存在
const refs = ['box1', 'box2', 'box3', 'box4', 'box5', 'box6', 'box7', 'box8', 'box9', 'box10', 'box11', 'box12', 'box13'];
const missingRefs = refs.filter(ref => !this.$refs[ref]);
if (missingRefs.length > 0) {
return;
}
try {
// 创建连线
// 销售计划-销售目标
const line1 = new LeaderLine(this.$refs.box1, this.$refs.box2, {
color: "#0e75ed", // 连线颜色
size: 1, // 连线粗细
path: "straight", // 连线类型
startSocket: "bottom", // 起点位置
endSocket: "top", // 终点位置
endPlug: 'behind' // 终点插件
});
// 销售计划-协议签订
const line2 = new LeaderLine(this.$refs.box1, this.$refs.box4, {
color: "#0e75ed",
size: 1,
path: "grid",
startSocket: "right",
endSocket: "top",
endPlug: 'behind'
});
// 销售计划-订单情况
const line3 = new LeaderLine(this.$refs.box1, this.$refs.box6, {
color: "#0e75ed",
size: 1,
path: "grid",
startSocket: "right",
endSocket: "top",
endPlug: 'behind'
});
// 订单情况-生产计划
const line4 = new LeaderLine(this.$refs.box6, this.$refs.box5, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "bottom",
endSocket: "top",
endPlug: 'behind'
});
// 生产计划-供应商履约
const line5 = new LeaderLine(this.$refs.box5, this.$refs.box3, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "left",
endSocket: "right",
endPlug: 'behind'
});
// 采购合同-生产计划
const line6 = new LeaderLine(this.$refs.box7, this.$refs.box5, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "left",
endSocket: "right",
endPlug: 'behind'
});
// 回款情况-商业库存
const line7 = new LeaderLine(this.$refs.box8, this.$refs.box12, {
color: "#0e75ed",
size: 1,
path: "grid",
startSocket: "top",
endSocket: "top",
endPlug: 'behind'
});
// 发货情况-商业库存
const line8 = new LeaderLine(this.$refs.box9, this.$refs.box12, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "right",
endSocket: "left",
endPlug: 'behind'
});
// 发货情况-库存管理
const line9 = new LeaderLine(this.$refs.box9, this.$refs.box10, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "bottom",
endSocket: "top",
endPlug: 'behind'
});
// 库存管理-生产执行
const line10 = new LeaderLine(this.$refs.box10, this.$refs.box11, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "bottom",
endSocket: "top",
endPlug: 'behind'
});
// 生产执行-采购合同
const line11 = new LeaderLine(this.$refs.box11, this.$refs.box7, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "left",
endSocket: "right",
endPlug: 'behind'
});
// 商业库存-终端覆盖
const line12 = new LeaderLine(this.$refs.box12, this.$refs.box13, {
color: "#0e75ed",
size: 1,
path: "straight",
startSocket: "bottom",
endSocket: "top",
endPlug: 'behind'
});
this.lines = [line1, line2, line3, line4, line5, line6, line7, line8, line9, line10, line11, line12];
} catch (error) {
console.error('Error initializing lines:', error);
}
},
// 路由跳转
toPath(url) {
if(!url) return Notification.warning({
title: '提示',
message: '功能开发中',
})
this.$router.push(url)
}
},
};
</script>
<style lang="scss">
@import "~@/assets/css/variables.scss";
.chain-chart {
height: 100%;
.content-container {
padding: vw(5) vw(10);
height: calc(100% - vw(10));
position: relative;
}
.box {
background: rgba(12, 51, 101, 0.6);
box-shadow: inset 0px 0px vw(25) 0px rgba(10, 77, 154, 0.6);
// border: 1px solid #0e75ed;
padding: vw(8);
width: vw(240);
color: #fff;
font-family: "Microsoft YaHei", Arial, sans-serif;
.header {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: space-between;
border-bottom: 1px solid #0e75ed;
padding-bottom: vw(5);
align-items: center;
.type {
font-size: vw(11);
border: 1px solid #0e75ed;
padding: vw(2) vw(8);
border-radius: vw(8);
}
.title {
font-size: vw(15);
font-weight: 600;
margin-right: vw(12);
}
.sub-title {
font-size: vw(11);
color: #b0c4de;
margin-right: vw(5);
min-width: vw(35);
cursor: pointer;
}
}
.stats {
margin-top: vw(5);
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: vw(13);
margin-bottom: vw(4);
.blue-text {
font-size: vw(16);
color: #1bb7c7;
font-weight: bold;
margin-right: vw(3);
}
.unit {
font-size: vw(12);
color: #fff;
margin-left: vw(2);
}
.red-text {
font-size: vw(16);
color: $defaultRedColor;
font-weight: bold;
margin-right: vw(3);
}
.text-box {
display: flex;
align-items: center;
}
.tips-icon {
font-size: vw(18);
color: $defaultYellowColor;
margin-left: vw(5);
}
.arrow img {
height: vw(12);
}
}
}
.switch-btns {
margin-top: vw(6);
display: flex;
gap: vw(8);
justify-content: flex-end;
span {
padding: vw(2) vw(10);
border-radius: vw(8);
border: 1px solid #0e75ed;
color: #1bb7c7;
cursor: pointer;
font-size: vw(12);
&.active {
background: #0e75ed;
color: #fff;
}
}
}
}
// 销售计划
.sales-plan-box {
position: absolute;
left: vw(10);
top: vw(5);
}
// 销售目标
.sales-target-box {
position: absolute;
left: vw(10);
top: 37%;
}
// 供应商履约
.supplier-box {
position: absolute;
left: vw(10);
top: 74.5%;
}
// 协议签订
.order-sign-box {
position: absolute;
left: 17.5%;
top: 37%;
}
// 生产计划
.prod-plan-box {
position: absolute;
left: 34%;
top: 75%;
}
// 订单情况
.order-detail-box {
position: absolute;
left: 34%;
top: 37%;
}
// 采购合同
.purchase-contract-box {
position: absolute;
left: 50.5%;
top: 75%;
}
// 回款情况
.collect-situation-box {
position: absolute;
left: 50.5%;
top: 37%;
}
// 发货情况
.send-situation-box {
position: absolute;
left: 67.5%;
top: 10%;
}
// 库存管理
.inventory-manage-box {
position: absolute;
left: 67.5%;
top: 42%;
}
// 生产执行
.prod-execution-box {
position: absolute;
left: 67.5%;
top: 75%;
}
// 商业库存
.business-inventory-box {
position: absolute;
right: vw(30);
top: 9.5%;
}
// 终端覆盖
.terminal-coverage-box {
position: absolute;
right: vw(30);
top: 42%;
}
}
</style>
注意
leader-line组件目前不支持直接引入依赖,需要下载对应leader-line.min.js文件,通过import文件才能正常使用。
报错排查方向
1、检查引入方式;
2、下载下来的leader-line.min.js该文件仅仅定义了一个LeaderLine 函数 ,并没有使用任何模块导出语句 (即没有module.export 或者 es6 export 导出语句)
针对第2个原因造成的报错,直接在leader-line.min.js后面增加导出语句即可