SimpleMapTest.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. <template>
  2. <div class="myapp map-container">
  3. <MapCadViewer
  4. ref="viewerRef"
  5. :map-config="mapConfig"
  6. :start-in-cad-mode="START_IN_CAD_MODE"
  7. @mode-change="onModeChange"
  8. @feature-select="onFeatureSelect"
  9. @marker-click="onMarkerClick"
  10. @marker-open-cad="onMarkerOpenCad"
  11. @icon-placed="onIconPlaced"
  12. @icon-moved="onIconMoved"
  13. @device-click="onDeviceClick"
  14. @device-popup-close="onDevicePopupClose"
  15. @icon-deleted="onIconDeleted"
  16. /> </div
  17. ><div v-if="!isTopLevel" class="map-reset-btn">
  18. <Button :loading="mapLoading" type="primary" @click="revertStack">返回上级</Button>
  19. </div>
  20. <!-- 返回上级 -->
  21. <div v-if="!isTopLevel" class="map-reset-btn">
  22. <Button :loading="mapLoading" type="primary" @click="revertStack">返回上级</Button>
  23. </div>
  24. <!-- 省域地图监测区图例(图3-1) -->
  25. <div v-if="!cadOpened" class="map-legend">
  26. <div class="legend-title">图例</div>
  27. <div class="legend-row"><span class="legend-dot legend-dot-red"></span>疑似隐蔽工作面</div>
  28. <div class="legend-row"><span class="legend-dot legend-dot-orange"></span>风量异常</div>
  29. <div class="legend-row"><span class="legend-dot legend-dot-yellow"></span>数据掉线</div>
  30. <div class="legend-row"><span class="legend-dot legend-dot-green"></span>监测正常</div>
  31. </div>
  32. <!-- 矿端通风系统图监测区图例(图3-3) -->
  33. <div v-if="cadOpened" class="map-legend">
  34. <div class="legend-title">图例</div>
  35. <div class="legend-row"><span class="legend-dot legend-dot-green"></span>测风正常</div>
  36. <div class="legend-row"><span class="legend-dot legend-dot-orange"></span>风量异常</div>
  37. <div class="legend-row"><span class="legend-dot legend-dot-red-dashed"></span>缺失测风点</div>
  38. <div class="legend-row"><span class="legend-line-red"></span>疑似隐蔽巷道</div>
  39. </div>
  40. </template>
  41. <script setup lang="ts">
  42. import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
  43. // 本地调试组件时使用
  44. // import 'map-cad-viewer/dist/map-cad-viewer.css';
  45. // import { MapCadViewer } from 'map-cad-viewer';
  46. // import type { CadData, MapConfig, BoundaryConfig, MapMarker } from 'map-cad-viewer/dist/types/index.d';
  47. // 直接加载打包好的组件
  48. // import { MapCadViewer } from '/@/components/map/map-cad-viewer.es';
  49. // import '/@/components/map/map-cad-viewer.css';
  50. // import type { CadData, MapConfig, BoundaryConfig, MapMarker } from '/@/components/map/types/index.d';
  51. import { Button } from 'ant-design-vue';
  52. import { useAppStore } from '/@/store/modules/app';
  53. import { useMineDepartmentStore } from '/@/store/modules/mine';
  54. import { get, last } from 'lodash-es';
  55. // import { getMarkerCollection, getShanxiGeoJSON } from '/@/api/sys/map';
  56. // import { findPath, listToTree } from '/@/utils/helper/treeHelper';
  57. // import { watchTriggerable } from '@vueuse/core';
  58. import { MapCadViewer } from '/@/components/map/map-cad-viewer.es';
  59. import '/@/components/map/map-cad-viewer.css';
  60. import type { CadData, MapConfig, MapMarker, MarkerPopup } from '/@/components/map/types/index.d';
  61. const props = defineProps<{
  62. silence: boolean;
  63. }>();
  64. const appStore = useAppStore();
  65. const mineStore = useMineDepartmentStore();
  66. const mapLoading = ref(false);
  67. // 设置地图中当前的参数以供其他页面使用
  68. appStore.setSimpleMapParams({ deptId: mineStore.getRootId, isLeaf: mineStore.getRoot?.isLeaf });
  69. // CAD是否可见
  70. const cadOpened = ref<boolean>(false);
  71. const GEO_LAYER_ID = 'geojson-layer';
  72. const GEO_BORDER_ID = 'geoline-layer';
  73. const DEFAULT_NODE = {
  74. id: mineStore.getRootId!,
  75. name: '根节点',
  76. parentId: null,
  77. longitude: 109.425873,
  78. latitude: 35.83317,
  79. zoom: 6,
  80. isLeaf: false,
  81. };
  82. const DEFAULT_TREE_CONFIG = {
  83. id: 'id',
  84. pid: 'parentId',
  85. };
  86. const DEFAULT_TREE_CONFIG = {
  87. id: 'id',
  88. pid: 'parentId',
  89. };
  90. // 用于存储历史节点的栈,实现返回上一级功能
  91. const historyStack = ref<any[]>([DEFAULT_NODE]);
  92. const isTopLevel = computed(() => get(historyStack.value, 'length', 1) === 1);
  93. const fileOptions = ref<any[]>([]);
  94. const fileId = ref();
  95. // 设置地图中当前的参数以供其他页面使用
  96. appStore.setSimpleMapParams({ deptId: mineStore.getRootId, isLeaf: mineStore.getRoot?.isLeaf });
  97. const viewerRef = ref<InstanceType<typeof MapCadViewer>>();
  98. // _dwg_req_newcal@
  99. // ===== 启动配置 =====
  100. /** 启动后默认进入的视图:false=地图页面, true=CAD 图纸页面 */
  101. const START_IN_CAD_MODE = false;
  102. /** 地图配置 */
  103. const mapConfig: Partial<MapConfig> = {
  104. center: [114.0, 38.0],
  105. zoom: 5,
  106. tileUrl: 'http://182.92.126.35:9999/sys/common/static/map/{z}/{x}/{y}.png',
  107. };
  108. const cadData = ref<CadData | undefined>();
  109. const currentIconId = ref('speed');
  110. /** 当前点击的设备图标(用于定时刷新时重建 HTML 内容) */
  111. const currentDeviceIcon = ref<{ id: number; type: string; name?: string } | null>(null);
  112. /** 设备类型 → 中文名称映射(弹框"类型"行显示用) */
  113. const DEVICE_TYPE_NAMES: Record<string, string> = {
  114. gate: '风门',
  115. modelsensor: '传感器',
  116. obfurage: '密闭',
  117. speed: '测风装置',
  118. };
  119. /** 设备弹框数据刷新定时器(每 3 秒触发一次) */
  120. let devicePopupTimer: ReturnType<typeof setInterval> | null = null;
  121. function onModeChange(mode: string) {
  122. console.log('Viewer mode changed:', mode);
  123. currentMode.value = mode as any;
  124. }
  125. const currentMode = ref<'map' | 'cad' | 'drawing'>('map');
  126. const isCadMode = computed(() => currentMode.value === 'cad' || currentMode.value === 'drawing');
  127. /** 点击 CAD 图元(线/面/文字等)回调:输出图元信息到控制台 */
  128. function onFeatureSelect(feature: any) {
  129. console.log('Selected feature:', feature.id, feature.typename, 'layer:', feature.layer);
  130. }
  131. /** 点击地图标注点回调:输出标注点信息到控制台 */
  132. function onMarkerClick(marker: any) {
  133. console.log('Marker clicked:', marker.label, 'coords:', marker.coords, 'switchToCad:', marker.switchToCad, 'popup:', marker.popup);
  134. }
  135. /**
  136. * 点击标注点弹框中的 CAD 入口回调
  137. * 携带预先定义好的设备图标数组(演示用)一并传入 CAD 视图,
  138. * 使这些图标按类型和坐标直接绘制到图纸上。
  139. */
  140. function onMarkerOpenCad(marker: any) {
  141. console.log('Marker title clicked, opening CAD:', marker.label);
  142. if (cadData.value) {
  143. viewerRef.value?.switchToCadMode(marker, cadData.value);
  144. // 设置 CAD 画布背景色(深色背景,无默认图元颜色兜底)
  145. viewerRef.value?.setCADMapColor(false, [0, 0, 0]);
  146. }
  147. }
  148. /**
  149. * 根据设备 id/type 动态拼接弹框的 HTML div(标题、类型、数据值)
  150. * @param icon 设备图标信息(id/type/name)
  151. * @returns HTML 字符串,包含设备ID、类型、以及按类型区分的模拟实时数据行
  152. * 说明:数据值目前为模拟数据(随机数),实际项目中可替换为接口请求结果
  153. */
  154. function buildDevicePopupHtml(icon: { id: number; type: string; name?: string }): string {
  155. // 优先用类型映射得到中文名称,其次用传入 name,最后回退为 type 原文
  156. const typeName = DEVICE_TYPE_NAMES[icon.type] || icon.name || icon.type;
  157. // 生成 [min, max) 区间随机数,保留 dec 位小数
  158. const rand = (min: number, max: number, dec = 1) => (min + Math.random() * (max - min)).toFixed(dec);
  159. // 根据不同类型拼接不同的"数据值"行
  160. let dataRows = '';
  161. switch (icon.type) {
  162. case 'gate': // 风门:开关状态 + 风量
  163. dataRows = `
  164. <div class="dev-row"><span class="dev-key">开关状态</span><span class="dev-val">${Math.random() > 0.5 ? '开启' : '关闭'}</span></div>
  165. <div class="dev-row"><span class="dev-key">风量</span><span class="dev-val">${rand(800, 3200, 0)} m³/h</span></div>`;
  166. break;
  167. case 'modelsensor': // 传感器:温度 + 湿度
  168. dataRows = `
  169. <div class="dev-row"><span class="dev-key">温度</span><span class="dev-val">${rand(15, 35)} ℃</span></div>
  170. <div class="dev-row"><span class="dev-key">湿度</span><span class="dev-val">${rand(30, 90)} %</span></div>`;
  171. break;
  172. case 'obfurage': // 密闭:密闭状态 + 压差
  173. dataRows = `
  174. <div class="dev-row"><span class="dev-key">密闭状态</span><span class="dev-val">${Math.random() > 0.3 ? '正常' : '异常'}</span></div>
  175. <div class="dev-row"><span class="dev-key">压差</span><span class="dev-val">${rand(0, 500)} Pa</span></div>`;
  176. break;
  177. case 'speed': // 测风装置:风速 + 风向
  178. dataRows = `
  179. <div class="dev-row"><span class="dev-key">风速</span><span class="dev-val">${rand(0.5, 12)} m/s</span></div>
  180. <div class="dev-row"><span class="dev-key">风向</span><span class="dev-val">${['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][Math.floor(Math.random() * 8)]}</span></div>`;
  181. break;
  182. default: // 其他类型:通用数值
  183. dataRows = `
  184. <div class="dev-row"><span class="dev-key">数值</span><span class="dev-val">${rand(0, 100)}</span></div>`;
  185. }
  186. // 组装完整弹框 HTML:设备ID + 类型 + 数据行
  187. return `
  188. <div class="dev-popup">
  189. <div class="dev-row"><span class="dev-key">设备ID</span><span class="dev-val">${icon.id}</span></div>
  190. <div class="dev-row"><span class="dev-key">类型</span><span class="dev-val">${typeName}</span></div>
  191. ${dataRows}
  192. </div>`;
  193. }
  194. // 切换当前选择的省,市,和描点,重新绘制蒙版和标识
  195. function changeView() {
  196. const boundary: BoundaryConfig = {
  197. provinces: ['山西省'],
  198. cities: ['太原市', '晋中市', '长治市'],
  199. counties: [],
  200. fillColor: [129, 230, 246, 30],
  201. strokeColor: [59, 130, 246, 160],
  202. regionColors: {
  203. 太原市: { fillColor: [50, 280, 50, 30], strokeColor: [50, 280, 50, 200] },
  204. 晋中市: { fillColor: [140, 20, 112, 30], strokeColor: [140, 20, 112, 180] },
  205. },
  206. };
  207. const newMarkers: MapMarker[] = [
  208. { coords: [112.7, 37.8], label: '2', color: [255, 60, 60, 250], fontColor: [255, 255, 255, 255], borderColor: [0, 0, 0, 255] },
  209. {
  210. coords: [112.2, 38.2],
  211. label: '',
  212. color: [60, 200, 60, 250],
  213. fontColor: [255, 255, 255, 255],
  214. borderColor: [0, 0, 0, 255],
  215. switchToCad: true,
  216. popup: {
  217. title: '太原市',
  218. items: [
  219. { subtitle: '矿井名称', content: '一号煤矿' },
  220. { subtitle: '设计产能', content: '120 万吨/年' },
  221. ],
  222. },
  223. },
  224. ];
  225. viewerRef.value?.changeView(boundary, newMarkers);
  226. setMapView([112.7, 37.8], 7.2, 2000);
  227. }
  228. /**
  229. * 点击 CAD 中绘制的设备图标回调
  230. * 1. 保存当前设备(供定时器重建 HTML)
  231. * 2. 立即拼接一次 HTML 并传给图纸弹框显示
  232. * 3. 启动 3 秒定时器,周期性刷新弹框数据(模拟实时数据)
  233. */
  234. function onDeviceClick(icon: { id: number; type: string; name?: string }) {
  235. console.log('Device clicked:', icon);
  236. currentDeviceIcon.value = icon;
  237. // 立即拼接一次并显示
  238. viewerRef.value?.showDevicePopup(icon.id, icon.type, buildDevicePopupHtml(icon));
  239. // 每 3 秒更新一次新的 div 数据到 CAD 图纸弹框
  240. if (devicePopupTimer) clearInterval(devicePopupTimer); // 防重复启动
  241. devicePopupTimer = setInterval(() => {
  242. if (currentDeviceIcon.value && viewerRef.value) {
  243. viewerRef.value.showDevicePopup(currentDeviceIcon.value.id, currentDeviceIcon.value.type, buildDevicePopupHtml(currentDeviceIcon.value));
  244. }
  245. }, 3000);
  246. }
  247. /** 组件卸载前清理:兜底停止定时器,防止内存泄漏 */
  248. onBeforeUnmount(() => {
  249. if (devicePopupTimer) {
  250. clearInterval(devicePopupTimer);
  251. devicePopupTimer = null;
  252. }
  253. });
  254. /** 设备弹框关闭回调:停止刷新定时器并清空当前设备 */
  255. function onDevicePopupClose() {
  256. console.log('Device popup closed, stopping timer');
  257. if (devicePopupTimer) {
  258. clearInterval(devicePopupTimer);
  259. devicePopupTimer = null;
  260. }
  261. currentDeviceIcon.value = null;
  262. }
  263. /** 切换地图中心点和缩放级别,仅在地图模式下生效 */
  264. // 飞到位置,zoom 14,动画 1 秒
  265. //setMapView([113.7, 38.6], 14, 1000)
  266. function setMapView(center: [number, number], zoom: number, duration: number = 500) {
  267. viewerRef.value?.setMapView(center, zoom, duration);
  268. }
  269. // CAD 控制方法
  270. /** 返回地图:退出 CAD 视图,恢复到地图页面 */
  271. function goBackToMap() {
  272. viewerRef.value?.goBackToMap();
  273. }
  274. /** 切换图层面板显示/隐藏 */
  275. function toggleLayerPanel() {
  276. viewerRef.value?.toggleLayerPanel();
  277. }
  278. /** 缩放至全图:重置视图为 CAD 图纸整体范围 */
  279. function zoomFit() {
  280. viewerRef.value?.zoomToFit();
  281. }
  282. /**
  283. * 选择要绘制的图标类型
  284. * item 图标定义(type 为类型标识)
  285. * 若当前处于绘制模式则即时切换绘制工具,否则仅更新选中状态
  286. */
  287. function selectIcon(type: string) {
  288. currentIconId.value = type;
  289. if (currentMode.value === 'drawing' && viewerRef.value) {
  290. viewerRef.value.setDrawingTool(type);
  291. } else {
  292. viewerRef.value?.setSelectedIcon(type);
  293. }
  294. }
  295. /** 清除所有图标 */
  296. function clearIcons() {
  297. viewerRef.value?.clearPlacedIcons();
  298. }
  299. /** 绘制模式下放置了设备图标回调 */
  300. function onIconPlaced(icon: any) {
  301. console.log('Icon placed:', icon);
  302. }
  303. /** 拖动设备图标后位置更新回调 */
  304. function onIconMoved(icon: any) {
  305. console.log('Icon moved:', icon);
  306. }
  307. /**
  308. * 删除模式下图标被删除回调:接收被删图标的 id/type
  309. * 若删除的正是当前弹框显示的设备,则关闭弹框并停止刷新定时器
  310. */
  311. function onIconDeleted(icon: { id: number; type: string }) {
  312. console.log('Icon deleted:', icon);
  313. if (currentDeviceIcon.value && currentDeviceIcon.value.id === icon.id) {
  314. onDevicePopupClose();
  315. }
  316. }
  317. /** 返回上一级操作,如果存在CAD底图,则切换至地图,否则回退历史记录栈 */
  318. async function revertStack() {
  319. if (isTopLevel.value) return;
  320. historyStack.value.pop();
  321. }
  322. /** 页面挂载后加载 CAD 数据,并按配置决定初始视图 */
  323. onMounted(async () => {
  324. try {
  325. // 请求 CAD 原始数据(图元 + 图层颜色表)
  326. const resp = await fetch('/hll.json');
  327. const data: CadData = await resp.json();
  328. cadData.value = data;
  329. console.log(`Loaded ${data.features.length} CAD features`);
  330. // 根据配置选择初始视图
  331. if (START_IN_CAD_MODE && viewerRef.value) {
  332. // 不飞向标注点,直接进入 CAD 全图视图(无预置图标)
  333. viewerRef.value.switchToCadMode(undefined, data, []);
  334. // 设置 CAD 画布背景色(深色背景,无默认图元颜色兜底)
  335. viewerRef.value?.setCADMapColor(false, [0, 0, 0]);
  336. } else {
  337. setTimeout(() => {
  338. changeView();
  339. }, 1000);
  340. }
  341. } catch (e) {
  342. // 开发环境没有 features.json 时降级(仅打印警告,不影响地图模式使用)
  343. console.warn('Could not load features.json (expected in dev)', e);
  344. }
  345. });
  346. </script>
  347. <style lang="less">
  348. .map-container {
  349. flex: 1;
  350. height: 100%;
  351. width: 100%;
  352. position: fixed;
  353. top: 0;
  354. left: 0;
  355. z-index: @simple-map-z-index;
  356. // filter: grayscale(80%) invert(0%) sepia(10%) hue-rotate(150deg) saturate(200%) brightness(100%) contrast(100%);
  357. canvas {
  358. /* width: 1920px !important;
  359. height: 948px !important; */
  360. }
  361. }
  362. /* 图例浮层 */
  363. .map-legend {
  364. position: fixed;
  365. bottom: 24px;
  366. left: 24px;
  367. z-index: @simple-map-z-index + 1;
  368. background: rgba(255, 255, 255, 0.92);
  369. border-radius: 8px;
  370. padding: 10px 14px;
  371. font-size: 12px;
  372. line-height: 1.8;
  373. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  374. color: #333;
  375. min-width: 140px;
  376. pointer-events: none;
  377. .legend-title {
  378. font-weight: 600;
  379. margin-bottom: 4px;
  380. font-size: 13px;
  381. }
  382. .legend-row {
  383. display: flex;
  384. align-items: center;
  385. gap: 6px;
  386. }
  387. .legend-dot {
  388. display: inline-block;
  389. width: 10px;
  390. height: 10px;
  391. border-radius: 50%;
  392. flex-shrink: 0;
  393. }
  394. .legend-dot-green {
  395. background: #52c41a;
  396. }
  397. .legend-dot-orange {
  398. background: #fa8c16;
  399. }
  400. .legend-dot-yellow {
  401. background: #fadb14;
  402. }
  403. .legend-dot-red {
  404. background: #f5222d;
  405. }
  406. .legend-dot-red-dashed {
  407. background: transparent;
  408. border: 2px dashed #f5222d;
  409. }
  410. .legend-line-red {
  411. display: inline-block;
  412. width: 14px;
  413. height: 3px;
  414. background: #f5222d;
  415. flex-shrink: 0;
  416. }
  417. }
  418. </style>
  419. <style>
  420. /* ============ CAD 工具栏(地图/CAD 共用,由 App.vue 控制界面) ============ */
  421. .cad-toolbar {
  422. position: absolute;
  423. top: 12px;
  424. left: 50%;
  425. transform: translateX(-50%); /* 水平居中 */
  426. z-index: 400; /* 悬浮于地图/CAD 画布之上 */
  427. display: flex;
  428. gap: 6px;
  429. background: rgba(20, 20, 30, 0.9); /* 半透明深色底 */
  430. border: 1px solid rgba(255, 255, 255, 0.15);
  431. border-radius: 8px;
  432. padding: 6px 10px;
  433. backdrop-filter: blur(8px); /* 毛玻璃背景 */
  434. }
  435. /* 工具栏通用按钮 */
  436. .cad-btn {
  437. background: rgba(255, 255, 255, 0.08);
  438. border: 1px solid rgba(255, 255, 255, 0.12);
  439. color: #ccc;
  440. padding: 6px 12px;
  441. border-radius: 6px;
  442. font-size: 12px;
  443. cursor: pointer;
  444. transition: all 0.15s;
  445. white-space: nowrap;
  446. }
  447. .cad-btn:hover {
  448. background: rgba(255, 255, 255, 0.15);
  449. color: #fff;
  450. }
  451. /* 激活态(绘制/删除模式按钮高亮) */
  452. .cad-btn.active {
  453. background: rgba(59, 130, 246, 0.4);
  454. border-color: rgba(59, 130, 246, 0.6);
  455. color: #fff;
  456. }
  457. /* ============ 绘制模式工具栏(底部,当前隐藏预留) ============ */
  458. .drawing-toolbar {
  459. position: absolute;
  460. bottom: 24px;
  461. left: 50%;
  462. transform: translateX(-50%);
  463. z-index: 400;
  464. display: flex;
  465. align-items: center;
  466. gap: 8px;
  467. background: rgba(20, 20, 30, 0.92);
  468. border: 1px solid rgba(255, 255, 255, 0.15);
  469. border-radius: 8px;
  470. padding: 8px 14px;
  471. backdrop-filter: blur(8px);
  472. }
  473. .drawing-label {
  474. color: #aaa;
  475. font-size: 12px;
  476. }
  477. .drawing-icons {
  478. display: flex;
  479. gap: 4px;
  480. }
  481. /* 图标选择按钮(背景显示对应 SVG 图标) */
  482. .icon-btn {
  483. background: rgba(255, 255, 255, 0.06);
  484. border: 1px solid rgba(255, 255, 255, 0.1);
  485. border-radius: 6px;
  486. padding: 4px;
  487. cursor: pointer;
  488. display: flex;
  489. align-items: center;
  490. justify-content: center;
  491. width: 32px;
  492. height: 32px;
  493. transition: all 0.15s;
  494. }
  495. /* 标注点弹框标题(changeView 动态 HTML 中使用) */
  496. .marker-info-popup .mp-title2 {
  497. margin-top: 10px;
  498. font-size: 20px;
  499. color: #ce1616;
  500. text-align: center;
  501. }
  502. </style>