Files
2026-07-22 17:23:49 +08:00

521 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>配置资产边界</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; overflow:hidden; font-family:"Microsoft YaHei",Arial,sans-serif; }
#app { display:flex; height:100%; }
/* 左侧地图 */
#mapWrap { flex:1; position:relative; min-width:0; }
#map { height:100%; width:100%; }
/* 右侧配置面板 */
#configPanel {
width:360px; height:100%; background:#fff;
border-left:1px solid #e0e0e0; display:flex; flex-direction:column;
flex-shrink:0; z-index:10000;
}
.panel-header {
padding:12px 16px; background:#f7f8fa; border-bottom:1px solid #eee;
font-size:14px; font-weight:bold; color:#333; flex-shrink:0;
}
.panel-body {
flex:1; overflow-y:auto; padding:12px 16px;
}
.panel-footer {
padding:10px 16px; border-top:1px solid #eee; display:flex; gap:8px; flex-shrink:0;
}
/* 基本设置 */
.setting-row {
display:flex; align-items:center; gap:8px; margin-bottom:10px;
}
.setting-row label { font-size:12px; color:#888; white-space:nowrap; }
.setting-row input[type="color"] { width:36px; height:30px; border:1px solid #ddd; border-radius:4px; cursor:pointer; }
.setting-row span { font-size:12px; color:#888; }
/* 区域卡片 */
.region-card {
border:1px solid #e0e0e0; border-radius:6px; padding:10px 12px;
margin-bottom:8px; background:#fafafa; cursor:pointer;
}
.region-card:hover { border-color:#27ae60; }
.region-card.selected { border-color:#27ae60; background:#e8f5e9; }
.region-header {
display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;
}
.region-header span { font-size:13px; font-weight:bold; color:#333; }
.region-header .del-btn { color:#e74c3c; cursor:pointer; font-size:16px; padding:0 4px; }
.region-info { font-size:12px; color:#888; }
.hole-info { font-size:11px; color:#e74c3c; margin-top:4px; padding-left:10px; border-left:2px solid #e74c3c; }
/* 按钮 */
.btn {
flex:1; padding:9px 0; border:none; border-radius:6px;
cursor:pointer; font-size:13px; font-weight:bold; transition:all 0.2s;
}
.btn-primary { background:#27ae60; color:#fff; }
.btn-primary:hover { background:#219653; }
.btn-success { background:#3498db; color:#fff; }
.btn-reset { background:#f0f0f0; color:#666; border:1px solid #ddd; }
.btn-danger { background:#e74c3c; color:#fff; }
.tip { font-size:11px; color:#999; margin-top:4px; }
/* 移动端 */
@media (max-width:768px) {
#app { flex-direction:column; }
#mapWrap { height:50%; flex:none; }
#configPanel { width:100%; height:50%; flex:none; border-left:none; border-top:1px solid #e0e0e0; }
}
</style>
</head>
<body>
<div id="app">
<div id="mapWrap"><div id="map"></div></div>
<div id="configPanel">
<div class="panel-header">配置资产区域边界</div>
<div class="panel-body">
<!-- 颜色 -->
<div class="setting-row">
<label>边界颜色:</label>
<input type="color" v-model="color" @change="onColorChange">
<span>{{color}}</span>
</div>
<!-- 区域列表 -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="font-size:13px;font-weight:bold;">区域列表 ({{regions.length}})</span>
<button class="btn btn-primary" style="flex:none;padding:5px 12px;" @click="addRegion">+ 新增区域</button>
</div>
<div v-if="regions.length === 0" style="text-align:center;padding:20px;color:#ccc;">
暂无区域,点击"新增区域"开始
</div>
<div v-for="(r, ri) in regions" :key="r.id"
class="region-card" :class="{selected: selectedRegionIndex === ri}"
@click="selectRegionCard(ri)">
<div class="region-header">
<span>区域 {{ri + 1}} <span style="font-size:11px;color:#aaa;">ID:{{r.id}}</span></span>
<span class="del-btn" @click.stop="removeRegion(ri)">&times;</span>
</div>
<div class="region-info">
外圈:{{r.outer ? r.outer.length : 0}} 个点
<span v-if="r.holes && r.holesCount > 0"> 孔洞:{{r.holesCount}} 个</span>
</div>
<div v-if="r.holes && r.holesCount > 0" class="hole-info">
孔洞共 {{r.holesCount}} 个
</div>
<div style="display:flex;gap:4px;margin-top:6px;">
<button class="btn" style="flex:none;padding:3px 8px;font-size:11px;background:#9b59b6;color:#fff;border:none;border-radius:4px;cursor:pointer;" @click.stop="startDrawHole(ri)">挖洞</button>
<button v-if="r.holes && r.holesCount > 0" class="btn" style="flex:none;padding:3px 8px;font-size:11px;background:#e67e22;color:#fff;border:none;border-radius:4px;cursor:pointer;" @click.stop="clearHoles(ri)">清除孔洞</button>
</div>
</div>
<div class="tip">提示:点击区域卡片选中后,可在地图上看到高亮显示</div>
</div>
<div class="panel-footer">
<button class="btn btn-primary" @click="confirmSave">页面暂存</button>
<button class="btn btn-reset" @click="cancelClose">取消</button>
</div>
</div>
</div>
<script>
// Leaflet.draw 中文配置
L.drawLocal = {
draw: {
toolbar: {
actions: { title: '取消绘制', text: '取消' },
finish: { title: '完成绘制', text: '完成' },
undo: { title: '删除最后一个点', text: '删除最后一点' },
buttons: { polygon: '绘制多边形', polyline: '绘制折线', rectangle: '绘制矩形', circle: '绘制圆', marker: '放置标记', circlemarker: '放置圆形标记' }
},
handlers: {
polygon: { tooltip: { start: '点击开始绘制区域', cont: '点击继续绘制', end: '点击起点完成绘制' } },
polyline: { tooltip: { start: '点击开始绘制', cont: '点击继续', end: '点击最后一点完成' } },
rectangle: { tooltip: { start: '点击并拖动绘制矩形' } },
circle: { tooltip: { start: '点击并拖动绘制圆' } },
marker: { tooltip: { start: '点击放置标记' } },
circlemarker: { tooltip: { start: '点击放置圆形标记' } },
simpleshape: { tooltip: { end: '松开鼠标完成绘制' } }
}
},
edit: {
toolbar: {
actions: { save: { title: '保存修改', text: '保存' }, cancel: { title: '取消编辑', text: '取消' }, clearAll: { title: '清除所有', text: '清除全部' } },
buttons: { edit: '编辑边界', editDisabled: '没有可编辑的边界', remove: '删除边界', removeDisabled: '没有可删除的边界' }
},
handlers: { edit: { tooltip: { text: '拖动节点编辑边界', subtext: '点击取消撤销修改' } }, remove: { tooltip: { text: '点击要删除的边界' } } }
}
};
// 从父页面获取资产编号
var assetsId = '';
var _host = '';
try {
var params = window.parentDialogObj['boundConfigDialog'].getTransParams();
console.log('[boundConfig] 父页面传入的参数:', JSON.stringify(params));
assetsId = params.assetsId || params.assetId || params.assetsNo || '';
} catch(e) {
console.warn('获取父页面参数异常:', e);
}
// 尝试从父页面URL推断host
try {
_host = window.parent.location.origin || '';
} catch(e) {}
console.log('[boundConfig] assetsId:', assetsId);
new Vue({
el: '#app',
data: {
map: null,
drawnItems: null,
color: '#27ae60',
regions: [],
assetsId: assetsId,
selectedRegionIndex: -1,
_drawingMode: null // 'new' | 'hole'
},
mounted() {
this.initMap();
},
methods: {
initMap() {
console.log('[boundConfig] initMap, assetsId:', assetsId);
this.map = L.map('map').setView([30.2856, 109.4783], 13);
L.tileLayer('http://sz.esdci.cn:52100/seeyon/seeyonExtend/assetsMap/map/{z}/{x}/{y}/tile.png', { maxZoom: 18, minZoom: 8 }).addTo(this.map);
this.drawnItems = L.featureGroup().addTo(this.map);
this.markerGroup = L.layerGroup().addTo(this.map);
this.markerIcon = L.divIcon({
html: '<img src="./markicon.png" style="width:48px;height:48px;" onerror="this.style.display=\'none\';this.parentNode.innerHTML=\'<div style=width:24px;height:24px;background:#e74c3c;border-radius:50%;border:3px solid #fff;box-shadow:0 0 4px rgba(0,0,0,0.4);></div>\';">',
className: 'custom-marker-icon',
iconSize: [48, 48],
iconAnchor: [24, 24]
});
// 绘制控件
var self = this;
this.drawControl = new L.Control.Draw({
draw: {
polygon: {
allowIntersection: false,
shapeOptions: { color: self.color, weight: 2, fillOpacity: 0.3 }
},
polyline: false, rectangle: false, circle: false, circlemarker: false, marker: false
},
edit: { featureGroup: self.drawnItems, remove: true }
});
this.map.addControl(this.drawControl);
// 同步颜色到绘制控件
this.syncDrawColor();
// 绘制完成事件
this.map.on(L.Draw.Event.CREATED, function(e) {
var layer = e.layer;
var latlngs = layer.getLatLngs()[0].map(function(ll) { return [parseFloat(ll.lat.toFixed(8)), parseFloat(ll.lng.toFixed(8))]; });
if (self._drawingMode === 'hole' && self.selectedRegionIndex >= 0 && self.selectedRegionIndex < self.regions.length) {
// 挖洞:将绘制的多边形作为孔洞添加到选中的区域
var region = self.regions[self.selectedRegionIndex];
if (!region.holes) region.holes = [];
region.holes.push(latlngs);
region.holesCount = region.holes.length;
} else if (self._pendingRegionIndex >= 0 && self._pendingRegionIndex < self.regions.length) {
self.regions[self._pendingRegionIndex].outer = latlngs;
} else {
self.regions.push({ id: self.generateId(), outer: latlngs, holes: [], holesCount: 0 });
self.selectedRegionIndex = self.regions.length - 1;
}
self._drawingMode = null;
self._pendingRegionIndex = -1;
self.refreshMap();
});
// 编辑完成事件
this.map.on(L.Draw.Event.EDITED, function(e) {
var layers = e.layers;
layers.eachLayer(function(layer) {
var latlngs = layer.getLatLngs();
var idx = self.findRegionByLayer(layer);
if (idx >= 0) {
self.regions[idx].outer = latlngs[0].map(function(ll) { return [parseFloat(ll.lat.toFixed(8)), parseFloat(ll.lng.toFixed(8))]; });
// 保留孔洞
var holes = [];
for (var i = 1; i < latlngs.length; i++) {
holes.push(latlngs[i].map(function(ll) { return [parseFloat(ll.lat.toFixed(8)), parseFloat(ll.lng.toFixed(8))]; }));
}
self.regions[idx].holes = holes;
self.regions[idx].holesCount = holes.length;
}
});
self.refreshMap();
});
// 删除事件
this.map.on(L.Draw.Event.DELETED, function(e) {
// 删除对应 region
e.layers.eachLayer(function(layer) {
var idx = self.findRegionByLayer(layer);
if (idx >= 0) {
self.regions.splice(idx, 1);
}
});
if (self.selectedRegionIndex >= self.regions.length) {
self.selectedRegionIndex = self.regions.length - 1;
}
self.refreshMap();
});
this._pendingRegionIndex = -1;
setTimeout(function() {
self.map.invalidateSize();
self.loadAssetLocation();
}, 200);
},
findRegionByLayer(layer) {
if (layer._regionIndex !== undefined && layer._regionIndex >= 0) {
return layer._regionIndex;
}
var latlngs = layer.getLatLngs()[0];
if (!latlngs || latlngs.length === 0) return -1;
var firstPt = [latlngs[0].lat, latlngs[0].lng];
for (var i = 0; i < this.regions.length; i++) {
var r = this.regions[i];
if (r.outer && r.outer.length > 0 &&
Math.abs(r.outer[0][0] - firstPt[0]) < 0.0001 &&
Math.abs(r.outer[0][1] - firstPt[1]) < 0.0001) {
return i;
}
}
return -1;
},
/**
* 根据资产编号调 getAssetDetail 接口,一次获取经纬度和边界数据
* 在地图上标点 + 加载历史边界
*/
loadAssetLocation() {
if (!assetsId) {
console.log('[boundConfig] assetsId为空跳过');
return;
}
var self = this;
var url = _host + '/seeyon/rest/assetsRegion/getAssetDetail?assetCode=' + encodeURIComponent(assetsId);
console.log('[boundConfig] 请求资产详情, url:', url);
fetch(url)
.then(function(r) { return r.json(); })
.then(function(d) {
console.log('[boundConfig] 资产详情返回:', JSON.stringify(d));
if (d.code === 0 && d.data) {
var data = d.data;
var lat = parseFloat(data.lat);
var lng = parseFloat(data.lng);
// 标点
if (!isNaN(lat) && !isNaN(lng)) {
var m = L.marker([lat, lng], { icon: self.markerIcon });
m.bindPopup(
'<b>' + (data.assetName || assetsId) + '</b><br>' +
'坐标:' + lat + ', ' + lng,
{ offset: [0, -24] }
);
m.addTo(self.markerGroup);
self.map.setView([lat, lng], 16);
setTimeout(function() { m.openPopup(); }, 300);
}
// 加载边界数据
if (data.boundData) {
try {
var bound = typeof data.boundData === 'string' ? JSON.parse(data.boundData) : data.boundData;
console.log('[boundConfig] 解析后的边界数据:', bound);
if (bound && bound.regions && bound.regions.length > 0) {
self.color = bound.color || '#27ae60';
self.syncDrawColor();
self.regions = self.normalizeBoundary(bound);
console.log('[boundConfig] regions数量:', self.regions.length);
self.refreshMap();
} else {
console.log('[boundConfig] 边界数据为空或无regions');
}
} catch(e) {
console.error('[boundConfig] 解析边界数据失败:', e, '原始boundData:', data.boundData);
}
} else {
console.log('[boundConfig] 该资产无边界数据 (boundData为空)');
}
} else {
console.warn('[boundConfig] 未查到资产数据, code:', d.code);
}
}).catch(function(err) {
console.error('[boundConfig] 请求资产详情失败:', err);
});
},
normalizeBoundary(bound) {
if (!bound) return [];
var self = this;
if (bound.regions && Array.isArray(bound.regions)) {
return bound.regions.map(function(r) {
var holes = r.holes || [];
return {
id: r.id || self.generateId(),
outer: r.outer || [],
holes: holes,
holesCount: holes.length
};
});
}
if (!bound.polygon || bound.polygon.length === 0) return [];
var polygon = bound.polygon;
if (typeof polygon[0][0] === 'number') {
return [{ id: self.generateId(), outer: polygon, holes: [], holesCount: 0 }];
}
if (typeof polygon[0][0][0] === 'number') {
return [{ id: self.generateId(), outer: polygon[0], holes: polygon.slice(1), holesCount: polygon.length - 1 }];
}
return polygon.map(function(r) {
if (typeof r[0][0] === 'number') {
return { id: self.generateId(), outer: r, holes: [], holesCount: 0 };
}
var h = r.slice(1);
return { id: self.generateId(), outer: r[0], holes: h, holesCount: h.length };
});
},
generateId() {
return Date.now() * 1000 + Math.floor(Math.random() * 1000);
},
onColorChange() {
this.syncDrawColor();
this.refreshMap();
},
syncDrawColor() {
// 重建绘制控件以同步颜色
if (this.drawControl) {
this.map.removeControl(this.drawControl);
}
var self = this;
this.drawControl = new L.Control.Draw({
draw: {
polygon: {
allowIntersection: false,
shapeOptions: { color: self.color, fillColor: self.color, weight: 2, fillOpacity: 0.3 }
},
polyline: false, rectangle: false, circle: false, circlemarker: false, marker: false
},
edit: { featureGroup: self.drawnItems, remove: true }
});
this.map.addControl(this.drawControl);
},
refreshMap() {
this.drawnItems.clearLayers();
var self = this;
this.regions.forEach(function(region, idx) {
if (!region.outer || region.outer.length < 3) return;
var rings = [region.outer].concat(region.holes || []);
var isSelected = (idx === self.selectedRegionIndex);
var polygon = L.polygon(rings, {
color: isSelected ? '#27ae60' : self.color,
fillColor: isSelected ? '#27ae60' : self.color,
weight: isSelected ? 4 : 2,
fillOpacity: isSelected ? 0.5 : 0.3
});
polygon._regionIndex = idx;
self.drawnItems.addLayer(polygon);
});
if (this.drawnItems.getLayers().length > 0) {
this.map.fitBounds(this.drawnItems.getBounds(), { padding: [30, 30] });
}
},
selectRegionCard(index) {
this.selectedRegionIndex = index;
this.refreshMap();
},
addRegion() {
// 直接触发多边形绘制,绘制完成后再添加到 regions
this._pendingRegionIndex = -1;
this._drawingMode = 'new';
new L.Draw.Polygon(this.map, {
shapeOptions: { color: this.color, weight: 2, fillOpacity: 0.3 },
allowIntersection: false
}).enable();
},
startDrawHole(index) {
if (!this.regions[index] || !this.regions[index].outer || this.regions[index].outer.length < 3) {
alert('请先绘制外圈边界!');
return;
}
this.selectedRegionIndex = index;
this._drawingMode = 'hole';
new L.Draw.Polygon(this.map, {
shapeOptions: { color: '#9b59b6', weight: 2, fillOpacity: 0.3 },
allowIntersection: false
}).enable();
},
clearHoles(index) {
if (this.regions[index]) {
this.regions[index].holes = [];
this.regions[index].holesCount = 0;
this.refreshMap();
}
},
removeRegion(index) {
this.regions.splice(index, 1);
if (this.selectedRegionIndex >= this.regions.length) {
this.selectedRegionIndex = this.regions.length - 1;
}
this.refreshMap();
},
confirmSave() {
var cleanRegions = this.regions
.filter(function(r) { return r.outer && r.outer.length >= 3; })
.map(function(r) {
return {
id: r.id,
outer: r.outer,
holes: (r.holes || []).filter(function(h) { return h && h.length >= 3; })
};
});
var result = JSON.stringify({ regions: cleanRegions, color: this.color });
// 设置返回值供父页面获取
window._boundConfigResult = result;
alert('区域边界数据生成成功');
},
cancelClose() {
window._boundConfigResult = '';
}
}
});
function OK() {
// 弹框框架调用此函数获取返回值
return window._boundConfigResult || '';
}
</script>
</body>
</html>