diff --git a/src/static/map.html b/src/static/map.html index 3d5a573..b69b7c0 100644 --- a/src/static/map.html +++ b/src/static/map.html @@ -32,8 +32,10 @@ height: 100%; z-index: 1000; display: flex; - pointer-events: none; /* 允许点击穿透到地图 */ - align-items: flex-start; /* 顶部对齐 */ + pointer-events: none; + /* 允许点击穿透到地图 */ + align-items: flex-start; + /* 顶部对齐 */ } .model-preview-tree { @@ -48,25 +50,26 @@ overflow-y: auto; overflow-x: hidden; padding: 20px; - pointer-events: auto; /* 列表区域可以交互 */ + pointer-events: auto; + /* 列表区域可以交互 */ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3); } - + /* 自定义滚动条样式 */ .model-preview-tree::-webkit-scrollbar { width: 6px; } - + .model-preview-tree::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.1); border-radius: 3px; } - + .model-preview-tree::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.3); border-radius: 3px; } - + .model-preview-tree::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.5); } @@ -83,7 +86,7 @@ .tree-node { margin-bottom: 4px; } - + .tree-node-item { display: flex; align-items: center; @@ -144,7 +147,8 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); transition: all 0.2s; z-index: 1001; - pointer-events: auto; /* 关闭按钮可以点击 */ + pointer-events: auto; + /* 关闭按钮可以点击 */ } .model-preview-close:hover { @@ -170,10 +174,10 @@ .action-menu { background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%); border-radius: 16px; - box-shadow: 0 8px 32px rgba(0, 45, 182, 0.12), - 0 4px 16px rgba(0, 0, 0, 0.08), - 0 2px 8px rgba(0, 0, 0, 0.04); - width: 100%; + box-shadow: 0 8px 32px rgba(0, 45, 182, 0.12), + 0 4px 16px rgba(0, 0, 0, 0.08), + 0 2px 8px rgba(0, 0, 0, 0.04); + width: 68% !important; height: 100%; overflow: hidden; border: 1px solid rgba(0, 45, 182, 0.08); @@ -257,6 +261,12 @@ .action-menu-item:hover span { transform: scale(1.15) rotate(5deg); } + + .BMap_bubble_center { + top: -8px !important; + left: 6px !important; + width: 100px !important; + } @@ -289,11 +299,11 @@ function parseUrlParams(queryString) { const params = {}; if (!queryString) return params; - + // 移除开头的 ? const cleanQuery = queryString.startsWith('?') ? queryString.substring(1) : queryString; if (!cleanQuery) return params; - + const pairs = cleanQuery.split('&'); for (let i = 0; i < pairs.length; i++) { const pair = pairs[i].split('='); @@ -332,11 +342,11 @@ const modelListParam = urlParams['modelList'] || null; const actionParam = urlParams['action'] || null; const clickedProjectParam = urlParams['clickedProject'] || null; - + // 获取 API 配置信息(从 URL 参数中获取) apiBaseUrl = urlParams['apiBaseUrl'] || '/api'; apiToken = urlParams['token'] || ''; - + // 如果 URL 中没有 token,尝试从 localStorage 获取(备用方案) if (!apiToken) { try { @@ -345,7 +355,7 @@ // localStorage 可能不可用 } } - + // 保存点击的项目信息 if (clickedProjectParam) { try { @@ -357,14 +367,14 @@ // 先初始化地图 initMap(projectInfo) - + // 如果 URL 中有模型预览请求(通过 URL 参数触发的情况) if (actionParam === 'modelPreview' && clickedProjectParam) { // 通过 postMessage 通知 Vue 组件获取模型列表 const triggerModelPreview = () => { try { const projectInfo = JSON.parse(decodeURIComponent(clickedProjectParam)); - + // 尝试多种方式发送消息 const messageData = { data: { @@ -372,13 +382,13 @@ projectInfo: projectInfo } }; - + // 方案1: uni.postMessage if (typeof uni !== 'undefined' && typeof uni.postMessage === 'function') { uni.postMessage(messageData); return; } - + // 方案2: window.parent.postMessage if (window.parent && window.parent !== window) { window.parent.postMessage(messageData, '*'); @@ -388,16 +398,16 @@ console.error('触发模型预览失败:', e); } }; - + // 等待 UniAppJSBridgeReady 后再触发 setTimeout(triggerModelPreview, 500); } - + // 如果 URL 中有模型预览请求(已有模型列表的情况),在页面加载完成后显示 if (actionParam === 'showPreview' && modelListParam) { try { const models = JSON.parse(decodeURIComponent(modelListParam)); - + // 定义一个函数来显示预览 const showPreviewWhenReady = () => { if (map && typeof window.showModelPreview === 'function') { @@ -406,7 +416,7 @@ setTimeout(showPreviewWhenReady, 200); } }; - + // 等待地图初始化完成后再显示预览 setTimeout(showPreviewWhenReady, 500); } catch (e) { @@ -448,7 +458,7 @@ const icon = new BMapGL.Icon('./image/location.png', new BMapGL.Size(36, 36), { anchor: new BMapGL.Size(12, 24), }) - + const marker = new BMapGL.Marker(projectPoint, { icon: icon, }) @@ -502,8 +512,8 @@ `; const infoWindow = new BMapGL.InfoWindow(menuHtml, { - width: 170, - height: 130, + width: 100, + height: 90, title: '', enableMessage: false, offset: new BMapGL.Size(0, -20), @@ -516,17 +526,17 @@ const menuContainer = document.querySelector('.action-menu'); if (menuContainer) { // 处理点击和触摸事件(兼容安卓) - const handleAction = function(e) { + const handleAction = function (e) { // 阻止默认行为和事件冒泡 e.preventDefault(); e.stopPropagation(); - + const target = e.target.closest('.action-menu-item'); if (!target) return; - + const action = target.getAttribute('data-action'); map.closeInfoWindow(); - + if (action === 'survey') { // 勘察 - 原来的逻辑 const surveyMessage = { @@ -535,7 +545,7 @@ projectInfo: projectInfo } }; - + // 尝试多种方式发送消息 try { if (typeof uni !== 'undefined' && typeof uni.postMessage === 'function') { @@ -551,21 +561,21 @@ sendModelPreviewMessage(projectInfo); } }; - + // 同时监听 click 和 touchstart 事件(安卓兼容) menuContainer.addEventListener('click', handleAction); - menuContainer.addEventListener('touchend', function(e) { + menuContainer.addEventListener('touchend', function (e) { // 触摸事件需要立即处理,避免延迟 handleAction(e); }); - + // 防止触摸时触发点击(避免重复触发) let touchStartTime = 0; - menuContainer.addEventListener('touchstart', function(e) { + menuContainer.addEventListener('touchstart', function (e) { touchStartTime = Date.now(); }); - - menuContainer.addEventListener('click', function(e) { + + menuContainer.addEventListener('click', function (e) { // 如果刚刚有触摸事件,忽略点击事件(避免重复) if (Date.now() - touchStartTime < 300) { e.preventDefault(); @@ -585,7 +595,7 @@ projectInfo: projectInfo } }; - + // 方案1: 尝试使用 uni.postMessage(iOS 和部分安卓) let messageSent = false; try { @@ -596,7 +606,7 @@ } catch (error) { // 静默失败,尝试下一个方案 } - + // 方案2: 尝试使用 window.parent.postMessage(安卓备选方案) if (!messageSent) { try { @@ -608,7 +618,7 @@ // 静默失败,尝试下一个方案 } } - + // 方案3: 尝试使用 window.webkit.messageHandlers(iOS WebView) if (!messageSent) { try { @@ -620,18 +630,18 @@ // 静默失败,尝试下一个方案 } } - + // 方案4: 通过 URL 参数方式触发(最可靠的安卓方案,作为最后备选) if (!messageSent) { try { // 获取当前 URL 参数(使用兼容方式) const currentUrlParams = parseUrlParams(window.location.search); const currentProjectInfo = currentUrlParams['projectInfo'] || ''; - + // 构建新的 URL,添加模型预览触发参数 const clickedProjectJson = encodeURIComponent(JSON.stringify(projectInfo)); const timestamp = Date.now(); - + // 构建完整的新 URL(保留 apiBaseUrl 和 token) let newUrl = window.location.pathname; const newParams = []; @@ -647,9 +657,9 @@ newParams.push('token=' + encodeURIComponent(apiToken)); } newParams.push('t=' + timestamp); - + newUrl += '?' + newParams.join('&'); - + // 使用 location.href 触发页面重新加载 // 注意:这会触发页面重新加载,但可以通过 URL 参数恢复状态 window.location.href = newUrl; @@ -681,16 +691,15 @@ // 递归构建树形结构(参考 test.vue 的 onBuildTree) function buildTree(data) { - console.log('data',JSON.stringify(data)); - + // 获取所有节点的 id 集合,用于判断是否为顶级节点 const allIds = new Set(data.map(item => item.id)); - + // 筛选顶级节点:nodelevel为"2" 且 parentId 不在数据id列表中(parentId 是项目ID) const topNodes = data.filter((item) => { return item.nodelevel == '2' && !allIds.has(item.parentId); }); - + return topNodes.map((node) => ({ ...node, expanded: true, // 默认展开 @@ -731,25 +740,25 @@ // 根据层级设置缩进 nodeDiv.style.paddingLeft = (level * 20) + 'px'; nodeDiv.style.marginLeft = '0'; - + const itemDiv = document.createElement('div'); itemDiv.className = 'tree-node-item'; - + // 判断是否为末级节点(使用节点的 isLeaf 属性,如果没有则计算) const isLeaf = node.isLeaf !== undefined ? node.isLeaf : (Number(node.nodelevel) === Number(node.nodeCount)); const hasChildren = node.children && node.children.length > 0; - + // 所有节点都显示复选框 const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'tree-node-checkbox'; checkbox.checked = checkedNodeIds.includes(node.id); checkbox.dataset.nodeId = node.id; - checkbox.addEventListener('change', function(e) { + checkbox.addEventListener('change', function (e) { handleNodeCheckChange(node, e.target.checked); }); itemDiv.appendChild(checkbox); - + // 如果有子节点,显示展开/折叠图标 if (hasChildren) { const expandIcon = document.createElement('span'); @@ -763,7 +772,7 @@ expandIcon.style.display = 'inline-block'; expandIcon.style.fontSize = '10px'; expandIcon.style.color = '#fff'; - expandIcon.addEventListener('click', function(e) { + expandIcon.addEventListener('click', function (e) { e.stopPropagation(); // 切换展开状态 const currentExpanded = node.expanded !== undefined ? node.expanded : true; @@ -776,28 +785,28 @@ }); itemDiv.insertBefore(expandIcon, checkbox); } - + const label = document.createElement('span'); label.className = 'tree-node-label'; label.textContent = node.nodeName || node.name || `节点 ${node.id}`; itemDiv.appendChild(label); - + nodeDiv.appendChild(itemDiv); - + // 如果有子节点,递归渲染 if (node.children && node.children.length > 0) { const childrenContainer = document.createElement('div'); childrenContainer.className = 'tree-children'; // 默认展开(expanded 默认为 true) childrenContainer.style.display = (node.expanded !== false) ? 'block' : 'none'; - + node.children.forEach(child => { renderTreeNode(child, childrenContainer, level + 1); }); - + nodeDiv.appendChild(childrenContainer); } - + container.appendChild(nodeDiv); } @@ -805,7 +814,7 @@ function handleNodeCheckChange(node, checked) { // 递归获取当前节点下所有子节点ID const allChildIds = getAllChildNodeIds(node); - + // 更新业务数组 if (checked) { allChildIds.forEach(id => { @@ -816,10 +825,10 @@ } else { checkedNodeIds = checkedNodeIds.filter(id => !allChildIds.includes(id)); } - + // 更新所有复选框状态 updateCheckboxStates(); - + // 加载模型数据 loadBatchCadData(); } @@ -837,17 +846,17 @@ async function loadBatchCadData() { // 清空旧模型覆盖物 clearModelOverlays(); - + if (checkedNodeIds.length === 0) { return; } - + // 检查必要的配置 if (!apiBaseUrl) { console.error('API baseURL 未配置'); return; } - + try { // 构建 API URL(确保 URL 格式正确) let apiUrl = apiBaseUrl; @@ -855,7 +864,7 @@ apiUrl += '/'; } apiUrl += 'model/openView'; - + // 如果 apiBaseUrl 是相对路径,需要转换为绝对路径 if (!apiUrl.startsWith('http')) { // 相对路径,使用当前域名 @@ -863,7 +872,7 @@ const host = window.location.host; apiUrl = protocol + '//' + host + (apiUrl.startsWith('/') ? '' : '/') + apiUrl.replace(/^\//, ''); } - + // 批量请求模型数据 const promises = checkedNodeIds.map(nodeId => { // 使用 XMLHttpRequest 作为 fetch 的备选方案(更好的兼容性) @@ -875,8 +884,8 @@ xhr.setRequestHeader('Authorization', apiToken); xhr.setRequestHeader('Token', apiToken); } - - xhr.onreadystatechange = function() { + + xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { try { @@ -897,19 +906,19 @@ } } }; - - xhr.onerror = function() { + + xhr.onerror = function () { console.error('请求错误 (nodeId: ' + nodeId + ')'); resolve([]); }; - + xhr.send(JSON.stringify({ id: nodeId })); }); }); - + // 等待所有请求完成 const results = await Promise.all(promises); - + // 合并所有模型数据 const modelData = results.reduce((total, data) => { if (Array.isArray(data)) { @@ -917,7 +926,7 @@ } return total; }, []); - + // 绘制模型 if (modelData.length > 0) { drawModel(modelData); @@ -928,21 +937,21 @@ } // 绘制模型覆盖物(参考 test.vue 的 drawModel)- 暴露到全局 - window.drawModel = function(modelInfoList) { + window.drawModel = function (modelInfoList) { // 清除现有模型覆盖物 clearModelOverlays(); - + if (!modelInfoList || modelInfoList.length === 0) { return; } - + for (const item of modelInfoList) { try { // 处理 LWPOLYLINE 数据结构 if (item.entityType === 'LWPOLYLINE') { const pointList = JSON.parse(item.geometry); const newPointList = pointList?.segments; - + if (newPointList) { newPointList.forEach((segment) => { if (segment.type === 'line') { @@ -994,7 +1003,7 @@ const geometry = JSON.parse(item.geometry); const start = geometry.start; const end = geometry.end; - + const polyline = new BMapGL.Polyline( [new BMapGL.Point(start[0], start[1]), new BMapGL.Point(end[0], end[1])], { strokeColor: 'red', strokeWeight: 2, strokeOpacity: 0.8 } @@ -1005,7 +1014,7 @@ const geometry = JSON.parse(item.geometry); const center = geometry.center; const radius = geometry.radius; - + if (center && center.length > 0) { const circle = new BMapGL.Circle( new BMapGL.Point(center[0], center[1]), @@ -1013,7 +1022,6 @@ { strokeColor: 'blue', strokeWeight: 2, - fillColor: 'rgba(0,0,255,0.3)', } ); map.addOverlay(circle); @@ -1022,7 +1030,7 @@ } else { // 处理多边形或其他图形 const pointList = JSON.parse(item.geometry); - + if (pointList && pointList.points && pointList.points.length > 0) { const path = pointList.points.map((p) => { const [lng, lat, , , angle] = p; @@ -1031,7 +1039,7 @@ angle: angle || 0 }; }); - + let overlay = null; if (path.length === 1) { const { point, angle } = path[0]; @@ -1050,12 +1058,12 @@ } else if (path.length === 2) { const linePoints = path.map((p) => p.point); const endAngle = (path[1].angle * 180) / Math.PI; - + overlay = new BMapGL.Polyline(linePoints, { strokeColor: 'green', strokeWeight: 2, }); - + const arrowMarker = new BMapGL.Marker(linePoints[1], { icon: new BMapGL.Icon( '//api.map.baidu.com/img/markers.png', @@ -1071,10 +1079,9 @@ overlay = new BMapGL.Polygon(polygonPoints, { strokeColor: '#3388ff', strokeWeight: 2, - fillColor: 'rgba(51,136,255,0.2)', }); } - + if (overlay) { map.addOverlay(overlay); modelOverlays.push(overlay); @@ -1085,7 +1092,7 @@ console.error('绘制图元失败:', item, error); } } - + // 调整地图视图以显示所有覆盖物 const points = modelOverlays .map((overlay) => { @@ -1099,7 +1106,7 @@ }) .flat() .filter(Boolean); - + if (points.length > 0) { const adjustedZoom = Math.min(16, map.getMaxZoom()); map.setViewport(points, { @@ -1123,12 +1130,12 @@ } // 显示模型预览面板(暴露到全局作用域) - window.showModelPreview = function(models, clickedProject) { + window.showModelPreview = function (models, clickedProject) { // 如果传入了点击的项目信息,保存它 if (clickedProject) { currentClickedProject = clickedProject; } - + if (!map) { // 如果地图未初始化,等待一下再试 setTimeout(() => { @@ -1140,32 +1147,32 @@ }, 500); return; } - + modelList = models || []; const panel = document.getElementById('model-preview-panel'); const treeContainer = document.getElementById('tree-container'); - + if (!panel) { console.error('找不到预览面板元素 #model-preview-panel'); return; } - + if (!treeContainer) { console.error('找不到树容器元素 #tree-container'); return; } - + // 清空所有覆盖物和模型覆盖物 clearAllOverlays(); clearModelOverlays(); - + // 清空已勾选节点 checkedNodeIds = []; - + // 加载项目树形数据(参考 test.vue 的 loadProjectTree) // 检查数据是否为平铺结构(没有 children 属性) const hasChildren = modelList.some(item => item.children && Array.isArray(item.children) && item.children.length > 0); - + let treeData; if (hasChildren) { // 如果已经是树形结构,直接使用 @@ -1174,7 +1181,7 @@ // 如果是平铺数据,转换为树形结构 treeData = buildTree(modelList); } - + // 为每个节点添加 isLeaf 属性和 expanded 属性(参考 test.vue 的 loadProjectTree) function processNode(node) { const isLeaf = Number(node.nodelevel) === Number(node.nodeCount); @@ -1182,14 +1189,14 @@ ...node, isLeaf: isLeaf, expanded: node.expanded !== undefined ? node.expanded : true, // 默认展开 - children: node.children && node.children.length > 0 + children: node.children && node.children.length > 0 ? node.children.map(child => processNode(child)) : [] }; } - + projectTreeData = treeData.map(node => processNode(node)); - + // 渲染树形结构 treeContainer.innerHTML = ''; if (projectTreeData.length === 0) { @@ -1199,18 +1206,18 @@ renderTreeNode(node, treeContainer, 0); }); } - + // 显示面板 panel.style.display = 'flex'; }; // 带数据的预览函数(用于直接调用) - window.showModelPreviewWithData = function(models, clickedProject) { + window.showModelPreviewWithData = function (models, clickedProject) { window.showModelPreview(models, clickedProject); }; // 监听 postMessage 消息 - window.addEventListener('message', function(event) { + window.addEventListener('message', function (event) { if (event.data && event.data.type === 'showModelPreview') { window.showModelPreview(event.data.modelList); } @@ -1220,19 +1227,19 @@ function closeModelPreview() { const panel = document.getElementById('model-preview-panel'); panel.style.display = 'none'; - + // 清除模型覆盖物 clearModelOverlays(); - + // 清空已勾选的节点 checkedNodeIds = []; - + // 更新所有复选框状态 updateCheckboxStates(); - + // 恢复所有覆盖物 restoreAllOverlays(); - + // 定位到点击的坐标 if (currentClickedProject && currentClickedProject.lng && currentClickedProject.lat) { const point = new BMapGL.Point(currentClickedProject.lng, currentClickedProject.lat); @@ -1245,7 +1252,7 @@ // 监听来自父组件的消息(用于接收模型列表) // 注意:uni-app 的 web-view 消息传递机制 - window.addEventListener('message', function(event) { + window.addEventListener('message', function (event) { if (event.data && event.data.type === 'showModelPreview') { const models = event.data.modelList; const clickedProject = event.data.clickedProject; @@ -1255,7 +1262,7 @@ // 也监听 uni 的消息(如果支持) if (typeof uni !== 'undefined' && uni.on) { - uni.on('modelPreviewData', function(data) { + uni.on('modelPreviewData', function (data) { if (data && data.modelList) { window.showModelPreview(data.modelList, data.clickedProject); } @@ -1263,7 +1270,7 @@ } // 监听来自 Vue 组件的模型数据响应 - window.addEventListener('message', function(event) { + window.addEventListener('message', function (event) { // 处理模型数据响应 if (event.data && event.data.action === 'modelDataResponse') { const modelData = event.data.modelData || [];