609 lines
14 KiB
HTML
609 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>资产地图(聚合版)</title>
|
||
|
||
<!-- Leaflet -->
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
|
||
<!-- MarkerCluster -->
|
||
<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" />
|
||
|
||
<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://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||
|
||
<style>
|
||
body {
|
||
margin: 0;
|
||
font-family: Arial, Helvetica, 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, 0.95);
|
||
padding: 10px 20px;
|
||
border-radius: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
|
||
.statItem {
|
||
font-size: 14px;
|
||
color: #666;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.statNumber {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
margin-left: 6px;
|
||
}
|
||
|
||
/* 不同颜色 */
|
||
.statNumber.total {
|
||
color: black;
|
||
}
|
||
|
||
.statNumber.rented {
|
||
color: black;
|
||
}
|
||
|
||
.statNumber.vacant {
|
||
color: black;
|
||
}
|
||
|
||
/* 分隔线 */
|
||
.divider {
|
||
width: 1px;
|
||
height: 16px;
|
||
background: #eee;
|
||
margin: 0 12px;
|
||
}
|
||
|
||
/* 搜索 */
|
||
.searchInput {
|
||
margin-left: 20px;
|
||
padding: 6px 10px;
|
||
border-radius: 8px;
|
||
border: 1px solid #ddd;
|
||
width: 280px; /* 这里加长了 */
|
||
}
|
||
|
||
.searchBtn {
|
||
margin-left: 8px;
|
||
padding: 6px 12px;
|
||
background: #27ae60;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 搜索结果 */
|
||
.searchPanel {
|
||
position: absolute;
|
||
top: 65px;
|
||
left: 50%;
|
||
transform: translateX(5%);
|
||
width: 280px;
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
||
z-index: 9999;
|
||
}
|
||
|
||
.searchItem {
|
||
padding: 10px;
|
||
border-bottom: 1px solid #eee;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.searchItem:hover {
|
||
background: #f5f5f5
|
||
}
|
||
|
||
/* 右侧面板 */
|
||
#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: 15px;
|
||
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: 10px 15px;
|
||
font-size: 13px;
|
||
color: #666;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.assetList {
|
||
flex: 1;
|
||
overflow: auto;
|
||
}
|
||
|
||
.assetItem {
|
||
cursor: pointer;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.marker-cluster-small {
|
||
background-color: rgba(231, 76, 60, 0.6) !important;
|
||
}
|
||
|
||
.marker-cluster-small div {
|
||
background-color: rgba(231, 76, 60, 0.8) !important;
|
||
}
|
||
|
||
|
||
.marker-cluster-medium {
|
||
background-color: rgba(231, 76, 60, 0.6) !important;
|
||
}
|
||
|
||
.marker-cluster-medium div {
|
||
background-color: rgba(231, 76, 60, 0.8) !important;
|
||
}
|
||
|
||
|
||
.marker-cluster-large {
|
||
background-color: rgba(192, 57, 43, 0.6) !important;
|
||
}
|
||
|
||
.marker-cluster-large div {
|
||
background-color: rgba(192, 57, 43, 0.85) !important;
|
||
}
|
||
|
||
.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;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.marker-cluster-custom .cluster-inner span {
|
||
color: #fff;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.assetTenant,
|
||
.assetManager,
|
||
.assetOwner,
|
||
.assetStatus,
|
||
.assetPurpose,
|
||
.assetAddress,
|
||
.assetRemark,
|
||
.assetArea {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin-top: 3px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<div id="app">
|
||
|
||
<!-- 顶部统计 -->
|
||
|
||
<!-- 顶部 -->
|
||
<div id="topStats">
|
||
<div class="statInline">
|
||
|
||
<div class="statItem">
|
||
<span class="statNumber total">总资产: {{allAssetsCount}}</span>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<div class="statItem">
|
||
<span class="statNumber rented">已出租: {{rentedCount}}</span>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
<div class="statItem">
|
||
<span class="statNumber vacant">未出租: {{vacantCount}}</span>
|
||
</div>
|
||
|
||
<!-- 搜索 -->
|
||
<input v-model="searchKeyword" @keyup.enter="searchLocation" class="searchInput" placeholder="输入详细地址">
|
||
|
||
<button @click="searchLocation" class="searchBtn">搜索</button>
|
||
|
||
</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 ? currentRegion.assets.length : 0}}
|
||
</div>
|
||
|
||
<div class="assetList">
|
||
<div class="assetItem" v-for="(a,index) in currentRegion.assets" :key="index" @click="goToDetail(a)">
|
||
<div class="assetManager">运营单位:{{a.manager}}</div>
|
||
<div class="assetOwner">产权单位:{{a.owner}}</div>
|
||
<div class="assetPurpose">用途:{{a.purpose}}</div>
|
||
<div class="assetAddress">地址:{{a.address}}</div>
|
||
<div class="assetStatus">资产状态:{{a.status || 未知}}</div>
|
||
<div class="assetArea">面积:{{a.area}}㎡</div>
|
||
<div class="assetRemark">备注:{{a.remark || '暂无'}}</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
new Vue({
|
||
|
||
el: "#app",
|
||
|
||
data: {
|
||
host: 'http://' + 'www.ycgongxiao.xyz:52100',
|
||
searchKeyword: "",
|
||
searchResults: [],
|
||
searchMarker: null,
|
||
map: null,
|
||
markerGroup: null,
|
||
regions: [],
|
||
allAssetsCount: 0,
|
||
rentedCount: 0,
|
||
vacantCount: 0,
|
||
panelVisible: false,
|
||
currentRegion: {}
|
||
},
|
||
|
||
mounted() {
|
||
this.initMap()
|
||
this.queryAssetsRegionsInfo()
|
||
this.countAllAssets()
|
||
this.countRentedAssets()
|
||
this.countRestAssets()
|
||
},
|
||
watch: {
|
||
// 输入清空自动隐藏
|
||
searchKeyword(val) {
|
||
if (!val) {
|
||
this.searchResults = []
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
|
||
// 🗺️ 初始化地图
|
||
initMap() {
|
||
|
||
this.map = L.map('map').setView([30.630608, 111.375863], 10)
|
||
|
||
L.tileLayer('./map/{z}/{x}/{y}/tile.png', {
|
||
maxZoom: 18,
|
||
minZoom: 8
|
||
}).addTo(this.map)
|
||
|
||
// ⭐ 核心:自定义聚合
|
||
this.markerGroup = L.markerClusterGroup({
|
||
|
||
iconCreateFunction: (cluster) => {
|
||
|
||
let total = 0
|
||
|
||
let markers = cluster.getAllChildMarkers()
|
||
|
||
markers.forEach(m => {
|
||
let region = m.options.regionData
|
||
total += (region && region.assets ? region.assets.length : 0)
|
||
})
|
||
|
||
return L.divIcon({
|
||
html: `
|
||
<div class="cluster-inner">
|
||
<span>${total}</span>
|
||
</div>
|
||
`,
|
||
className: 'marker-cluster-custom',
|
||
iconSize: L.point(40, 40)
|
||
})
|
||
}
|
||
|
||
})
|
||
|
||
this.map.addLayer(this.markerGroup)
|
||
|
||
// 点击地图关闭弹窗
|
||
// 点击地图空白关闭面板
|
||
this.map.on('click', () => {
|
||
this.searchResults = []
|
||
this.panelVisible = false
|
||
})
|
||
},
|
||
countAllAssets() {
|
||
fetch(this.host + "/seeyon/rest/assetsRegion/countAssets", {
|
||
method: 'GET'
|
||
})
|
||
.then(res => res.json())
|
||
.then(res => {
|
||
|
||
if (res.code !== 0) {
|
||
console.error("接口错误", res.message)
|
||
return
|
||
}
|
||
|
||
this.allAssetsCount = res.data || 0
|
||
})
|
||
.catch(err => {
|
||
console.error("加载失败", err)
|
||
})
|
||
},
|
||
countRentedAssets() {
|
||
fetch(this.host + "/seeyon/rest/assetsRegion/countAssets?type=1", {
|
||
method: 'GET'
|
||
})
|
||
.then(res => res.json())
|
||
.then(res => {
|
||
|
||
if (res.code !== 0) {
|
||
console.error("接口错误", res.message)
|
||
return
|
||
}
|
||
|
||
this.rentedCount = res.data || 0
|
||
})
|
||
.catch(err => {
|
||
console.error("加载失败", err)
|
||
})
|
||
},
|
||
countRestAssets() {
|
||
fetch(this.host + "/seeyon/rest/assetsRegion/countAssets?type=2", {
|
||
method: 'GET'
|
||
}).then(res => res.json())
|
||
.then(res => {
|
||
|
||
if (res.code !== 0) {
|
||
console.error("接口错误", res.message)
|
||
return
|
||
}
|
||
|
||
this.vacantCount = res.data || 0
|
||
})
|
||
.catch(err => {
|
||
console.error("加载失败", err)
|
||
})
|
||
},
|
||
// 📡 请求数据
|
||
queryAssetsRegionsInfo() {
|
||
|
||
fetch(this.host + "/seeyon/rest/assetsRegion/getall", {
|
||
method: 'POST'
|
||
})
|
||
.then(res => res.json())
|
||
.then(res => {
|
||
|
||
if (res.code !== 0) {
|
||
console.error("接口错误", res.message)
|
||
return
|
||
}
|
||
|
||
this.regions = res.data || []
|
||
this.renderMarkers()
|
||
|
||
})
|
||
.catch(err => {
|
||
console.error("加载失败", err)
|
||
})
|
||
},
|
||
|
||
// 📊 面积计算
|
||
calcArea(list) {
|
||
let sum = 0
|
||
list.forEach(a => {
|
||
sum += Number(a.area || 0)
|
||
})
|
||
return sum
|
||
},
|
||
|
||
// 📍 渲染 marker(核心)
|
||
renderMarkers() {
|
||
|
||
// 清空旧数据
|
||
this.markerGroup.clearLayers()
|
||
|
||
this.regions.forEach(r => {
|
||
|
||
let marker = L.marker([r.lat, r.lng])
|
||
|
||
marker.options.regionData = r
|
||
|
||
// 点击打开右侧面板
|
||
marker.on("click", () => {
|
||
this.showAssetPanel(r)
|
||
})
|
||
|
||
// hover提示
|
||
marker.on("mouseover", () => {
|
||
|
||
let area = this.calcArea(r.assets || [])
|
||
let owner = r.owner || ""
|
||
let manager = r.manager || ""
|
||
let count = (r.assets || []).length
|
||
|
||
let html =
|
||
"<b>位置:" + r.address + "</b><br>" +
|
||
"运营单位:" + manager + "<br>" +
|
||
"产权单位:" + owner + "<br>" +
|
||
"面积:" + area + "㎡<br>" +
|
||
"资产数量:" + count
|
||
|
||
marker.bindPopup(html).openPopup()
|
||
})
|
||
|
||
marker.on("mouseout", () => {
|
||
marker.closePopup()
|
||
})
|
||
|
||
// ⭐ 加入聚合组
|
||
this.markerGroup.addLayer(marker)
|
||
})
|
||
},
|
||
// 搜索
|
||
searchLocation() {
|
||
|
||
if (!this.searchKeyword) return
|
||
|
||
fetch(this.host + "/seeyon/rest/assetsRegion/search", {
|
||
method: 'POST',
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify({
|
||
keyword: this.searchKeyword
|
||
})
|
||
})
|
||
.then(res => res.json())
|
||
.then(res => {
|
||
if (res.code !== 0) return
|
||
|
||
let list = res.data || []
|
||
|
||
if (!Array.isArray(list)) {
|
||
list = [list]
|
||
}
|
||
|
||
this.searchResults = list
|
||
// this.selectSearchResult(list[0])
|
||
})
|
||
},
|
||
goToDetail(item) {
|
||
let id = item.id
|
||
let baseUrl =
|
||
`http://www.ycgongxiao.xyz:52100/seeyon/cap4/businessTemplateController.do?v=V10_0SP1_251022_222110_0&method=formContent&type=browse&rightId=-51300951786173064.-7277875919159408408&moduleId=${id}&formTemplateId=3171356881354704902&viewConditionId=&conditionIdDeal=&columnId=3171356881354704902&moduleType=42&isUnFlowDrawer=true&needUpdate=1&tplId=850010&btnId=&comeFrom=list&_v=1776323480999`
|
||
window.open(baseUrl)
|
||
},
|
||
// 选择搜索结果
|
||
selectSearchResult(item) {
|
||
|
||
// 定位
|
||
this.map.setView([item.lat, item.lng], 18);
|
||
|
||
// 移除之前的搜索标记
|
||
if (this.searchMarker) {
|
||
this.markerGroup.removeLayer(this.searchMarker);
|
||
}
|
||
|
||
// ==============================================
|
||
// ✅ 最佳方案:找到原有的那个 marker,直接高亮/打开弹窗
|
||
// ==============================================
|
||
let targetMarker = null;
|
||
|
||
// 遍历聚合组里所有原有 marker,匹配数据
|
||
this.markerGroup.eachLayer((layer) => {
|
||
if (layer.regionData && layer.regionData.id === item.id) {
|
||
targetMarker = layer;
|
||
}
|
||
});
|
||
// 直接打开原有 marker 的弹窗(不新增任何标记)
|
||
if (targetMarker) {
|
||
targetMarker.fire('mouseover');
|
||
return;
|
||
}
|
||
// 如果没找到(新地点),再创建一个底层标记
|
||
// this.searchMarker = L.marker([item.lat, item.lng], {
|
||
// zIndexOffset: -1000,
|
||
// }).addTo(this.markerGroup);
|
||
this.searchResults = []
|
||
},
|
||
// 📌 打开面板
|
||
showAssetPanel(region) {
|
||
this.currentRegion = region
|
||
this.panelVisible = true
|
||
},
|
||
|
||
// ❌ 关闭面板
|
||
closePanel() {
|
||
this.panelVisible = false
|
||
}
|
||
}
|
||
|
||
})
|
||
</script>
|
||
|
||
</body>
|
||
</html> |