vue3 项目中 使用 leader-line-vue 绘制指引线
项目需要点击菜单并且使用联动展示
链接: 参考
部分预览:
1、安装:npm i leader-line-vue
2、页面 script 中引入:import LeaderLine from ‘leader-line-vue’;
3、创建页面元素
<div class="left">
<el-menu :default-active="activeJob" class="job-menu" :collapse="false">
<el-menu-item v-for="job in jobs" :key="job.id" :index="job.id" @click="handleSelect(job)">
<div :id="`job-${job.id}`" class="job-menu-item-inner">
{{ job.name }}
</div>
</el-menu-item>
</el-menu>
</div>
<div class="right overflow-y-auto">
<el-card v-for="ability in abilities" :key="ability.id" :id="`ability-${ability.id}`"
@click="handleAbilityClick(ability.id)" class="ability-card">
{{ ability.name }}
</el-card>
</div>
4、获取页面元素添加划线(使用 LeaderLine.setLine !!!)
// 绘制线条
function drawLines() {
// 清除线条
clearLines()
// 等待DOM更新后执行
nextTick(() => {
// 获取当前职业和技能的DOM元素
const jobEl = document.getElementById(`job-${activeJob.value}`)
const abilityEl = document.getElementById(`ability-${activeAbility.value}`)
// 如果DOM元素存在且LeaderLine存在且LeaderLine.setLine方法存在
if (jobEl && abilityEl && LeaderLine && LeaderLine.setLine) {
// 使用LeaderLine.setLine方法绘制线条
const line = LeaderLine.setLine(
jobEl,
abilityEl,
{
// 线条颜色
color: '#2196f3',
// 线条宽度
size: 1.5,
// 线条路径
path: 'fluid',
// 起始端点样式
startPlug: 'disc',
// 结束端点样式
endPlug: 'arrow3',
// 是否使用渐变
gradient: true,
// 是否使用阴影
dropShadow: true,
// 是否使用虚线
dash: { animation: true }
}
)
// 将线条添加到lines数组中
lines.push(line)
}
})
}
5、每次点击需要清空连线的对象
// 连线对象
let lines: any[] = []
function clearLines() {
lines.forEach(line => line.remove())
lines = []
}
特别注意!!!
a、不要和 leader-line 弄混,leader-line 是直接使用构造函数,可以直接使用new LeaderLine(startElement, endElement, {color: ‘red’, size: 8});
b、leader-line-vue 使用 LeaderLine.setLine(jobEl, abilityEl, { …options })
页面完整代码
<template>
<div>
<div class="ability-map">
<div class="left">
<el-menu :default-active="activeJob" class="job-menu" :collapse="false">
<el-menu-item v-for="job in jobs" :key="job.id" :index="job.id" @click="handleSelect(job)">
<div :id="`job-${job.id}`" class="job-menu-item-inner">
{{ job.name }}
</div>
</el-menu-item>
</el-menu>
</div>
<div class="right overflow-y-auto">
<el-card v-for="ability in abilities" :key="ability.id" :id="`ability-${ability.id}`"
@click="handleAbilityClick(ability.id)" class="ability-card">
{{ ability.name }}
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { ElMenu, ElMenuItem, ElCard } from 'element-plus'
import 'element-plus/dist/index.css'
import LeaderLine from 'leader-line-vue'
// 岗位数据
const jobs = [
{ id: 'job2', name: '第一个', abilities: ['b1', 'b2', 'b3'] },
{ id: 'job3', name: '第二个', abilities: ['c1', 'c2', 'c3'] },
{ id: 'job4', name: '第三个', abilities: ['d1', 'd2', 'd3'] },
{ id: 'job5', name: '第四个', abilities: ['e1', 'e2', 'e3'] },
]
// 能力项数据
const allAbilities = [
{ id: 'b1', name: '1.1第一个' },
{ id: 'b2', name: '2.1第一个' },
{ id: 'b3', name: '3.1第一个' },
{ id: 'c1', name: '1.1第二个' },
{ id: 'c2', name: '2.1第二个' },
{ id: 'c3', name: '3.1第二个' },
{ id: 'd1', name: '1.1第三个' },
{ id: 'd2', name: '2.1第三个' },
{ id: 'd3', name: '3.1第三个' },
{ id: 'e1', name: '1.1第四个' },
{ id: 'e2', name: '2.1第四个' },
{ id: 'e3', name: '3.1第四个' },
]
// 当前选中的岗位
const activeJob = ref(jobs[0].id)
const selectedAbilities = computed(() => {
return jobs.find(j => j.id === activeJob.value)?.abilities ?? []
})
const abilities = computed(() => {
return allAbilities.filter(a => selectedAbilities.value.includes(a.id))
})
// 连线对象
let lines: any[] = []
function clearLines() {
lines.forEach(line => line.remove())
lines = []
}
const activeAbility = ref<string | null>('b1')
// 能力项点击事件
function handleAbilityClick(id: string) {
activeAbility.value = id
drawLines()
}
// 绘制线条
function drawLines() {
// 清除线条
clearLines()
// 等待DOM更新后执行
nextTick(() => {
// 获取当前职业和技能的DOM元素
const jobEl = document.getElementById(`job-${activeJob.value}`)
const abilityEl = document.getElementById(`ability-${activeAbility.value}`)
// 如果DOM元素存在且LeaderLine存在且LeaderLine.setLine方法存在
if (jobEl && abilityEl && LeaderLine && LeaderLine.setLine) {
// 使用LeaderLine.setLine方法绘制线条
const line = LeaderLine.setLine(
jobEl,
abilityEl,
{
// 线条颜色
color: '#2196f3',
// 线条宽度
size: 1.5,
// 线条路径
path: 'fluid',
// 起始端点样式
startPlug: 'disc',
// 结束端点样式
endPlug: 'arrow3',
// 是否使用渐变
gradient: true,
// 是否使用阴影
dropShadow: true,
// 是否使用虚线
dash: { animation: true }
}
)
// 将线条添加到lines数组中
lines.push(line)
}
})
}
// 岗位切换
function handleSelect(val: any) {
activeJob.value = val.id
activeAbility.value = val.abilities[0]
}
// 监听岗位切换和窗口变化
watch([activeJob, abilities], () => {
drawLines()
})
let scrollBox: HTMLElement | null = null
onMounted(async () => {
drawLines()
window.addEventListener('resize', drawLines)
// 获取右侧滚动容器
nextTick(() => {
scrollBox = document.querySelector('.right')
if (scrollBox) {
scrollBox.addEventListener('scroll', handleScroll, false)
}
})
})
onBeforeUnmount(() => {
clearLines()
window.removeEventListener('resize', drawLines)
if (scrollBox) {
scrollBox.removeEventListener('scroll', handleScroll)
}
})
function handleScroll() {
// 重新定位所有连线
lines.forEach(line => line.position())
}
</script>
<style scoped>
.ability-map {
display: flex;
width: 100%;
height: 700px;
background: radial-gradient(ellipse at 60% 40%, #11204a 60%, #061b3a 100%);
border-radius: 24px;
padding: 32px;
box-shadow: 0 0 40px #0a1a3a inset;
position: relative;
overflow: hidden;
}
.left {
width: 240px;
margin-right: 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.job-menu {
background: transparent;
color: #fff;
border: none;
width: 100%;
}
.job-menu-item-inner {
width: 100%;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 18px;
background: linear-gradient(90deg, #1a2a4f 80%, #2b3e6b 100%);
color: #b3d1ff;
font-size: 20px;
font-weight: bold;
box-shadow: 0 0 12px #1e2e5a;
transition: background 0.3s, color 0.3s, box-shadow 0.3s;
}
.el-menu-item.is-active .job-menu-item-inner,
.el-menu-item:hover .job-menu-item-inner {
background: linear-gradient(90deg, #2b5cff 60%, #1a2a4f 100%);
color: #fff;
border: 2px solid #00ffcc;
box-shadow: 0 0 24px #00ffcc, 0 0 32px #1e2e5a inset;
}
.right {
width: 340px;
display: flex;
flex-direction: column;
gap: 18px;
}
.ability-card {
background: linear-gradient(90deg, #173b6c 60%, #1a237e 100%);
color: #fff;
border-radius: 18px;
font-size: 18px;
font-weight: bold;
margin-bottom: 0;
box-shadow: 0 0 16px #1e2e5a, 0 0 32px #0a1a3a inset;
transition: background 0.2s, border 0.2s, box-shadow 0.2s;
cursor: pointer;
border: 2px solid transparent;
min-height: 48px;
display: flex;
align-items: center;
padding-left: 32px;
}
.ability-card:hover {
border: 2px solid #00ffcc;
background: linear-gradient(90deg, #1e88e5 60%, #283593 100%);
color: #fff;
}
</style>