Files
AssetsMap/assetsMap/mapbak.html

1070 lines
34 KiB
HTML
Raw Normal View History

2026-06-18 16:26:31 +08:00
<!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.markercluster/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
body { margin: 0; font-family: Arial, sans-serif; }
#map { height: 100vh; width: 100%; }
#topStats {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
}
.statInline {
background: rgba(255,255,255,1);
padding: 16px 24px;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
min-width: 700px;
border: 1px solid #eee;
}
.statItem {
font-size:14px;
color:#555;
}
.statNumber {
font-size:18px;
font-weight:bold;
margin-left:6px;
color: #27ae60;
}
.divider {
width:90%;
height:1px;
background:#eee;
}
.filterRow {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
}
.searchRow {
display: flex;
align-items: center;
gap: 8px;
}
.filterGroup {
display:flex;
align-items: center;
gap:4px;
}
.filterLabel {
font-size:12px;
color:#888;
white-space:nowrap;
}
.filterSelect {
padding:8px 12px;
border-radius:10px;
border:1px solid #ddd;
font-size:13px;
background:#fff;
cursor:pointer;
min-width:110px;
transition: all 0.2s;
}
.filterSelect:focus {
outline: none;
border-color: #27ae60;
box-shadow: 0 0 0 3px rgba(39, 174, 96, 0.1);
}
.searchInput {
padding:10px 16px;
border-radius:12px;
border:1px solid #ddd;
width:280px;
font-size:14px;
transition: all 0.2s;
}
.searchInput:focus {
outline: none;
border-color: #27ae60;
box-shadow: 0 0 0 3px rgba(39, 174, 96, 0.15);
}
.searchBtn {
padding:10px 20px;
background:#27ae60;
color:#fff;
border:none;
border-radius:12px;
cursor:pointer;
font-size:14px;
font-weight: bold;
transition: all 0.2s;
}
.searchBtn:hover {
background: #219653;
transform: translateY(-1px);
}
.searchPanel {
position: absolute;
top:70px;
left: 50%;
transform:translateX(5%);
width:280px;
background:#fff;
border-radius:12px;
box-shadow:0 6px 20px rgba(0,0,0,0.2);
z-index:9999;
overflow: hidden;
}
.searchItem {
padding:12px 14px;
border-bottom:1px solid #eee;
cursor:pointer;
font-size: 14px;
}
.searchItem:hover {
background:#f8f9fa;
}
#assetPanel {
position: absolute;
top:0;
right:0;
width: 360px;
height: 100vh;
background:#fff;
box-shadow:-2px 0 10px rgba(0,0,0,0.15);
transform:translateX(100%);
transition:0.3s;
display:flex;
flex-direction:column;
z-index:9999;
}
#assetPanel.show { transform:translateX(0); }
.panelHeader {
padding:18px;
background:#f7f8fa;
border-bottom:1px solid #eee;
}
.panelTitle {
font-size:17px;
font-weight:800;
}
.closeBtn {
float:right;
cursor:pointer;
font-size:18px;
color:#888;
}
.panelMeta {
padding:12px 18px;
font-size:13px;
color:#666;
border-bottom:1px solid #eee;
}
.assetList {
flex:1;
overflow:auto;
}
.assetItem {
padding:16px;
border-bottom:1px solid #f0f0f0;
cursor:pointer;
border-radius: 8px;
}
.assetItem:hover {
background: #fafafa;
}
.drawBtn {
margin-top:8px;
padding:8px 14px;
border:none;
border-radius:8px;
cursor:pointer;
background:#27ae60;
color:#fff;
font-weight: bold;
}
.drawBtn.hasBounds {
background:#e67e22;
}
.color-item {
margin-top:8px;
display:flex;
align-items:center;
gap:8px;
}
.color-item label {
font-size:12px;
color:#666;
}
input[type="color"] {
width:32px;
height:26px;
border:none;
cursor:pointer;
border-radius: 4px;
}
.assetManager,.assetOwner,.assetStatus,.assetAddress {
font-size:14px;
color:#666;
margin-top:4px;
}
.marker-cluster-custom {
background:transparent !important;
border:none !important;
}
.marker-cluster-custom .cluster-inner {
width:40px;
height:40px;
border-radius:50%;
background:rgba(231,76,60,0.85);
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-weight:bold;
}
.assetTenant,.assetPurpose,.assetRemark,.assetArea {
font-size: 14px;
color: #666;
margin-top: 4px;
}
.custom-marker-icon {
background: none !important;
border: none !important;
}
.filter-toggle-btn {
display: none;
padding: 8px 16px;
background: #27ae60;
color: #fff;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 13px;
margin-top: 8px;
}
.panel-collapse-btn {
position: absolute;
bottom: -32px;
left: 50%;
transform: translateX(-50%);
background: rgba(255,255,255,0.95);
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 10px 10px;
padding: 4px 20px;
cursor: pointer;
font-size: 12px;
color: #888;
z-index: 9999;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
transition: all 0.2s;
}
.panel-collapse-btn:hover {
color: #27ae60;
background: #fff;
}
.collapsed-indicator {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
background: rgba(255,255,255,0.95);
padding: 8px 20px;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
cursor: pointer;
font-size: 13px;
color: #27ae60;
font-weight: bold;
border: 1px solid #eee;
user-select: none;
}
.collapsed-indicator:hover {
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
}
/* 移动端样式 */
@media (max-width: 768px) {
#topStats {
width: calc(100% - 24px);
left: 12px;
transform: none;
}
.statInline {
min-width: auto;
width: 100%;
max-width: none;
padding: 12px 16px;
gap: 8px;
}
.filter-toggle-btn {
display: block;
width: 100%;
}
.filterRow {
display: none;
flex-direction: column;
gap: 8px;
width: 100%;
}
.filterRow.expanded {
display: flex;
}
.filterGroup {
width: 100%;
}
.filterSelect {
width: 100%;
min-height: 44px;
font-size: 16px;
}
.searchRow {
width: 100%;
flex-direction: column;
}
.searchInput {
width: 100%;
min-height: 44px;
font-size: 16px;
box-sizing: border-box;
}
.searchBtn {
min-height: 44px;
width: 100%;
}
.searchPanel {
left: 16px;
right: 16px;
width: auto;
transform: none;
}
#assetPanel {
width: 100%;
height: 60vh;
top: auto;
bottom: 0;
right: 0;
transform: translateY(100%);
border-radius: 16px 16px 0 0;
}
#assetPanel.show {
transform: translateY(0);
}
.panelHeader {
padding: 14px 16px;
}
.closeBtn {
padding: 8px;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.assetItem .drawBtn {
min-height: 44px;
font-size: 14px;
}
.assetItem > div:last-child {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.color-item input[type="color"] {
width: 44px;
height: 44px;
}
.leaflet-draw-actions {
display: flex !important;
flex-wrap: wrap;
}
.panel-collapse-btn {
bottom: -28px;
padding: 3px 16px;
font-size: 11px;
}
.collapsed-indicator {
padding: 6px 16px;
font-size: 12px;
}
}
</style>
</head>
<body>
<div id="app">
<div v-if="panelCollapsed" class="collapsed-indicator" @click="panelCollapsed = false">
资产数量: {{filteredCount}} ▼
</div>
<div id="topStats" v-show="!isDrawing && !panelCollapsed">
<div class="statInline">
<button class="panel-collapse-btn" @click="panelCollapsed = true">收起面板 ▲</button>
<div class="statItem"><span class="statNumber total">资产数量: {{filteredCount}}</span></div>
<div class="divider"></div>
<button v-if="isMobile" @click="toggleFilter" class="filter-toggle-btn">
{{ filterExpanded ? '收起筛选' : '展开筛选' }}
</button>
<div class="filterRow" :class="{ expanded: filterExpanded || !isMobile }">
<div class="filterGroup">
<label class="filterLabel">所属单位</label>
<select v-model="filterOwner" @change="onFilterChange" class="filterSelect">
<option value="">全部</option>
<option v-for="(id, name) in deptOptions" :key="id" :value="name">{{name}}</option>
</select>
</div>
<div class="filterGroup">
<label class="filterLabel">一级类型</label>
<select v-model="filterLevel1" @change="onLevel1Change" class="filterSelect">
<option value="">全部</option>
<option v-for="(val, key) in categoryOptions" :key="key" :value="val.id">{{val.showValue}}</option>
</select>
</div>
<div class="filterGroup">
<label class="filterLabel">二级类型</label>
<select v-model="filterLevel2" @change="onLevel2Change" class="filterSelect" :disabled="!filterLevel1">
<option value="">全部</option>
<option v-for="(val, key) in level2Options" :key="key" :value="val.id">{{val.showValue}}</option>
</select>
</div>
<div class="filterGroup">
<label class="filterLabel">资产分类</label>
<select v-model="filterLevel3" @change="onFilterChange" class="filterSelect" :disabled="!filterLevel2">
<option value="">全部</option>
<option v-for="(val, key) in level3Options" :key="key" :value="val.id">{{val.showValue}}</option>
</select>
</div>
</div>
<div class="divider"></div>
<div class="searchRow">
<input v-model="searchKeyword" @keyup.enter="searchLocation" class="searchInput" placeholder="输入详细地址">
<button @click="searchLocation" class="searchBtn">搜索</button>
</div>
</div>
</div>
<div v-if="searchResults.length" class="searchPanel">
<div class="searchItem" v-for="(item,index) in searchResults" :key="index" @click="selectSearchResult(item)">
{{item.assetName || item.address}}
</div>
</div>
<div id="map"></div>
<div id="assetPanel" :class="{show:panelVisible}">
<div class="panelHeader">
<span class="panelTitle">{{currentRegion.address}}</span>
<span class="closeBtn" @click="closePanel"></span>
</div>
<div class="panelMeta">资产数量:{{currentRegion.assets?.length || 0}}</div>
<div class="assetList">
<div class="assetItem" v-for="(a,index) in currentRegion.assets" :key="index">
<div class="assetManager">资产编码:{{a.assetCode || a.id}}</div>
<div class="assetOwner">资产名称:{{a.assetName || '-'}}</div>
<div class="assetAddress">坐落位置:{{a.address || '-'}}</div>
<div class="assetStatus">地点:{{a.owner || '-'}}</div>
<div class="assetArea">面积:{{a.area || '-'}}㎡</div>
<div class="assetRemark">管理单位:{{a.manager || '-'}}</div>
<div class="assetRemark">备注:{{a.remark || '暂无'}}</div>
<div class="color-item">
<label>边界颜色:</label>
<input type="color" v-model="bindColor" @change="onColorChange(a)">
</div>
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;">
<button class="drawBtn" @click.stop="showAssetPolygon(a)">显示边界</button>
<button class="drawBtn" @click.stop="startDrawBound(a)">绘制资产边界</button>
<button class="drawBtn" style="background:#9b59b6;" @click.stop="startDrawHole(a)">掏空区域</button>
<button class="drawBtn" style="background:#3498db;" @click.stop="goToDetail(a)">查看详情</button>
</div>
</div>
</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: '点击要删除的边界' } } }
}
};
new Vue({
el: "#app",
data: {
host: 'http://' + '223.76.78.69:52100',
searchKeyword: '',
searchResults: [],
map: null,
markerGroup: null,
polygonLayer: null,
drawnItems: null,
drawControl: null,
bindAsset: null,
bindColor: '#ff0000',
regions: [],
loadedKeys: new Set(),
_debounceTimer: null,
filteredCount: 0,
filterOwner: '',
filterLevel1: '',
filterLevel2: '',
filterLevel3: '',
deptOptions: {},
categoryOptions: {},
level2Options: {},
level3Options: {},
panelVisible: false,
isDrawing: false,
activeMarker: null,
currentRegion: {},
markerIcon: null,
isMobile: false,
filterExpanded: false,
panelCollapsed: false,
// 边界数据 - 格式: [{id, outer:[[lat,lng]...], holes:[[[lat,lng]...]...]}]
currentRegions: null,
skipColorSave: false,
_drawingMode: null, // 'new' | 'hole'
_selectedRegionId: null,
_regionClicked: false,
_currentAssetId: null
},
watch: {
bindColor(newColor) {
this.drawnItems.eachLayer(layer => {
if (layer.setStyle && layer._regionId !== this._selectedRegionId) {
layer.setStyle({ color: newColor, fillColor: newColor });
}
});
if (!this.skipColorSave && this._currentAssetId && this.currentRegions && this.currentRegions.length > 0) {
this.saveAssetBound(this._currentAssetId, this.currentRegions, newColor);
}
this.skipColorSave = false;
}
},
mounted() {
this.checkDevice();
window.addEventListener('resize', () => this.checkDevice());
this.initMap();
this.initDrawControl();
this.loadFilterOptions();
let params = new URLSearchParams(window.location.search);
let assetId = params.get('assetId');
if (assetId) {
this.loadAssetById(assetId);
} else {
this.loadByViewport();
}
},
methods: {
checkDevice() {
this.isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent)
|| window.innerWidth <= 768;
},
toggleFilter() {
this.filterExpanded = !this.filterExpanded;
},
generateRegionId() {
return Date.now() * 1000 + Math.floor(Math.random() * 1000);
},
initMap() {
this.map = L.map('map').setView([30.2856, 109.4783], 13);
L.tileLayer('./map/{z}/{x}/{y}/tile.png', { maxZoom: 18, minZoom: 8 }).addTo(this.map);
this.drawnItems = L.featureGroup().addTo(this.map);
this.polygonLayer = L.featureGroup().addTo(this.map);
this.markerIcon = L.divIcon({
html: '<img src="./markicon.png" style="width:48px;height:48px;">',
className: 'custom-marker-icon',
iconSize: [48, 48],
iconAnchor: [24, 24]
});
this.markerGroup = L.markerClusterGroup({
iconCreateFunction: function(cluster) {
var total = 0;
cluster.getAllChildMarkers().forEach(function(m) {
var rd = m.options.regionData;
total += (rd && rd.assets) ? rd.assets.length : 0;
});
return L.divIcon({
html: '<div class="cluster-inner">' + total + '</div>',
className: 'marker-cluster-custom',
iconSize: L.point(40, 40)
});
}
}).addTo(this.map);
this.map.on('click', () => {
if (this._regionClicked) { this._regionClicked = false; return; }
if (this.isDrawing) return;
this.searchResults = [];
this.panelVisible = false;
this.polygonLayer.clearLayers();
this._selectedRegionId = null;
this.drawnItems.eachLayer(layer => {
if (layer.setStyle && layer._regionId) {
layer.setStyle({ weight: 2, fillOpacity: 0.4, color: this.bindColor });
}
});
if (this.activeMarker) {
this.unhighlightMarker(this.activeMarker);
this.activeMarker = null;
}
});
this.map.on('moveend', () => {
clearTimeout(this._debounceTimer);
this._debounceTimer = setTimeout(() => this.loadByViewport(), 300);
});
},
initDrawControl() {
this.drawControl = new L.Control.Draw({
edit: { featureGroup: this.drawnItems },
draw: { polyline: false, rectangle: false, circle: false, marker: false, polygon: true }
});
this.map.addControl(this.drawControl);
this.map.on(L.Draw.Event.DRAWSTOP, () => { this.isDrawing = false; this._drawingMode = null; });
this.map.on(L.Draw.Event.CREATED, (e) => {
this.isDrawing = false;
if (!this.bindAsset) { alert("请先选择资产!"); return; }
let latlngs = e.layer.getLatLngs()[0].map(p => [p.lat, p.lng]);
if (this._drawingMode === 'hole' && this._selectedRegionId && this.currentRegions) {
let region = this.currentRegions.find(r => r.id === this._selectedRegionId);
if (region) { region.holes.push(latlngs); }
} else {
if (!this.currentRegions) this.currentRegions = [];
this.currentRegions.push({ id: this.generateRegionId(), outer: latlngs, holes: [] });
}
this._drawingMode = null;
this._selectedRegionId = null;
this.renderBoundRegions();
if (confirm("确定保存边界?")) {
this.saveAssetBound(this._currentAssetId, this.currentRegions, this.bindColor);
}
});
this.map.on(L.Draw.Event.EDITED, (e) => {
if (!this._currentAssetId || !this.currentRegions) return;
let changed = false;
e.layers.eachLayer(layer => {
let region = this.currentRegions.find(r => r.id === layer._regionId);
if (!region) return;
let latlngs = layer.getLatLngs();
if (latlngs && latlngs.length > 0) {
region.outer = latlngs[0].map(p => [p.lat, p.lng]);
region.holes = latlngs.slice(1).map(h => h.map(p => [p.lat, p.lng]));
changed = true;
}
});
if (changed) {
this.renderBoundRegions();
if (confirm("确定保存修改后的边界?")) {
this.saveAssetBound(this._currentAssetId, this.currentRegions, this.bindColor);
}
}
});
this.map.on(L.Draw.Event.DELETED, (e) => {
if (!this._currentAssetId || !this.currentRegions) return;
let deletedIds = [];
e.layers.eachLayer(layer => { if (layer._regionId) deletedIds.push(layer._regionId); });
if (deletedIds.length > 0 && confirm("确定删除选中的边界?")) {
this.currentRegions = this.currentRegions.filter(r => !deletedIds.includes(r.id));
this.renderBoundRegions();
this.saveAssetBound(this._currentAssetId, this.currentRegions, this.bindColor);
}
});
},
selectRegion(regionId) {
this._regionClicked = true;
this._selectedRegionId = regionId;
this.drawnItems.eachLayer(layer => {
if (layer._regionId === regionId) {
layer.setStyle({ weight: 4, fillOpacity: 0.6, color: '#e74c3c' });
} else if (layer.setStyle && layer._regionId) {
layer.setStyle({ weight: 2, fillOpacity: 0.4, color: this.bindColor });
}
});
if (this._drawingMode === 'hole') {
setTimeout(() => document.querySelector('.leaflet-draw-draw-polygon').click(), 100);
}
},
renderBoundRegions() {
this.drawnItems.clearLayers();
if (!this.currentRegions || this.currentRegions.length === 0) return;
let self = this;
this.currentRegions.forEach(function(region) {
if (!region.outer || region.outer.length < 3) return;
let rings = [region.outer].concat(region.holes || []);
let polygon = L.polygon(rings, {
color: self.bindColor, fillColor: self.bindColor, weight: 2, fillOpacity: 0.4
});
polygon._regionId = region.id;
polygon.on('click', function(e) {
L.DomEvent.stopPropagation(e);
self.selectRegion(region.id);
});
self.drawnItems.addLayer(polygon);
});
},
onColorChange(asset) {
if (this.currentRegions && this.currentRegions.length > 0) {
this.saveAssetBound(asset.id, this.currentRegions, this.bindColor);
}
},
startDrawBound(asset) {
this.bindAsset = asset;
this._currentAssetId = asset.id;
if (!this.currentRegions) this.currentRegions = [];
this._selectedRegionId = null;
this.panelVisible = false;
this.isDrawing = true;
this._drawingMode = 'new';
document.querySelector('.leaflet-draw-draw-polygon').click();
},
startDrawHole(asset) {
if (!this.currentRegions || this.currentRegions.length === 0) {
alert("请先绘制或加载边界!");
return;
}
this.bindAsset = asset;
this._currentAssetId = asset.id;
this._drawingMode = 'hole';
this._selectedRegionId = null;
this.panelVisible = false;
this.isDrawing = true;
alert("请点击地图上要掏空的外圈区域");
},
saveAssetBound(assetId, regions, color) {
let polygonData = (regions && regions.length > 0) ? JSON.stringify({ regions: regions, color: color }) : '';
fetch(this.host + "/seeyon/rest/assetsRegion/saveAssetPolygon", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ assetId: assetId, polygonData: polygonData })
})
.then(function(res) { return res.json(); })
.then(function(res) {
if (res.code === 0) {
if (!polygonData) { this.currentRegions = []; this.drawnItems.clearLayers(); }
this.skipColorSave = true;
this.bindColor = color;
} else {
alert("操作失败:" + res.message);
}
this.bindAsset = null;
}.bind(this))
.catch(function() { alert("网络异常"); this.bindAsset = null; }.bind(this));
},
showAssetPolygon(asset) {
this.drawnItems.clearLayers();
this.polygonLayer.clearLayers();
this.bindAsset = asset;
this._currentAssetId = asset.id;
this.currentRegions = [];
this._selectedRegionId = null;
var self = this;
fetch(this.host + "/seeyon/rest/assetsRegion/getAssetBound?assetId=" + asset.id)
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.code === 0 && d.data && d.data.boundData) {
var bound = typeof d.data.boundData === 'string' ? JSON.parse(d.data.boundData) : d.data.boundData;
var color = bound.color || '#3498db';
self.currentRegions = self.normalizeBoundary(bound);
self.skipColorSave = true;
self.bindColor = color;
self.renderBoundRegions();
if (self.drawnItems.getLayers().length > 0) {
self.map.fitBounds(self.drawnItems.getBounds(), { padding: [30, 30] });
}
}
});
},
normalizeBoundary(bound) {
if (!bound) return [];
var self = this;
// 新格式: { regions: [{id, outer, holes}], color }
if (bound.regions && Array.isArray(bound.regions)) {
return bound.regions.map(function(r) {
return { id: r.id || self.generateRegionId(), outer: r.outer || [], holes: r.holes || [] };
});
}
// 旧格式: { polygon: [...], color }
if (!bound.polygon || bound.polygon.length === 0) return [];
var polygon = bound.polygon;
if (typeof polygon[0][0] === 'number') {
return [{ id: this.generateRegionId(), outer: polygon, holes: [] }];
}
if (typeof polygon[0][0][0] === 'number') {
return [{ id: this.generateRegionId(), outer: polygon[0], holes: polygon.slice(1) }];
}
return polygon.map(function(r) {
if (typeof r[0][0] === 'number') {
return { id: self.generateRegionId(), outer: r, holes: [] };
}
return { id: self.generateRegionId(), outer: r[0], holes: r.slice(1) };
});
},
loadFilterOptions() {
var self = this;
fetch(this.host + "/seeyon/rest/assetsRegion/getDeptOptions")
.then(function(r) { return r.json(); }).then(function(d) {
if (d.code === 0) self.deptOptions = self.flattenDeptTree(d.data || []);
});
fetch(this.host + "/seeyon/rest/assetsRegion/getCategoryOptions")
.then(function(r) { return r.json(); }).then(function(d) { if (d.code === 0) self.categoryOptions = d.data || {}; });
},
flattenDeptTree(tree) {
var result = {};
function walk(nodes) {
if (!nodes) return;
nodes.forEach(function(node) {
result[node.name] = node.id;
if (node.children && node.children.length > 0) walk(node.children);
});
}
walk(tree);
return result;
},
findOptionById(options, id) {
for (var key in options) { if (options[key].id === id) return options[key]; }
return null;
},
onLevel1Change() {
this.filterLevel2 = ''; this.filterLevel3 = ''; this.level3Options = {};
var found = this.findOptionById(this.categoryOptions, this.filterLevel1);
this.level2Options = found ? (found.children || {}) : {};
this.onFilterChange();
},
onLevel2Change() {
this.filterLevel3 = '';
var found = this.findOptionById(this.level2Options, this.filterLevel2);
this.level3Options = found ? (found.children || {}) : {};
this.onFilterChange();
},
onFilterChange() {
this.regions = []; this.loadedKeys.clear(); this.loadByViewport();
},
loadByViewport() {
var self = this;
var b = this.map.getBounds();
var latPad = (b.getNorth() - b.getSouth()) * 0.5;
var lngPad = (b.getEast() - b.getWest()) * 0.5;
var body = {
south: b.getSouth() - latPad, west: b.getWest() - lngPad,
north: b.getNorth() + latPad, east: b.getEast() + lngPad
};
if (this.filterOwner) body.owner = this.filterOwner;
if (this.filterLevel1) body.level1 = this.filterLevel1;
if (this.filterLevel2) body.level2 = this.filterLevel2;
if (this.filterLevel3) body.level3 = this.filterLevel3;
fetch(this.host + "/seeyon/rest/assetsRegion/getByBounds", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.code === 0) { self.mergeRegions(d.data || []); self.renderMarkers(); }
});
},
mergeRegions(newRegions) {
var self = this;
newRegions.forEach(function(nr) {
var key = nr.lat + '_' + nr.lng + '_' + nr.address;
if (self.loadedKeys.has(key)) return;
self.loadedKeys.add(key);
self.regions.push(nr);
});
this.filteredCount = this.regions.reduce(function(s, r) { return s + (r.assets || []).length; }, 0);
},
calcArea(list) { return list.reduce(function(s, a) { return s + Number(a.area || 0); }, 0); },
highlightMarker(m) {
if (this.activeMarker && this.activeMarker !== m) this.unhighlightMarker(this.activeMarker);
var el = m.getElement();
if (el) {
var img = el.querySelector('img') || el;
img.style.filter = 'drop-shadow(0 0 6px #e74c3c) drop-shadow(0 0 12px #e74c3c) drop-shadow(0 0 20px rgba(231,76,60,0.6))';
img.style.transform = 'scale(2)'; img.style.transformOrigin = 'center center'; el.style.zIndex = 1000;
}
this.activeMarker = m;
},
unhighlightMarker(m) {
var el = m.getElement();
if (el) { var img = el.querySelector('img') || el; img.style.filter = ''; img.style.transform = ''; el.style.zIndex = ''; }
},
renderMarkers() {
this.markerGroup.clearLayers();
if (this.activeMarker) this.activeMarker = null;
var self = this;
this.regions.forEach(function(r) {
var lat = parseFloat(r.lat), lng = parseFloat(r.lng);
if (isNaN(lat) || isNaN(lng)) return;
var m = L.marker([lat, lng], { icon: self.markerIcon });
m.options.regionData = r;
m.on("click", function() {
self.highlightMarker(m);
self.showAssetPanel(r);
if (self.isMobile) {
var assets = r.assets || [];
var totalArea = self.calcArea(assets);
var codes = assets.map(function(a) { return a.assetCode || a.id; }).join('、');
var names = assets.map(function(a) { return a.assetName || '-'; }).join('、');
m.bindPopup(
'<b>资产编码:' + codes + '</b><br>资产名称:' + names + '<br>坐落位置:' + (r.address || '-') +
'<br>地点:' + (r.owner || '-') + '<br>资产面积:' + totalArea + '㎡<br>管理单位:' + (r.manager || '-'),
{ offset: [0, -24] }
).openPopup();
}
});
if (!self.isMobile) {
m.on("mouseover", function() {
var assets = r.assets || [];
var totalArea = self.calcArea(assets);
var codes = assets.map(function(a) { return a.assetCode || a.id; }).join('、');
var names = assets.map(function(a) { return a.assetName || '-'; }).join('、');
m.bindPopup(
'<b>资产编码:' + codes + '</b><br>资产名称:' + names + '<br>坐落位置:' + (r.address || '-') +
'<br>地点:' + (r.owner || '-') + '<br>资产面积:' + totalArea + '㎡<br>管理单位:' + (r.manager || '-'),
{ offset: [0, -24] }
).openPopup();
});
m.on("mouseout", function() { m.closePopup(); });
}
self.markerGroup.addLayer(m);
});
},
searchLocation() {
if (!this.searchKeyword) return;
this.filterOwner = ''; this.filterLevel1 = ''; this.filterLevel2 = ''; this.filterLevel3 = '';
this.level2Options = {}; this.level3Options = {};
this.regions = []; this.loadedKeys.clear(); this.filteredCount = 0; this.markerGroup.clearLayers();
fetch(this.host + "/seeyon/rest/assetsRegion/search?keyword=" + encodeURIComponent(this.searchKeyword))
.then(function(r) { return r.json(); }).then(function(d) { if (d.code === 0) this.searchResults = Array.isArray(d.data) ? d.data : [d.data]; }.bind(this));
},
selectSearchResult(item) { this.map.setView([item.lat, item.lng], 18); this.searchResults = []; },
loadAssetById(assetId) {
var self = this;
fetch(this.host + "/seeyon/rest/assetsRegion/getByBounds", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ assetId: assetId })
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.code === 0 && d.data && d.data.length > 0) {
var region = d.data[0];
var lat = parseFloat(region.lat), lng = parseFloat(region.lng);
if (!isNaN(lat) && !isNaN(lng)) {
self.map.setView([lat, lng], 18);
self.regions = d.data;
self.filteredCount = region.assets ? region.assets.length : 0;
self.renderMarkers();
self.showAssetPanel(region);
}
}
});
},
showAssetPanel(r) { this.currentRegion = r; this.panelVisible = true; },
closePanel() {
this.panelVisible = false; this.polygonLayer.clearLayers();
if (this.activeMarker) { this.unhighlightMarker(this.activeMarker); this.activeMarker = null; }
},
goToDetail(item) {
var host = this.host;
fetch(this.host + "/seeyon/rest/assetsRegion/getDetailBaseUrlByAssetsType?assetsType=" + item.assetType)
.then(function(r) { return r.json(); }).then(function(d) {
if (d.code === 0 && d.data) { var url = d.data.replace(/\{assetsId\}/g, item.id); window.open(host + url); }
});
}
}
});
</script>
</body>
</html>