本篇文档主要记录自己对目前完成的2d模型图进行相关操作的心得,下面进行简单的阐述,不喜勿喷!
新建名为ModelChart2D.vue的全局公共组件,代码如下:
<!-- 2d 模型图组件 -->
<template>
<div class="w100 ofh pr flex-c pr">
<!-- 2d模型图 -->
<img :src="props.chartImg"
alt="2d模型图"
usemap="#modelBodyChart"
:width="props.imgWidth"
:height="props.imgHeight" />
<!-- 区域映射 -->
<!-- <map name="modelBodyChart">
<area v-for="(item,index) in props.chartArea"
:key="item.code"
target=""
class="area"
:alt="item.title"
:title="item.title"
@click="clickArea(item)"
href="javascript:void(0)"
:coords="item.coords"
:shape="item.shape" />
</map> -->
<!-- 使用svg绘制区域 -->
<svg :width="props.imgWidth"
:height="props.imgHeight"
class="svg-box"
:style="{'--top':`${props.offsetTop}px`}">
<!-- 不规则区域 -->
<polygon v-for="(item,index) in props.chartArea"
:key="item.code"
:id="`myPolygon${index}`"
:points="item.coords"
@click="clickArea(item)"
opacity="0.3"
:fill="fillColor(item,props.data)"
:class="{'polygon-active': !props.isShow}">
<title>{{item.title}}</title>
</polygon>
<!-- 绘制线段 -->
<line v-for="(item,index) in lineData"
:x1="item.startX"
:y1="item.startY"
:x2="item.endX"
:y2="item.endY"
:stroke="item.color"
stroke-width="1" />
<!-- 在线段另一端添加文字 -->
<text v-for="(item,index) in textData"
:x="item.x"
:y="item.y"
:class="{'blinking-text':item.isTip == 1}"
:text-anchor="item.textAnchor"
fill="black"
font-size="14"
font-family="Arial">
{{item.name}}
<title>{{item.name}}</title>
</text>
</svg>
<!-- svg定义不规则区域(点击某区域高亮填充) -->
<svg :width="props.imgWidth"
:height="props.imgHeight"
class="svg-box"
:style="{'--z-index':zIndex,'--top':`${props.offsetTop}px`}">
<polygon :points="highlightAreaPoints"
class="highlight-area" />
</svg>
<!-- 图例v-draggable -->
<div v-if="props.isShowLegend && props.data.length>0"
class="legend-box">
<div class="header">
<div class="item"
v-for="(item,index) in props.data"
:key="index"
:style="{'--bg-color': item.legendColor}">{{item.name}}({{item.value}})</div>
</div>
</div>
</div>
</template>
<script setup>
import { nextTick, onMounted, ref } from "vue";
import testImgUrl from "./test_chart.png" // 默认人体图像
const emit = defineEmits(['callback'])
const props = defineProps({
// 2d模型图宽度
imgWidth: {
type: Number,
default: 628,
},
// 2d模型图高度
imgHeight: {
type: Number,
default: 700,
},
// 2d模型图名称
chartArea: {
type: Array,
default: () => [
{
code: 'A',
title: '头',
shape: "poly",
coords: "176,70,186,68,195,69,204,80,206,90,206,101,204,107,203,119,201,126,197,131,190,135,177,139,169,138,164,133,160,122,159,104,159,89,165,76"
}
],
},
// 2d模型图图像
chartImg: {
type: String,
default: testImgUrl,
},
// 是否只需要展示
isShow: {
type: Boolean,
default: false,
},
//是否显示图例信息
isShowLegend: {
type: Boolean,
default: true,
},
// 图例数据--用于展示图例信息
data: {
type: Array,
default: () => [],
},
// 是否显示线段和文字信息
isShowLineText: {
type: Boolean,
default: true,
},
//向上偏移量(用绘制区域不规范)
offsetTop: {
type: Number,
default: 0
}
})
const highlightAreaPoints = ref("") // 高亮的区域
const zIndex = ref(-1) // 高亮区域的层级
// 取消高亮区域层级
const cancelHighlightZindex = () => {
zIndex.value = -1
}
// 填充的区域颜色
const fillColor = (item, data) => {
if (data.length > 0) {
let index = data.find(f => f?.catheterSiteList?.map((m) => m.code)?.includes(item.code))
if (index) {
return index.legendColor
} else {
return 'transparent'
}
} else {
return 'transparent'
}
}
// 点击区域回调函数
const clickArea = (data) => {
// console.log(`点击了=》${data.title}`, data);
if (!props.isShow) {
emit('callback', data)
highlightAreaPoints.value = data.coords
zIndex.value = 1
}
}
const lineData = ref([]) // 线段数据
const textData = ref([]) // 文字数据
// 线段和文字数据设置函数
const setData = (index, el, item) => {
nextTick(() => {
const polygonEle = document.getElementById(`myPolygon${index}`);
// 计算多边形的中心点
const bbox = polygonEle.getBBox();
const centerX = bbox.x + bbox.width / 2;
const centerY = bbox.y + bbox.height / 2;
// 定义线段的起点和终点
const startX = centerX; // 线段起点 X 坐标
const startY = centerY; // 线段起点 Y 坐标
let endX, endY, textAnchor;
if (item.direction === 'right') {
endX = centerX + 100; // 线段终点 X 坐标
endY = centerY; // 线段终点 Y 坐标
textAnchor = 'end'
} else if (item.direction === 'bottom-left') {
endX = centerX + 20; // 线段终点 X 坐标
endY = centerY + 50; // 线段终点 Y 坐标
} else if (item.direction === 'bottom-right') {
endX = centerX - 20; // 线段终点 X 坐标
endY = centerY + 50; // 线段终点 Y 坐标
} else {
endX = centerX - 100; // 线段终点 X 坐标
endY = centerY; // 线段终点 Y 坐标
textAnchor = 'start'
}
// console.log(el, 'el-9000');
let list = el?.catheterSiteList?.filter((f) => f.code === item.code)
let name = list?.map((f) => f.name)?.join(',')
let isTipList = list?.map((f) => f.isExpiredWithinOneDay)
let isTip = Math.max(...isTipList)
lineData.value.push({ startX, startY, endX, endY, color: el.legendColor });
textData.value.push({ x: endX, y: endY, name: name, textAnchor: textAnchor || 'middle', isTip: isTip || 0 });
// console.log(startX, startY, endX, endY, '线段起点和终点坐标');
// console.log(lineData.value, 'lineData');
// console.log(textData.value, 'textData');
})
}
// 设置线段和文字数据函数
const setLineTextData = (areaArea, data) => {
areaArea?.forEach((item, index) => {
let el = data.find(f => f?.catheterSiteList?.map((m) => m.code)?.includes(item.code))
if (el) {
setData(index, el, item)
}
});
}
// 初始化设置线段和文字数据函数
const init = () => {
lineData.value = textData.value = []
props.isShowLineText && setLineTextData(props.chartArea, props.data)
}
onMounted(() => {
setTimeout(() => {
init()
}, 500)
})
defineExpose({
cancelHighlightZindex, init
})
</script>
<style lang="scss" scoped>
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.blinking-text {
fill: red;
animation: blink 1s infinite;
}
.svg-box {
position: absolute;
top: var(--top);
z-index: var(--z-index);
}
.polygon-active {
cursor: pointer;
&:hover {
fill: var(--g-color-base);
opacity: 0.3;
}
}
/* 样式设置区域的过渡效果 */
.highlight-area {
fill: var(--g-color-base);
opacity: 0.6;
transition: fill 0.3s ease;
}
.legend-box {
position: absolute;
top: 16px;
right: 36px;
// transform: translateX(-50%);
width: 185px;
min-height: 50px;
flex-shrink: 0;
border-radius: 6px;
background: #fff;
box-shadow: 0 0 10px 4px #0000001a;
.header {
padding: 10px 10px 10px 30px;
.item {
position: relative;
color: #000000;
font-size: 14px;
font-weight: 400;
&::before {
content: '';
position: absolute;
left: -17px;
top: 3px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--bg-color);
}
}
}
}
</style>
新建modelConfig.js文件,用于配置组件相关的信息。同时可以引入不同类型的图像和区域,并且可以在区域配置中配置自己所需的相关信息。
import modelTest from "xxx.png" // 引入组件图像名称
// 模型图配置
export const imageConfig = {
'test': modelTest,
}
// 区域配置
export const areaConfig = {
'test': [
{
code: 'A',
title: '头部',
shape: "poly",
direction: "left",
coords: "178,18,171,18,164,19,159,23,159,29,159,34,160,42,159,50,166,50,174,50,180,50,186,50,192,50,197,48,196,41,197,34,196,26,193,20,185,18"
},
......
]
}
可以在自己所需的页面引入组件即可。
import { imageConfig, areaConfig } from 'modelConfig.js' // 模型图配置信息
<ModelChart2D ref="modelChart2DRef"
:chartImg="imageConfig['test']"
:chartArea="areaConfig['test']"
@callback="callback">
</ModelChart2D>
callback:组件回调函数
当第一次遇到这样的需求时,总是那么的猝不及防,当时内心非常的不安。但是通过自己不断地查阅资料,不断地去尝试,慢慢的有了组件的最初的样子,再到后面的慢慢优化,不断地得到了需求的认可。 虽然不是很完美,但是相关的功能还是实现了。实现了心里总是很欣慰的。 所以每当我们第一次接触的东西时,不要一看就说不,需要自己先调研然后给出可行的实现方案。
不喜勿喷!