完成初版

This commit is contained in:
2026-06-18 16:26:31 +08:00
parent 6ca67ec941
commit 8e3dd5e1cd
27 changed files with 5024 additions and 678 deletions

View File

@@ -0,0 +1,498 @@
<!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>
// 从父页面获取已有边界数据
var oldBoundConfig = '';
var assetsId = '';
var _host = '';
try {
var params = window.parentDialogObj['boundConfigDialog'].getTransParams();
console.log(window.parentDialogObj['boundConfigDialog'])
console.log('[boundConfig] 父页面传入的参数:', JSON.stringify(params));
oldBoundConfig = params.oldBoundConfig || '';
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] oldBoundConfig:', oldBoundConfig ? (oldBoundConfig.substring(0, 100) + '...') : '(空)');
console.log('[boundConfig] assetsId:', assetsId);
new Vue({
el: '#app',
data: {
map: null,
drawnItems: null,
color: '#ff0000',
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://localhost:3080/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 [ll.lat, ll.lng]; });
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 [ll.lat, ll.lng]; });
// 保留孔洞
var holes = [];
for (var i = 1; i < latlngs.length; i++) {
holes.push(latlngs[i].map(function(ll) { return [ll.lat, ll.lng]; }));
}
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();
self.loadBoundData();
}, 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;
},
loadBoundData() {
console.log('[boundConfig] loadBoundData, oldBoundConfig:', oldBoundConfig);
if (!oldBoundConfig) {
console.log('[boundConfig] oldBoundConfig为空跳过');
return;
}
try {
var bound = typeof oldBoundConfig === 'string' ? JSON.parse(oldBoundConfig) : oldBoundConfig;
console.log('[boundConfig] 解析后的边界数据:', bound);
this.color = bound.color || '#ff0000';
this.syncDrawColor();
this.regions = this.normalizeBoundary(bound);
console.log('[boundConfig] regions数量:', this.regions.length);
this.refreshMap();
} catch(e) {
console.error('[boundConfig] 解析边界数据失败:', e, '原始数据:', oldBoundConfig);
}
},
// 根据 assetsId资产编号调接口获取经纬度在地图上标点
loadAssetLocation() {
if (!assetsId) {
console.log('[boundConfig] assetsId为空跳过标点');
return;
}
var self = this;
var url = _host + '/seeyon/rest/assetsRegion/getByBounds';
console.log('[boundConfig] 请求资产位置, url:', url, 'assetId:', assetsId);
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assetId: assetsId })
}).then(function(r) {
console.log('[boundConfig] 响应状态:', r.status);
return r.json();
}).then(function(d) {
console.log('[boundConfig] 资产位置返回:', d);
if (d.code === 0 && d.data && d.data.length > 0) {
var region = d.data[0];
var lat = parseFloat(region.lat);
var lng = parseFloat(region.lng);
console.log('[boundConfig] 经纬度:', lat, lng);
if (!isNaN(lat) && !isNaN(lng)) {
var m = L.marker([lat, lng], { icon: self.markerIcon });
m.bindPopup(
'<b>' + (region.assets && region.assets[0] ? region.assets[0].assetName : assetsId) + '</b><br>' +
'坐标:' + lat + ', ' + lng,
{ offset: [0, -24] }
);
m.addTo(self.markerGroup);
self.map.setView([lat, lng], 16);
setTimeout(function() { m.openPopup(); }, 300);
}
} else {
console.warn('[boundConfig] 未查到资产位置数据, code:', d.code, 'data:', d.data);
}
}).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>