一、实现效果
对方案可以添加多张图片,并在图片上进行标注。并且可以通过下方的缩略图切换方案图片
(demo)
二、效果图
三、页面元素
<div class="w-full overflow-auto p-2" style="height: calc(100% - 7rem)"><div class="btns mb-2"><el-button size="small" type="primary" @click="setMode('CIRCLE')">{{ Translate.i18n("圆") }}</el-button><el-button size="small" type="primary" @click="setMode('RECT')">{{ Translate.i18n("矩形") }}</el-button><el-button size="small" type="primary" @click="setMode('POLYGON')">{{ Translate.i18n("多边形") }}</el-button><el-button size="small" type="primary" @click="setMode('PAN')">{{ Translate.i18n("平移模式") }}</el-button></div><div v-for="(v, k) in imgs" :key="k" :id="v.id" v-show="v.id == selectedId" class="relative w-full overflow-hidden bg-gray-110" style="height: calc(100% - 2rem)"></div></div><div class="thumbnail flex h-16 w-full items-center justify-center overflow-auto border-t p-2"><divclass="mr-2 flex h-12 w-12 flex-shrink-0 cursor-pointer items-center justify-center rounded border p-1"v-for="(v, k) in imgs":key="k"@click="changeImg(v, k)":class="selectedId == v.id ? 'border-green-550' : ''"><img class="h-full w-full" :src="v.url" /></div></div>
四、基础定义
npm install ailabel
import AILabel from "ailabel"
let selectedId = ref(1), // 选中的图片ID
imgs = ref([]), // 图片列表
gMap = {}, // 地图实例drawingStyle = {}, // 绘制样式featureLayer = {}, // 绘制图层textLayer = {}, // 文字图层imgLayer = {}, // 图片图层graphCount = ref(0); // 图片计数
const textStyle = { fillStyle: "#F4A460", strokeStyle: "#D2691E", background: true, globalAlpha: 1, fontColor: "#0f0" }; // 文本样式
五、主要逻辑
5.1 添加图片
在添加图片的时候进行画布初始化
// 添加图片
const addImg = async () => {graphCount.value++;if (!selectedId.value) {selectedId.value = graphCount.value;}const n = imgs.value.length;//推荐以网络图片地址为例测试const obj = {url: `http://192.168.2.179:9100/template/img-${n + 1}.jpg`,count: 0,id: graphCount.value};imgs.value.push(obj);await nextTick();initGraph(graphCount.value);
};
5.2 画布初始化
//初始化画布
const initGraph = (id) => {if (selectedId.value != id) return;gMap[id] = new AILabel.Map(id, {center: { x: 400, y: 250 },zoom: 500,mode: "PAN",refreshDelayWhenZooming: true,zoomWhenDrawing: true,panWhenDrawing: true,zoomWheelRatio: 5,withHotKeys: true});// click单击事件gMap[id].events.on("click", (point) => {const feature = gMap[id].getTargetFeatureWithPoint(point.global) || {};if (feature.id) {gMap[id].setActiveFeature(feature);addDeleteIcon(id, feature);showItem.value = true; //可以控制详情显示}});// 绘制完成事件gMap[id].events.on("drawDone", (type, data) => {addFeature(id, type, data);});// 双击编辑gMap[id].events.on("featureSelected", (feature) => {gMap[id].setActiveFeature(feature);// 增加删除按钮addDeleteIcon(id, feature);});// 单机空白取消编辑gMap[id].events.on("featureUnselected", () => {gMap[id].markerLayer.removeAllMarkers();gMap[id].setActiveFeature(null);});// 更新完gMap[id].events.on("featureUpdated", (feature, shape) => {// 更新或者移动需要重新设置删除图标gMap[id].markerLayer.removeAllMarkers();// 更新text的位置const text = textLayer[id].getTextById(feature.id);text.updatePosition(getIndexPosition(feature, shape));feature.updateShape(shape);addDeleteIcon(id, feature);});// 删除gMap[id].events.on("FeatureDeleted", () => {});// 初始化图层featureLayer[id] = new AILabel.Layer.Feature("featureLayer", // 图层id{ name: "featureLayer" },{ zIndex: 10 });gMap[id].addLayer(featureLayer[id]);textLayer[id] = new AILabel.Layer.Text("text",{ name: "textLayer" }, // props{ zIndex: 12, opacity: 1 } // style);gMap[id].addLayer(textLayer[id]);const imgurl = imgs.value.find((v) => v.id == id)?.url;imgLayer[id] = new AILabel.Layer.Image("img",{src: imgurl,width: 800,height: 500,position: {// 左上角相对中心点偏移量x: 0,y: 0}// grid: {// // 3 * 3// columns: [{ color: "#9370DB" }, { color: "#FF6347" }],// rows: [{ color: "#9370DB" }, { color: "#FF6347" }]// } // 网格线颜色},{ zIndex: 5 });gMap[id].addLayer(imgLayer[id]);
};
5.3 增加删除按钮
// 增加删除图标
const addDeleteIcon = (id, feature) => {// 添加delete-iconlet points = getPoints(feature);const gFirstMarker = new AILabel.Marker(+new Date(), // id{src: "http://192.168.2.179:9100/template/delete.png",position: points[1], // 矩形右上角offset: {x: -16,y: -16}}, // markerInfo{ name: "delete" } // props);gFirstMarker.events.on("click", (marker) => {// 首先删除当前markergMap[id].markerLayer.removeMarkerById(marker.id);// 删除对应texttextLayer[id].removeTextById(feature.id);// 删除对应featurefeatureLayer[id].removeFeatureById(feature.id);});gMap[id].markerLayer.addMarker(gFirstMarker);
};
5.4 获取所有点
// 获取所有点
const getPoints = (feature) => {switch (feature.type) {case "RECT":return feature.getPoints();case "CIRCLE":return feature.getEdgePoints();case "POLYGON":return feature.shape.points;default:return [];}
};
5.5 设置模式
// 设置模式
const setMode = (mode) => {const layers = gMap[selectedId.value].getLayers()?.filter((v) => v.type == "IMAGE") || [];if (mode != "PAN" && !layers.length) {elMessage(Translate.i18n("请先添加方案图片"), "warning");return;}gMap[selectedId.value].setMode(mode);if (mode == "CIRCLE") {drawingStyle = { fillStyle: "#9370DB", strokeStyle: "#0000FF", lineWidth: 1 };gMap[selectedId.value].setDrawingStyle(drawingStyle);} else if (mode == "RECT") {drawingStyle = { strokeStyle: "#0f0", lineWidth: 1 };gMap[selectedId.value].setDrawingStyle(drawingStyle);} else if (mode == "POLYGON") {drawingStyle = { strokeStyle: "#00f", fillStyle: "#0f0", globalAlpha: 0.3, lineWidth: 1, fill: true, stroke: true };gMap[selectedId.value].setDrawingStyle(drawingStyle);}
};
5.6 添加图层元素
// 添加图层元素-元素数字标记
const addFeature = (id, type, data, name) => {let count = imgs.value.find((v) => v.id == id)?.count;count++;imgs.value.forEach((v) => {if (v.id == id) {v.count = count;}});if (type == "CIRCLE") {const circleFeature = new AILabel.Feature.Circle(count, // iddata, // shape{ name }, // propsdrawingStyle // style);featureLayer[id].addFeature(circleFeature);addText(id, count, { x: data.cx, y: data.cy });} else if (type == "RECT") {const rectFeature = new AILabel.Feature.Rect(count, // iddata, // shape{ name }, // propsdrawingStyle // style);featureLayer[id].addFeature(rectFeature);addText(id, count, { x: data.x, y: data.y });} else if (type == "POLYGON") {const polygonFeature = new AILabel.Feature.Polygon(count, // id{ points: data }, // shape{ name }, // propsdrawingStyle // style);featureLayer[id].addFeature(polygonFeature);addText(id, count, data[0]);}
};
5.7 添加标记序号文本
// 添加文本
const addText = (layerId, textId, point) => {const textPointer = new AILabel.Text(textId, // id{ text: textId, position: point, offset: { x: 0, y: 0 } }, // shape{ name: "" }, // propstextStyle // style);textLayer[layerId].addText(textPointer);
};
5.8 获取序号文本位置
// 获取序号位置
const getIndexPosition = (feature, shape) => {switch (feature.type) {case "RECT":return { x: shape.x, y: shape.y };case "CIRCLE":return { x: shape.cx, y: shape.cy };case "POLYGON":return shape.points[0];default:return {};}
};
至此大致标注逻辑已全部完成,略微补充一段切换底下缩略图变化画布的逻辑
六、切换缩略图
const changeImg = async (v, k) => {selectedId.value = v.id;await nextTick();if (!gMap[v.id]) {initGraph(v.id);}
};
七、画布resize
const resizeMap = () => {nextTick(() => {gMap[selectedId.value] && gMap[selectedId.value].resize();gMap[selectedId.value] && gMap[selectedId.value].centerAndZoom({ center: { x: 400, y: 250 }, zoom: 500 });});
};watch(() => showItem.value,() => {resizeMap();}
);
八、官方文档
ailabel官方文档