1070 lines
34 KiB
HTML
1070 lines
34 KiB
HTML
<!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>
|