+
+ 筛选条件
+
+
+
+
+
+
+
+
+
+ {{ node.name }}
+ ▶
+ ✓
+
+
+
+
+
+ {{ node.name }}
+ ▶
+ ✓
+
+
+
+
+
-
-
-
-
-
已出租: {{rentedCount}}
+
+ {{ filterOwner || '点击选择单位...' }}
-
-
-
- 未出租: {{vacantCount}}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- {{item.assetName || item.address}}
-
-
-
-
-
-
-
-
-
-
-
- 资产数量:{{currentRegion.assets ? currentRegion.assets.length : 0}}
-
-
-
-
-
运营单位:{{a.manager}}
-
产权单位:{{a.owner}}
-
用途:{{a.purpose}}
-
地址:{{a.address}}
-
资产状态:{{a.status || 未知}}
-
面积:{{a.area}}㎡
-
-
-
-
-
-
-
+ // =====================================================================
+ // 筛选选项加载
+ // =====================================================================
-
-
\ No newline at end of file
+ /**
+ * 并行加载筛选选项(所属单位 + 资产分类)
+ * @returns {Promise} 等待两个接口都完成
+ */
+ loadFilterOptions() {
+ var self = this;
+ return Promise.all([
+ // 加载部门树
+ fetch(this.host + "/seeyon/rest/assetsRegion/getDeptOptions")
+ .then(r => r.json()).then(d => {
+ if (d.code === 0) {
+ self.deptTree = d.data || [];
+ self.deptOptions = self.flattenDeptTree(self.deptTree);
+ }
+ }),
+ // 加载资产分类
+ fetch(this.host + "/seeyon/rest/assetsRegion/getCategoryOptions")
+ .then(r => r.json()).then(d => { if (d.code === 0) self.categoryOptions = d.data || {}; })
+ ]);
+ },
+
+ /**
+ * 将树形部门数据扁平化为 {name: id} 映射
+ * 用于筛选下拉框的选项值
+ * @param {Array} tree - 树形数据
+ * @returns {Object} 扁平化映射
+ */
+ 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;
+ },
+
+ // =====================================================================
+ // 级联选择器交互
+ // =====================================================================
+
+ /**
+ * 级联选择器节点点击
+ * @param {Object} node - 当前点击的节点
+ * @param {number} level - 当前层级(0=根)
+ */
+ cascadeClick(node, level) {
+ this.cascadeActiveId = node.id;
+ if (node.children && node.children.length > 0) {
+ // 有子级:展开下一级列
+ var newChildren = this.cascadeChildren.slice(0, level);
+ newChildren[level] = node.children;
+ this.cascadeChildren = newChildren;
+ } else {
+ // 无子级:选中该单位并关闭选择器
+ this.filterOwner = node.name;
+ this.showCascade = false;
+ }
+ },
+
+ /** 清除级联选择器选中状态 */
+ clearCascadeSelect() {
+ this.filterOwner = '';
+ this.cascadeActiveId = null;
+ this.cascadeChildren = [];
+ },
+
+ // =====================================================================
+ // 筛选与查询
+ // =====================================================================
+
+ /** 重置所有筛选条件并清空地图标点和边界 */
+ resetFilter() {
+ this.filterOwner = ''; this.filterCatagory = ''; this.keyword = '';
+ this.assetList = []; this.queried = false; this.total = 0;
+ this.markerGroup.clearLayers();
+ this.clearAllBoundLayers();
+ this.markerMap = {};
+ this.clearActiveMarker();
+ this.activeAssetId = null;
+ },
+
+ /**
+ * 清除所有marker的边界图层
+ * 遍历 markerBoundLayers 中每个 layerGroup 并从地图移除
+ */
+ clearAllBoundLayers() {
+ var self = this;
+ Object.keys(this.markerBoundLayers).forEach(function(key) {
+ self.map.removeLayer(self.markerBoundLayers[key]);
+ });
+ this.markerBoundLayers = {};
+ },
+
+ /**
+ * 查询资产列表
+ * - 清空现有标点和边界
+ * - 调用 queryAssetList 接口获取分页数据
+ * - 成功后构建标点
+ */
+ queryAssets() {
+ var self = this;
+ this.queried = true;
+ // 清空现有图层
+ this.markerGroup.clearLayers();
+ this.clearAllBoundLayers();
+ this.markerMap = {};
+ this.clearActiveMarker();
+ this.activeAssetId = null;
+
+ // 构建查询参数
+ var body = {
+ owner: this.filterOwner,
+ level3: this.filterCatagory,
+ keyword: this.keyword,
+ pageNum: 1,
+ pageSize: 20
+ };
+
+ fetch(this.host + "/seeyon/rest/assetsRegion/queryAssetList", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body)
+ }).then(r => r.json()).then(d => {
+ if (d.code === 0) {
+ self.assetList = d.data.records || [];
+ self.total = d.data.total || 0;
+ self.buildMarkers();
+ }
+ });
+ },
+
+ // =====================================================================
+ // 标点构建(按经纬度分组)
+ // =====================================================================
+
+ /**
+ * 按经纬度分组构建标点
+ * 核心逻辑:
+ * 1. 将资产按 lat,lng 分组(toFixed(8) 避免浮点精度问题)
+ * 2. 每组创建一个标点,标点上挂载 _assets 数组
+ * 3. 绑定点击事件:选中 + 弹框 + 高亮边界
+ * 4. 自动加载该标点所有资产的边界
+ */
+ buildMarkers() {
+ this.markerGroup.clearLayers();
+ this.markerMap = {};
+ this.markerBoundLayers = {};
+ var self = this;
+
+ // Step 1: 按经纬度分组
+ var groups = {};
+ this.assetList.forEach(function(a) {
+ var lat = parseFloat(a.lat);
+ var lng = parseFloat(a.lng);
+ if (isNaN(lat) || isNaN(lng)) return;
+ var key = lat.toFixed(8) + ',' + lng.toFixed(8);
+ if (!groups[key]) groups[key] = { lat: lat, lng: lng, assets: [] };
+ groups[key].assets.push(a);
+ });
+
+ // Step 2: 为每组创建标点
+ Object.keys(groups).forEach(function(key) {
+ var group = groups[key];
+ var m = L.marker([group.lat, group.lng], { icon: self.markerIcon });
+ m._groupKey = key; // 分组key,用于关联边界图层
+ m._assets = group.assets; // 该位置的所有资产
+ m._activeIdx = 0; // 当前展示的资产索引
+
+ // Step 3: 标点点击事件
+ m.on('click', function() {
+ self.highlightMarker(m);
+ var activeIdx = m._activeIdx || 0;
+ // 注释:切换单资产边界逻辑已屏蔽,改为一次性加载所有资产边界
+ // self.loadMarkerBound(m, activeIdx);
+ self.activeAssetId = m._assets[activeIdx].id;
+ self.highlightBoundForAsset(m._assets[activeIdx].id);
+ self.showAssetPopup(m, m._assets[activeIdx]);
+ self.scrollListToAsset(m._assets[activeIdx].id);
+ // 多资产时额外弹出切换弹框
+ if (m._assets.length > 1) {
+ self.showSwitchPopup(m);
+ }
+ });
+
+ m.addTo(self.markerGroup);
+ self.markerMap[key] = m;
+
+ // Step 4: 自动加载该标点所有资产的边界
+ self.loadAllMarkerBounds(m);
+ });
+ },
+
+ // =====================================================================
+ // 边界加载与高亮
+ // =====================================================================
+
+ /**
+ * 为指定资产的边界添加斜线填充效果
+ * 选中:区域内部填充斜线纹理(SVG pattern),边框加粗
+ * 非选中:恢复默认纯色填充
+ * @param {string} assetId - 要高亮的资产ID
+ */
+ highlightBoundForAsset(assetId) {
+ this._ensureHatchPattern();
+ var self = this;
+ Object.keys(this.markerBoundLayers).forEach(function(key) {
+ var layerGroup = self.markerBoundLayers[key];
+ layerGroup.eachLayer(function(layer) {
+ if (!layer.setStyle) return;
+ if (layer._assetId === assetId) {
+ // 选中:斜线填充 + 加粗边框
+ layer.setStyle({ weight: 3, fillOpacity: 1 });
+ layer.bringToFront();
+ // 用SVG pattern替换填充为斜线纹理
+ if (layer._path) {
+ layer._path.setAttribute('fill', 'url(#hatch)');
+ }
+ } else {
+ // 非选中:恢复默认纯色填充
+ layer.setStyle({ weight: 2, fillOpacity: 0.3 });
+ if (layer._path) {
+ layer._path.setAttribute('fill', layer.options.fillColor || layer.options.color);
+ }
+ }
+ });
+ });
+ },
+
+ /**
+ * 加载标点内所有资产的边界(一次性全部加载)
+ * 每个资产的边界独立请求,渲染到同一个 layerGroup
+ * polygon 通过 _assetId 属性关联到对应资产,用于后续高亮
+ * @param {L.Marker} m - 标点对象,包含 _assets 数组和 _groupKey
+ */
+ loadAllMarkerBounds(m) {
+ var self = this;
+ // 清除该marker旧的边界图层
+ if (self.markerBoundLayers[m._groupKey]) {
+ self.map.removeLayer(self.markerBoundLayers[m._groupKey]);
+ }
+ var layerGroup = L.layerGroup().addTo(self.map);
+ self.markerBoundLayers[m._groupKey] = layerGroup;
+
+ // 遍历每个资产,异步加载边界
+ m._assets.forEach(function(asset) {
+ var assetId = asset.id || asset.assetCode;
+ if (!assetId) return;
+ var url = self.host + "/seeyon/rest/assetsRegion/getAssetBound?assetId=" + encodeURIComponent(assetId);
+ fetch(url)
+ .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 regions = self.normalizeBoundary(bound);
+ var color = bound.color || '#e74c3c';
+ regions.forEach(function(region) {
+ if (!region.outer || region.outer.length < 3) return;
+ // rings: [外圈坐标, ...孔洞坐标]
+ var rings = [region.outer].concat(region.holes || []);
+ var polygon = L.polygon(rings, {
+ color: color, fillColor: color, weight: 2, fillOpacity: 0.3
+ });
+ polygon._assetId = asset.id; // 标记归属资产,用于高亮匹配
+ layerGroup.addLayer(polygon);
+ });
+ }
+ })
+ .catch(function(err) { console.error('[map] 加载边界失败:', err); });
+ });
+ },
+
+ /**
+ * 加载指定marker的单个资产的边界(暂保留,供后续切换边界功能使用)
+ * 当前已屏蔽调用,改为 loadAllMarkerBounds 一次性加载
+ * @param {L.Marker} m - 标点对象
+ * @param {number} assetIdx - 资产在 _assets 数组中的索引
+ */
+ loadMarkerBound(m, assetIdx) {
+ var self = this;
+ var asset = m._assets[assetIdx];
+ if (!asset) { console.warn('[map] asset为空, idx:', assetIdx); return; }
+ var assetId = asset.id || asset.assetCode;
+ if (!assetId) { console.warn('[map] assetId为空:', asset); return; }
+
+ // 清除该marker旧的边界图层
+ if (self.markerBoundLayers[m._groupKey]) {
+ self.map.removeLayer(self.markerBoundLayers[m._groupKey]);
+ }
+ var layerGroup = L.layerGroup().addTo(self.map);
+ self.markerBoundLayers[m._groupKey] = layerGroup;
+ m._activeIdx = assetIdx;
+ self.activeAssetId = asset.id;
+
+ var url = self.host + "/seeyon/rest/assetsRegion/getAssetBound?assetId=" + encodeURIComponent(assetId);
+ console.log('[map] loadMarkerBound, assetId:', assetId, 'assetName:', asset.assetName);
+ fetch(url)
+ .then(function(r) { return r.json(); })
+ .then(function(d) {
+ console.log('[map] getAssetBound返回:', 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;
+ console.log('[map] 解析后的边界:', bound);
+ var regions = self.normalizeBoundary(bound);
+ console.log('[map] regions:', regions.length, regions);
+ var color = bound.color || '#e74c3c';
+ regions.forEach(function(region) {
+ if (!region.outer || region.outer.length < 3) {
+ console.warn('[map] region.outer不足3个点:', region.outer);
+ return;
+ }
+ var rings = [region.outer].concat(region.holes || []);
+ var polygon = L.polygon(rings, {
+ color: color, fillColor: color, weight: 2, fillOpacity: 0.3
+ });
+ polygon._assetId = asset.id;
+ layerGroup.addLayer(polygon);
+ console.log('[map] polygon已添加, 点数:', region.outer.length);
+ });
+ } else {
+ console.warn('[map] 无boundData, code:', d.code, 'data:', d.data);
+ }
+ })
+ .catch(function(err) { console.error('[map] 加载边界失败:', err); });
+ },
+
+ // =====================================================================
+ // 多资产切换弹框
+ // =====================================================================
+
+ /**
+ * 显示资产切换弹框(多资产标点点击时触发)
+ * 弹框内列出该位置所有资产,点击可切换信息弹框和列表高亮
+ * 注:边界已一次性加载,切换时只更新信息和高亮,不重新加载边界
+ * @param {L.Marker} m - 标点对象
+ */
+ showSwitchPopup(m) {
+ var self = this;
+ var activeIdx = m._activeIdx || 0;
+
+ // 构建弹框HTML
+ var html = '';
+
+ // 打开弹框(Leaflet popup)
+ L.popup({ closeButton: true, className: 'switch-popup-container', offset: [0, -24] })
+ .setLatLng(m.getLatLng())
+ .setContent(html)
+ .openOn(self.map);
+
+ // 延迟绑定点击事件(等待DOM渲染完成)
+ setTimeout(function() {
+ var items = document.querySelectorAll('.switch-popup .sp-item');
+ items.forEach(function(item) {
+ item.addEventListener('click', function() {
+ var idx = parseInt(this.getAttribute('data-idx'));
+ // 注释:切换边界逻辑已屏蔽,改为一次性加载所有资产边界
+ // if (idx !== activeIdx) {
+ // self.loadMarkerBound(m, idx);
+ // }
+ m._activeIdx = idx;
+ self.activeAssetId = m._assets[idx].id;
+ self.highlightBoundForAsset(m._assets[idx].id);
+ self.scrollListToAsset(m._assets[idx].id);
+ // showAssetPopup 会替换当前弹框,不需要手动 close
+ self.showAssetPopup(m, m._assets[idx]);
+ });
+ });
+ }, 50);
+ },
+
+ // =====================================================================
+ // 列表交互
+ // =====================================================================
+
+ /**
+ * 从右侧列表点击资产时触发
+ * - 高亮对应标点
+ * - 高亮对应边界
+ * - 显示信息弹框
+ * - 地图定位到该资产位置
+ * @param {Object} asset - 资产对象
+ */
+ locateFromList(asset) {
+ var lat = parseFloat(asset.lat);
+ var lng = parseFloat(asset.lng);
+ if (isNaN(lat) || isNaN(lng)) return;
+
+ this.activeAssetId = asset.id;
+ var key = lat.toFixed(8) + ',' + lng.toFixed(8);
+ var m = this.markerMap[key];
+ if (m) {
+ this.highlightMarker(m);
+ // 找到该资产在marker中的索引
+ var idx = m._assets.findIndex(function(a) { return a.id === asset.id; });
+ if (idx < 0) idx = 0;
+ // 注释:切换边界逻辑已屏蔽
+ // this.loadMarkerBound(m, idx);
+ m._activeIdx = idx;
+ this.highlightBoundForAsset(asset.id);
+ this.showAssetPopup(m, asset);
+ }
+ // 仅平移到资产位置,不改变缩放层级
+ this.map.panTo([lat, lng]);
+ },
+
+ /**
+ * 滚动右侧列表到指定资产项
+ * 使用 $nextTick 确保DOM已更新后再滚动
+ * @param {string} assetId - 资产ID
+ */
+ scrollListToAsset(assetId) {
+ this.$nextTick(function() {
+ var ref = this.$refs['asset-' + assetId];
+ if (ref && ref[0]) {
+ ref[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ });
+ },
+
+ // =====================================================================
+ // 边界数据解析
+ // =====================================================================
+
+ /**
+ * 标准化边界数据格式
+ * 兼容两种格式:
+ * 1. 新格式:{ regions: [{id, outer, holes}], color }
+ * 2. 旧格式:{ polygon: [...], color }(polygon 可能是单层或多层嵌套)
+ * @param {Object} bound - 原始边界数据
+ * @returns {Array} 标准化后的 regions 数组 [{id, outer, holes}]
+ */
+ normalizeBoundary(bound) {
+ if (!bound) return [];
+ var self = this;
+ // 新格式:直接取 regions 数组
+ if (bound.regions && Array.isArray(bound.regions)) {
+ return bound.regions.map(r => ({ id: r.id, outer: r.outer || [], holes: r.holes || [] }));
+ }
+ // 旧格式:从 polygon 字段解析
+ if (!bound.polygon || bound.polygon.length === 0) return [];
+ var p = bound.polygon;
+ // 单个多边形:polygon[0] = [lat, lng]
+ if (typeof p[0][0] === 'number') return [{ id: 0, outer: p, holes: [] }];
+ // 单个带孔洞:polygon[0] = outer, polygon[1..] = holes
+ if (typeof p[0][0][0] === 'number') return [{ id: 0, outer: p[0], holes: p.slice(1) }];
+ // 多个多边形
+ return p.map(function(r, i) {
+ if (typeof r[0][0] === 'number') return { id: i, outer: r, holes: [] };
+ return { id: i, outer: r[0], holes: r.slice(1) };
+ });
+ },
+
+ // =====================================================================
+ // 弹框与标点高亮
+ // =====================================================================
+
+ /**
+ * 显示资产基本信息弹框(在标点位置弹出)
+ * @param {L.Marker} m - 标点对象(用于定位弹框位置)
+ * @param {Object} asset - 资产对象
+ */
+ showAssetPopup(m, asset) {
+ var area = asset.area || '-';
+ var html =
+ '
资产编码:' + (asset.assetCode || asset.id) + '
' +
+ '
资产名称:' + (asset.assetName || '-') + '
' +
+ '
坐落位置:' + (asset.address || '-') + '
' +
+ '
面积:' + area + '㎡
' +
+ '
管理单位:' + (asset.manager || '-');
+ L.popup({ offset: [0, -24] })
+ .setLatLng(m.getLatLng())
+ .setContent(html)
+ .openOn(this.map);
+ },
+
+ /**
+ * 高亮指定标点(红色发光 + 放大效果)
+ * 同时取消上一个高亮标点
+ * @param {L.Marker} m - 要高亮的标点
+ */
+ 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;
+ },
+
+ /**
+ * 取消标点高亮(恢复原始样式)
+ * @param {L.Marker} m - 要取消高亮的标点
+ */
+ unhighlightMarker(m) {
+ var el = m.getElement();
+ if (el) {
+ var img = el.querySelector('img') || el;
+ img.style.filter = '';
+ img.style.transform = '';
+ el.style.zIndex = '';
+ }
+ },
+
+ /** 清除当前活跃标点的高亮状态 */
+ clearActiveMarker() {
+ if (this.activeMarker) {
+ this.unhighlightMarker(this.activeMarker);
+ this.activeMarker = null;
+ }
+ this.clearBoundHighlight();
+ },
+
+ /** 清除所有边界的斜线填充效果,恢复默认纯色填充 */
+ clearBoundHighlight() {
+ var self = this;
+ Object.keys(this.markerBoundLayers).forEach(function(key) {
+ var layerGroup = self.markerBoundLayers[key];
+ layerGroup.eachLayer(function(layer) {
+ if (!layer.setStyle) return;
+ layer.setStyle({ weight: 2, fillOpacity: 0.3 });
+ if (layer._path) {
+ layer._path.setAttribute('fill', layer.options.fillColor || layer.options.color);
+ }
+ });
+ });
+ },
+
+ // =====================================================================
+ // URL穿透定位
+ // =====================================================================
+
+ /**
+ * 根据资产编号(URL参数)定位到指定资产
+ * 流程:调 getByBounds 接口 → 构建标点 → 地图定位 → 自动选中目标资产
+ * @param {string} assetId - 资产编号(来自URL参数)
+ */
+ 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.assetList = region.assets || [];
+ self.total = self.assetList.length;
+ self.queried = true;
+ self.buildMarkers();
+ self.map.setView([lat, lng], 18);
+ // 自动定位到目标资产
+ var target = self.assetList.find(function(a) { return a.assetCode === assetId || a.id === assetId; });
+ if (target) self.locateFromList(target);
+ }
+ }
+ });
+ },
+
+ // =====================================================================
+ // 跳转详情
+ // =====================================================================
+
+ /**
+ * 跳转到资产详情页
+ * 先根据资产类型获取详情页URL模板,再替换资产ID打开新窗口
+ * @param {Object} item - 资产对象
+ */
+ goToDetail(item) {
+ var host = this.host;
+ fetch(this.host + "/seeyon/rest/assetsRegion/getDetailBaseUrlByAssetsType?assetsType=" + item.assetType)
+ .then(r => r.json()).then(d => {
+ if (d.code === 0 && d.data) {
+ window.open(host + d.data.replace(/\{assetsId\}/g, item.id));
+ }
+ });
+ }
+ }
+ });
+
+