GIS地图优化

This commit is contained in:
cwchen 2026-01-13 15:01:19 +08:00
parent 76851a6059
commit 2764edffdb
1 changed files with 126 additions and 119 deletions

View File

@ -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;
}
</style>
</head>
@ -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.postMessageiOS 和部分安卓)
let messageSent = false;
try {
@ -596,7 +606,7 @@
} catch (error) {
// 静默失败,尝试下一个方案
}
// 方案2: 尝试使用 window.parent.postMessage安卓备选方案
if (!messageSent) {
try {
@ -608,7 +618,7 @@
// 静默失败,尝试下一个方案
}
}
// 方案3: 尝试使用 window.webkit.messageHandlersiOS 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 || [];