次元图库
36.81M · 2026-03-09
界面入口为对话框模式(Dialog),包含基础表单与地图绘制区:
基本链路如下:
示例代码initMap:
const initMap = () => {
map = new TMap.Map("map-container", {
zoom: 16,
center: new TMap.LatLng(latitude.value, longitude.value),
showControl: false,
});
// 已有几何解析与注入(编辑/查看)
const polygonGeometries: any[] = [];
if ((formType.value === "update" || formType.value === "view") && formData.value.fenceArea) {
const geometries = JSON.parse(formData.value.fenceArea);
geometries.forEach((geo) => {
polygonGeometries.push({
id: `polygon_${polygonGeometries.length}`,
paths: geo.paths.map((p) => new TMap.LatLng(p.lat, p.lng)),
});
});
}
// 多边形与矩形覆盖物
polygon = new TMap.MultiPolygon({ map, geometries: polygonGeometries });
rectangle = new TMap.MultiRectangle({ map, geometries: [] });
// 编辑器绑定
editor = new TMap.tools.GeometryEditor({
map,
overlayList: [
{ overlay: polygon, id: "polygon", styles: { highlight: new TMap.PolygonStyle({ color: "rgba(255,255,0,.6)" }) }, selectedStyleId: "highlight" },
{ overlay: rectangle, id: "rectangle", styles: { highlight: new TMap.PolygonStyle({ color: "rgba(255,255,0,.6)" }) }, selectedStyleId: "highlight" },
],
actionMode: "", // 由外部模式切换驱动
activeOverlayId: activeType.value,
snappable: !isViewMode.value,
selectable: !isViewMode.value,
});
// 绘制/编辑完成后更新数据
editor.on("draw_complete", updateFenceArea);
editor.on("adjust_complete", updateFenceArea);
};
模式切换实现(绘制/编辑/删除/一键删除):
const handleModeChange = (id: "draw"|"edit"|"delete"|"deletes") => {
if (activeMode.value === id && id !== "delete" && id !== "deletes") return;
switch (id) {
case "draw":
editor.stop();
editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.DRAW);
activeMode.value = id;
break;
case "edit":
editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.INTERACT);
activeMode.value = id;
break;
case "delete":
editor.delete();
updateFenceArea();
break;
case "deletes":
// 临时切换到编辑模式,批量选择并删除所有几何
const wasInDrawMode = activeMode.value === "draw";
if (wasInDrawMode) {
activeMode.value = "edit";
editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.INTERACT);
}
editor.select([]);
const polygonIds = polygon?.geometries?.map((g) => g.id) || [];
const rectIds = rectangle?.geometries?.map((g) => g.id) || [];
if (polygonIds.length) { editor.setActiveOverlay("polygon"); editor.select(polygonIds); editor.delete(); }
if (rectIds.length) { editor.setActiveOverlay("rectangle"); editor.select(rectIds); editor.delete(); }
updateFenceArea();
if (wasInDrawMode) {
activeMode.value = "draw";
editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.DRAW);
}
break;
}
};
工具切换(多边形/矩形)仅需切换 activeOverlayId:
const handleToolChange = (id: "polygon"|"rectangle") => {
if (activeType.value === id) return;
activeType.value = id;
editor.setActiveOverlay(id);
};
目标:统一收集 polygon/rectangle 的路径坐标,序列化为字符串到 fenceArea
相交检测:两两比较所有多边形路径,借助 TMap.geometry.computePolygonIntersection 判断是否相交,若相交阻断提交
const updateFenceArea = () => {
const geometries: any[] = [];
const allPolygons: any[] = [];
if (polygon?.geometries?.length) {
polygon.geometries.forEach((geo) => {
geometries.push({ type: "polygon", paths: geo.paths });
allPolygons.push(geo.paths);
});
}
if (rectangle?.geometries?.length) {
rectangle.geometries.forEach((geo) => {
geometries.push({ type: "rectangle", paths: geo.paths });
allPolygons.push(geo.paths);
});
}
// 多边形两两相交检测
if (allPolygons.length > 1) {
let hasIntersection = false;
for (let i = 0; i < allPolygons.length - 1; i++) {
for (let j = i + 1; j < allPolygons.length; j++) {
const inter = TMap.geometry.computePolygonIntersection(
allPolygons[i].map((p) => new TMap.LatLng(p.lat, p.lng)),
allPolygons[j].map((p) => new TMap.LatLng(p.lat, p.lng))
);
if (inter && inter.length > 0) { hasIntersection = true; break; }
}
if (hasIntersection) break;
}
if (hasIntersection) {
message.error("围栏区域不能相交或重叠,请调整区域位置!");
return false;
}
}
formData.value.fenceArea = geometries.length ? JSON.stringify(geometries) : undefined;
return true;
};
动机:列表/详情等界面快速预览围栏形状,减少进入地图的成本
方法:将所有几何的经纬度投影到 canvas 坐标系;取坐标极值计算缩放与居中,绘制填充+描边
const drawFenceThumbnail = async () => {
if (!formData.value.fenceArea) return;
const canvas = document.createElement("canvas");
canvas.width = 384; canvas.height = 216;
const ctx = canvas.getContext("2d"); if (!ctx) return;
// 背景图可替换为项目默认底图
const bg = await new Promise<HTMLImageElement>((res, rej) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => res(img);
img.onerror = rej;
img.src = "https://via.placeholder.com/384x216.png?text=BG";
});
ctx.drawImage(bg, 0, 0, canvas.width, canvas.height);
const geometries = JSON.parse(formData.value.fenceArea);
let minLat=Infinity,maxLat=-Infinity,minLng=Infinity,maxLng=-Infinity;
geometries.forEach((g) => g.paths.forEach((p:any) => {
const lat = p.lat || p.latitude; const lng = p.lng || p.longitude;
minLat = Math.min(minLat, lat); maxLat = Math.max(maxLat, lat);
minLng = Math.min(minLng, lng); maxLng = Math.max(maxLng, lng);
}));
const padding = 10;
const contentW = canvas.width - padding * 2;
const contentH = canvas.height - padding * 2;
const latRange = maxLat - minLat; const lngRange = maxLng - minLng;
let scale = Math.min(contentW / lngRange, contentH / latRange) * 0.9; // 安全边距
const centerLng = (minLng + maxLng) / 2; const centerLat = (minLat + maxLat) / 2;
const cx = canvas.width / 2; const cy = canvas.height / 2;
ctx.strokeStyle = "rgba(252,193,31,.70)";
ctx.lineWidth = 2; ctx.fillStyle = "rgba(219,132,38,.40)";
geometries.forEach((g:any) => {
ctx.beginPath();
g.paths.forEach((p:any, idx:number) => {
const x = cx + ( (p.lng||p.longitude) - centerLng ) * scale;
const y = cy - ( (p.lat||p.latitude) - centerLat ) * scale;
idx === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.closePath(); ctx.fill(); ctx.stroke();
});
const blob = await new Promise<Blob|null>((res) => canvas.toBlob(res, "image/png"));
if (!blob) return;
const file = new File([blob], `fence-thumbnail-${Date.now()}.png`, { type: "image/png" });
const uploadResult = await httpRequest({ file: file as any, action: uploadUrl, method: "POST", filename: "file", data: {} });
if (uploadResult?.data) formData.value.thumbnail = uploadResult.data;
};
(背景图为示例图片)
const getSuggestions = throttle(() => {
if (!address.value) { suggestionList.value = []; return; }
suggest.getSuggestions({ keyword: address.value, location: map.getCenter() })
.then((result) => { suggestionList.value = result.data; })
.catch((error) => {
if (error.status == 120) message.error("搜索过于频繁,请稍后再试");
else message.error("搜索失败," + error.message + ",请联系系统管理员");
});
}, 500);
function setSuggestion(item) {
suggestionList.value = [];
infoWindowList.forEach((w) => w.close()); infoWindowList.length = 0;
address.value = item.title;
const w = new TMap.InfoWindow({ map, position: item.location, content: `<h3>${item.title}</h3><p>地址:${item.address}</p>` });
infoWindowList.push(w);
map.setCenter(item.location);
}
const open = async (type: "create"|"update"|"view", id?: number) => {
dialogVisible.value = true; formType.value = type; resetForm();
if (id) { formLoading.value = true; try { formData.value = await PatrolEfenceApi.getPatrolEfence(id); } finally { formLoading.value = false; } }
nextTick(() => {
initMap();
if (type === "update") { dialogTitle.value = "编辑围栏区域"; activeMode.value = "edit"; editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.INTERACT); }
else if (type === "create") { dialogTitle.value = "新建围栏区域"; activeMode.value = "draw"; editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.DRAW); }
else { dialogTitle.value = "查看围栏区域"; }
});
};
const submitForm = async () => {
editor.stop();
const isValid = updateFenceArea();
if (!isValid) return;
await formRef.value.validate();
formLoading.value = true;
await drawFenceThumbnail();
try {
const data = formData.value as unknown as PatrolEfenceVO;
if (formType.value === "create") { await PatrolEfenceApi.createPatrolEfence(data); message.success(t("common.createSuccess")); }
else { await PatrolEfenceApi.updatePatrolEfence(data); message.success(t("common.updateSuccess")); }
dialogVisible.value = false; emit("success");
} finally { formLoading.value = false; }
};
const cleanupMap = () => {
if (editor) { editor.destroy(); editor = null; }
if (map) { map.destroy(); map = null; }
};
onUnmounted(cleanupMap);
编辑器状态一致性
绘制结束与提交时机
缩略图映射边界
搜索联想与调用频率
只读模式开关
资源释放