<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
*{box-sizing: border-box;}
body{margin:0;padding:10px;font-size:14px;}
.top-bar{
display:flex;
gap:8px;
margin-bottom:10px;
align-items:center;
flex-wrap:wrap;
}
.top-bar button,.top-bar input{
padding:6px 10px;
cursor:pointer;
}
.map-wrap{
position:relative;
width:1900px;
height:900px;
border:2px solid #666;
overflow:hidden;
}
#bgImg{
position:absolute;
left:0;top:0;
width:1900px;height:900px;
object-fit:cover;
display:block;
}
.heat-layer{
position:absolute;left:0;top:0;width:100%;height:100%;
z-index:2;pointer-events:none;display:none;
}
.heat-block{
position:absolute;
filter: blur(12px);
border-radius:6px;
}
.zone-wrap{
position:absolute;
border:2px solid #999;
background:rgba(150,150,150,0.05);
z-index:3;
}
.zone-title{
position:absolute;top:-22px;left:0;
font-weight:bold;white-space:nowrap;
}
.factory-hot{
position:absolute;
border:2px dashed #0066ff;
background:rgba(0,102,255,0.08);
cursor:pointer;
transition:all 0.2s;
}
.factory-hot:hover{background:rgba(0,102,255,0.2);}
.summary-tag{
position:absolute;
transform:translate(-50%,-50%);
padding:4px 10px;border-radius:8px;
color:#fff;font-weight:bold;white-space:nowrap;
box-shadow:0 2px 8px rgba(0,0,0,0.15);
cursor:pointer;
}
.sensor-hot{
position:absolute;
width:32px;height:32px;
transform:translate(-50%,-50%);
border-radius:50%;
background:rgba(0,0,0,0.1);
cursor:pointer;
transition:all 0.2s;
}
.sensor-hot:hover{transform:translate(-50%,-50%) scale(1.2);}
.sensor-dot{
width:12px;height:12px;border-radius:50%;
margin:10px;
}
.drag-rect{
position:absolute;
border:1px dashed #f00;
background:rgba(255,0,0,0.1);
pointer-events:none;
z-index:999;
}
.edit-modal{
position:fixed;
top:50%;left:50%;
transform:translate(-50%,-50%);
background:#fff;
border:1px solid #ccc;
padding:15px;
box-shadow:0 0 20px #0003;
z-index:9999;
display:none;
}
.edit-modal input{
width:280px;
margin:6px 0;
padding:5px;
display:block;
}
.detail-popup{
position:absolute;
background:#fff;border:1px solid #ddd;
padding:12px;box-shadow:0 3px 15px rgba(0,0,0,0.2);
z-index:99;display:none;min-width:220px;
}
.del-btn{
margin-top:8px;
color:#fff;
background:#f44336;
border:none;
padding:4px 8px;
cursor:pointer;
}
</style>
</head>
<body>
<div class="top-bar">
<input type="file" id="uploadImg" accept="image/*">
<button id="drawRectFactory">矩形厂房(拖拽)</button>
<button id="drawSensor">绘制传感器点位</button>
<button id="stopDraw">停止绘制</button>
<button id="toggleHeat">开关热力图层</button>
<button id="toggleSensor">展开/收起点位</button>
<button id="exportJson">导出配置JSON</button>
<button id="importJson">导入配置JSON</button>
<input type="file" id="jsonFile" accept=".json" style="display:none">
</div>
<div class="map-wrap" id="mapWrap">
<img id="bgImg">
<div class="heat-layer" id="heatLayer"></div>
<div class="detail-popup" id="detailPopup"></div>
<div class="drag-rect" id="dragRect"></div>
</div>
<div class="edit-modal" id="editModal">
<h4>编辑配置信息</h4>
<label>图层层级 z(数字越大越上层)</label>
<input type="number" id="editZ" value="5">
<label>名称</label>
<input type="text" id="editName">
<label>自定义参数doing</label>
<input type="text" id="editDoing">
<label>温度(仅传感器生效)</label>
<input type="number" id="editTemp">
<button id="saveEdit">保存</button>
<button id="cancelEdit">取消</button>
</div>
<script>
$(function(){
const $mapWrap = $("#mapWrap");
const $bgImg = $("#bgImg");
const $heatLayer = $("#heatLayer");
const $popup = $("#detailPopup");
const $dragRect = $("#dragRect");
const $editModal = $("#editModal");
// 全局配置:厂房、传感器完全分开存储
let parkConfig = {
zones:[]
};
// 绘制模式:rectFactory / sensor / null
let drawMode = null;
let isDrag = false;
let dragStart = {x:0,y:0};
let editTarget = null;
// 删除标记:记录当前要删除的分区、下标、类型
let delInfo = {zoneIdx: -1, type: "", idx: -1};
let heatShow = false;
let sensorShow = false;
// 1.上传底图(仅前端预览,不存入JSON)
$("#uploadImg").on("change",function(e){
const file = e.target.files[0];
if(!file)return;
const reader = new FileReader();
reader.onload = function(res){
$bgImg.attr("src",res.target.result);
renderAll();
}
reader.readAsDataURL(file);
})
// 切换绘制模式
$("#drawRectFactory").click(()=>drawMode="rectFactory");
$("#drawSensor").click(()=>drawMode="sensor");
$("#stopDraw").click(()=>drawMode=null);
// 矩形拖拽绘制厂房
$mapWrap.mousedown(function(e){
if(drawMode !== "rectFactory") return;
const rect = $mapWrap[0].getBoundingClientRect();
dragStart.x = e.clientX - rect.left;
dragStart.y = e.clientY - rect.top;
isDrag = true;
$dragRect.css({left:dragStart.x,top:dragStart.y,width:0,height:0});
})
$mapWrap.mousemove(function(e){
if(!isDrag || drawMode !== "rectFactory") return;
const rect = $mapWrap[0].getBoundingClientRect();
const curX = e.clientX - rect.left;
const curY = e.clientY - rect.top;
const w = Math.abs(curX - dragStart.x);
const h = Math.abs(curY - dragStart.y);
const l = Math.min(curX, dragStart.x);
const t = Math.min(curY, dragStart.y);
$dragRect.css({left:l,top:t,width:w,height:h});
})
$mapWrap.mouseup(function(e){
if(drawMode !== "rectFactory" || !isDrag){
$popup.hide();
return;
}
isDrag = false;
const x1 = parseInt($dragRect.css("left"));
const y1 = parseInt($dragRect.css("top"));
const x2 = x1 + parseInt($dragRect.css("width"));
const y2 = y1 + parseInt($dragRect.css("height"));
$dragRect.css({width:0,height:0});
if(x2-x1 < 20 || y2-y1 <20)return;
editTarget = {
type:"rectFactory",
data:{x1,y1,x2,y2,z:5,name:"矩形厂房",doing:""}
};
showEditModal();
})
// 点击画布绘制传感器点位
$mapWrap.click(function(e){
if(drawMode === "sensor"){
const rect = $mapWrap[0].getBoundingClientRect();
const x = parseInt(e.clientX - rect.left);
const y = parseInt(e.clientY - rect.top);
editTarget = {
type:"sensor",
data:{x,y,z:8,name:"传感器",doing:"",temp:25}
};
showEditModal();
return;
}
if(!drawMode)$popup.hide();
})
// 弹出编辑框
function showEditModal(){
$("#editZ").val(editTarget.data.z||5);
$("#editName").val(editTarget.data.name);
$("#editDoing").val(editTarget.data.doing);
$("#editTemp").val(editTarget.data.temp||25);
$editModal.show();
}
$("#saveEdit").click(function(){
const z = Number($("#editZ").val()) || 5;
const name = $("#editName").val();
const doing = $("#editDoing").val();
const temp = parseFloat($("#editTemp").val())||25;
editTarget.data.z = z;
editTarget.data.name = name;
editTarget.data.doing = doing;
if(editTarget.type==="sensor") editTarget.data.temp = temp;
// 初始化默认分区
if(parkConfig.zones.length ===0){
parkConfig.zones.push({
zoneName:"默认园区分区",
zoneX1:0,zoneY1:0,zoneX2:1920,zoneY2:1080,
z:3,
factories:[],
sensors:[] // 传感器独立数组,与厂房完全分离
})
}
const zone = parkConfig.zones[0];
if(editTarget.type==="rectFactory"){
zone.factories.push(editTarget.data);
}else{
zone.sensors.push(editTarget.data);
}
$editModal.hide();
renderAll();
})
$("#cancelEdit").click(()=>$editModal.hide());
// 删除元素
$popup.on("click",".del-btn",function(){
const zone = parkConfig.zones[delInfo.zoneIdx];
if(delInfo.type === "factory"){
zone.factories.splice(delInfo.idx,1);
}else if(delInfo.type === "sensor"){
zone.sensors.splice(delInfo.idx,1);
}
$popup.hide();
renderAll();
})
// 温度配色工具
function getTempColor(temp){
if(temp<20)return "#2196F3";
else if(temp>30)return "#f44336";
return "#4CAF50";
}
function clearRender(){
$(".zone-wrap,.factory-hot,.summary-tag,.sensor-hot,.heat-block").remove();
}
// 全局渲染
function renderAll(){
clearRender();
parkConfig.zones.forEach((zone,zoneIdx)=>{
const zw = zone.zoneX2 - zone.zoneX1;
const zh = zone.zoneY2 - zone.zoneY1;
// 分区层级z
let $zone = $(`<div class="zone-wrap"><div class="zone-title">${zone.zoneName}</div></div>`)
.css({
left:zone.zoneX1,
top:zone.zoneY1,
width:zw,
height:zh,
zIndex: zone.z || 3
});
$mapWrap.append($zone);
// ========== 渲染厂房区域 ==========
zone.factories.forEach((fac,facIdx)=>{
const tw = fac.x2 - fac.x1;
const th = fac.y2 - fac.y1;
const cx = fac.x1 + tw/2;
const cy = fac.y1 + th/2;
// 厂房层级z
let $fac = $(`<div class="factory-hot"></div>`)
.css({
left:fac.x1,
top:fac.y1,
width:tw,
height:th,
zIndex: fac.z || 5
});
let $tag = $(`<div class="summary-tag">${fac.name} ${fac.doing}</div>`)
.css({
left:cx,
top:cy,
background:"#0066ff",
zIndex: (fac.z || 5) + 1
});
// 点击弹窗带删除按钮
$fac.add($tag).click(function(e){
e.stopPropagation();
delInfo = {zoneIdx, type:"factory", idx:facIdx};
let html = `<h4>${fac.name} 备注:${fac.doing} | 层级z:${fac.z}</h4>`;
html += `<button class="del-btn">删除此厂房</button>`;
$popup.html(html).css({left:e.pageX,top:e.pageY}).show();
})
$mapWrap.append($fac).append($tag);
// 热力色块
let $heat = $(`<div class="heat-block"></div>`)
.css({
left:fac.x1,
top:fac.y1,
width:tw,
height:th,
background:"#0066ff",
opacity:0.28
})
$heatLayer.append($heat);
})
// ========== 渲染独立传感器(单独数组,和厂房分离) ==========
zone.sensors.forEach((sen,senIdx)=>{
let c = getTempColor(sen.temp);
let $sen = $(`<div class="sensor-hot"><div class="sensor-dot" style="background:${c}"></div></div>`)
.css({
left:sen.x,
top:sen.y,
display:sensorShow?"block":"none",
zIndex: sen.z || 8
})
.click(function(e){
e.stopPropagation();
delInfo = {zoneIdx, type:"sensor", idx:senIdx};
let html = `
<h4>${sen.name}</h4>
<p>层级z:${sen.z}</p>
<p>doing:${sen.doing}</p>
<p>温度:${sen.temp}℃</p>
<button class="del-btn">删除此传感器</button>
`;
$popup.html(html).css({left:e.pageX,top:e.pageY}).show();
})
$mapWrap.append($sen);
})
})
}
// 热力图层开关
$("#toggleHeat").click(function(){
heatShow = !heatShow;
$heatLayer.css("display",heatShow?"block":"none");
$(this).text(heatShow?"关闭热力图层":"开启热力图层");
})
// 传感器显示开关
$("#toggleSensor").click(function(){
sensorShow = !sensorShow;
$(".sensor-hot").css("display",sensorShow?"block":"none");
$(this).text(sensorShow?"收起点位":"展开点位");
})
// 导出JSON:不含图片,厂房、传感器分开放
$("#exportJson").click(function(){
const output = JSON.parse(JSON.stringify(parkConfig));
const jsonStr = JSON.stringify(output,null,2);
const blob = new Blob([jsonStr],{type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "园区地图配置.json";
a.click();
URL.revokeObjectURL(url);
})
// 导入JSON
$("#importJson").click(()=>$("#jsonFile").click());
$("#jsonFile").on("change",function(e){
const file = e.target.files[0];
if(!file)return;
const reader = new FileReader();
reader.onload = function(res){
try{
parkConfig = JSON.parse(res.target.result);
renderAll();
}catch(err){
alert("JSON格式错误");
}
}
reader.readAsText(file);
})
renderAll();
})
</script>
</body>
</html>