index-amap.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <template>
  2. <div style="height: 100%; width: 100%; display: flex; flex-direction: row">
  3. <!-- 左边是列表 -->
  4. <div style="width: 300px; height: 100%">
  5. <el-card style="height: 100%">
  6. <template #header>
  7. <div class="card-header">
  8. <el-icon :size="15" class="mr-2" style="vertical-align: middle">
  9. <component :is="Menu" />
  10. </el-icon>
  11. <span>项目列表</span>
  12. </div>
  13. </template>
  14. <el-menu
  15. :key="menuKey"
  16. :default-active="activePath"
  17. :default-openeds="defaultOpeneds"
  18. class="custom-menu"
  19. mode="vertical"
  20. @select="handleMenuSelect"
  21. >
  22. <el-sub-menu
  23. v-for="(parent, parentIndex) in menuItems"
  24. :key="parentIndex"
  25. :index="parent.index"
  26. :class="{ 'parent-active': isParentActive(parent.index) }"
  27. >
  28. <template #title>
  29. <!-- 父菜单图标 -->
  30. <el-icon :size="18" class="mr-2">
  31. <component :is="parent.icon" />
  32. </el-icon>
  33. <span>{{ parent.title }}</span>
  34. </template>
  35. <el-menu-item
  36. v-for="(child, childIndex) in parent.children"
  37. :key="childIndex"
  38. :index="child.index"
  39. >
  40. <!-- 子菜单图标 -->
  41. <el-icon :size="18" class="mr-2">
  42. <component :is="child.icon" />
  43. </el-icon>
  44. <span>{{ child.title }}</span>
  45. </el-menu-item>
  46. </el-sub-menu>
  47. </el-menu>
  48. </el-card>
  49. </div>
  50. <!-- 右边是地图引擎 -->
  51. <div style="width: 100%; height: 100%; display: grid">
  52. <div id="mapview" ref="mapview" v-loading="mapSet.loading" />
  53. <el-card class="float-stat">
  54. <div class="float-stat-item">
  55. <div style="font-weight: 900">
  56. {{ selLocation.name }}
  57. </div>
  58. </div>
  59. <el-divider></el-divider>
  60. <div class="float-stat-item">
  61. <div class="float-stat-item-key">分配基站:</div>
  62. <div class="float-stat-item-value">{{ cntAnchor }}</div>
  63. </div>
  64. <div class="float-stat-item">
  65. <div class="float-stat-item-key">在线基站:</div>
  66. <div class="float-stat-item-value">{{ cntAnchorOnline }}</div>
  67. </div>
  68. <div class="float-stat-item">
  69. <div class="float-stat-item-key">考勤人数:</div>
  70. <div class="float-stat-item-value">{{ cntPersonAttend }}</div>
  71. </div>
  72. <!-- <div class="float-stat-item">
  73. <div class="float-stat-item-key">今日到岗:</div>
  74. <div class="float-stat-item-value">{{ cntPersonConfirm }}</div>
  75. </div> -->
  76. <!-- <div style="font-size: 12px; margin-top: 10px; color: blue">
  77. 上次统计时间:
  78. </div> -->
  79. </el-card>
  80. </div>
  81. </div>
  82. </template>
  83. <script setup lang="ts">
  84. import {
  85. h,
  86. ref,
  87. onMounted,
  88. computed,
  89. onUnmounted,
  90. reactive,
  91. getCurrentInstance,
  92. onBeforeMount
  93. } from "vue";
  94. // 导入 Element Plus 内置图标
  95. import { Bottom, Folder, Menu } from "@element-plus/icons-vue";
  96. import { deviceDetection } from "@pureadmin/utils";
  97. import AMapLoader from "@amap/amap-jsapi-loader";
  98. import { mapJson } from "@/api/mock";
  99. import PicCar from "@/assets/car.png";
  100. import PicCarDisable from "@/assets/car-disable.png";
  101. import { clear } from "console";
  102. import { duration } from "dayjs";
  103. import RtWatchPart from "./rtWatchPart.vue";
  104. import Location from "@/model/location";
  105. import Project from "@/model/project";
  106. const vGlobal = window.vueGlobal;
  107. export interface MapConfigureInter {
  108. on: Fn;
  109. destroy?: Fn;
  110. clearEvents?: Fn;
  111. addControl?: Fn;
  112. setCenter?: Fn;
  113. setZoom?: Fn;
  114. plugin?: Fn;
  115. }
  116. defineOptions({
  117. name: "Amap"
  118. });
  119. let MarkerCluster;
  120. let map: MapConfigureInter;
  121. let AMap = null;
  122. const instance = getCurrentInstance();
  123. const mapSet = reactive({
  124. loading: deviceDetection() ? false : true
  125. });
  126. // 菜单数据
  127. const menuItems = [
  128. {
  129. index: "1",
  130. title: "父菜单1",
  131. icon: Menu,
  132. children: [{ index: "1-1", title: "子菜单1-1", icon: Folder }]
  133. }
  134. ];
  135. const menuKey = ref(0);
  136. let dictMenus = {};
  137. let firstMenuId = null;
  138. // 当前选中的路径
  139. const activePath = ref("");
  140. // 计算所有父菜单的索引,用于默认展开所有子菜单
  141. const defaultOpeneds = computed(() => {
  142. // 提取所有父菜单的 index 组成数组
  143. return menuItems.map(item => item.index);
  144. });
  145. const markerAnchors = [];
  146. let scheduleId = null;
  147. const selLocation = ref(new Location());
  148. const cntPersonAttend = ref(0);
  149. const cntPersonOnline = ref(0);
  150. const cntPersonConfirm = ref(0);
  151. const cntAnchor = ref(0);
  152. const cntAnchorOnline = ref(0);
  153. const createMenus = () => {
  154. menuItems.splice(0);
  155. dictMenus = {};
  156. for (const p of vGlobal.vecProject) {
  157. const pIdx = "" + p.id;
  158. menuItems.push({
  159. index: pIdx,
  160. title: p.name,
  161. icon: Menu,
  162. children: []
  163. });
  164. dictMenus[pIdx] = p;
  165. for (const m of p.locations) {
  166. const mIdx = pIdx + "-" + m.id;
  167. menuItems[menuItems.length - 1].children.push({
  168. index: mIdx,
  169. title: m.name,
  170. icon: Folder
  171. });
  172. dictMenus[mIdx] = m;
  173. if (!firstMenuId) {
  174. firstMenuId = mIdx;
  175. }
  176. }
  177. }
  178. menuKey.value++;
  179. };
  180. // 处理菜单选中事件
  181. const handleMenuSelect = index => {
  182. activePath.value = index;
  183. console.log("dictMenus", dictMenus);
  184. console.log("handleMenuSelect", index, dictMenus[index]);
  185. const location = dictMenus[index];
  186. if (location) {
  187. selLocation.value = location;
  188. const proj = location.parent;
  189. map.setCenter([location.center.x, location.center.y]);
  190. map.setZoom(13);
  191. cntAnchor.value = 0;
  192. cntAnchorOnline.value = 0;
  193. for (const anc of vGlobal.vecAnchor) {
  194. if (Number(anc.projId) == Number(proj.id)) {
  195. cntAnchor.value++;
  196. if (anc.isOnline) {
  197. cntAnchorOnline.value++;
  198. }
  199. }
  200. }
  201. cntPersonAttend.value = 0;
  202. for (const p of selLocation.value.parent.persons) {
  203. cntPersonAttend.value++;
  204. }
  205. }
  206. };
  207. // 判断父菜单是否需要激活样式
  208. const isParentActive = parentIndex => {
  209. // 检查当前选中项是否属于该父菜单
  210. return activePath.value.startsWith(parentIndex + "-");
  211. };
  212. const handleOpen = (key: string, keyPath: string[]) => {
  213. console.log(key, keyPath);
  214. };
  215. const handleClose = (key: string, keyPath: string[]) => {
  216. console.log(key, keyPath);
  217. };
  218. const genMarkerLabel = anc => {
  219. const label = {
  220. direction: "bottom",
  221. //设置文本标注偏移量
  222. offset: new AMap.Pixel(-4, 0),
  223. //设置文本标注内容
  224. content: anc.isCharging
  225. ? `<div style="color: red; font-weight: bold; font-size: 14px; " > A[#${anc.id}](充电中)</div>`
  226. : `<div style="font-size: 14px; font-weight: bold; "> A[#${anc.id}](${anc.percentBattery}%)</div>`
  227. };
  228. return label;
  229. };
  230. const schedule = () => {
  231. cntPersonAttend.value = 0;
  232. if (selLocation.value.parent) {
  233. const proj = selLocation.value.parent;
  234. for (const p of proj.persons) {
  235. if (p.locId == selLocation.value.id) {
  236. cntPersonAttend.value++;
  237. }
  238. }
  239. cntAnchorOnline.value = 0;
  240. for (const anc of vGlobal.vecAnchor) {
  241. if (Number(anc.projId) == Number(proj.id)) {
  242. if (anc.isOnline) {
  243. cntAnchorOnline.value++;
  244. }
  245. }
  246. }
  247. }
  248. if (markerAnchors.length === 0) return;
  249. for (const m of markerAnchors) {
  250. const extData = m.getExtData();
  251. const anc = vGlobal.getAnchor(extData["id"]);
  252. if (!anc) continue;
  253. m.setPosition([anc.x, anc.y]);
  254. // m.moveTo([anc.x, anc.y],
  255. // {
  256. // duration: 500,
  257. // autoRotation: true,
  258. // });
  259. m.setLabel(genMarkerLabel(anc));
  260. if (extData["isOnline"] != anc.isOnline) {
  261. extData["isOnline"] = anc.isOnline;
  262. m.setExtData(extData);
  263. if (anc.isOnline) {
  264. m.setIcon(PicCar);
  265. } else {
  266. m.setIcon(PicCarDisable);
  267. }
  268. }
  269. }
  270. };
  271. onBeforeMount(() => {
  272. if (!instance) return;
  273. const { MapConfigure } = instance.appContext.config.globalProperties.$config;
  274. const { options } = MapConfigure;
  275. AMapLoader.load({
  276. key: MapConfigure.amapKey,
  277. version: "2.0",
  278. plugins: ["AMap.MarkerCluster", "AMap.MoveAnimation"]
  279. })
  280. .then(aMap => {
  281. AMap = aMap;
  282. // 创建地图实例
  283. map = new AMap.Map(instance.refs.mapview, options);
  284. //地图中添加地图操作ToolBar插件
  285. map.plugin(["AMap.ToolBar", "AMap.MapType", "AMap.ControlBar"], () => {
  286. map.addControl(new AMap.ToolBar());
  287. //地图类型切换
  288. map.addControl(
  289. new AMap.MapType({
  290. defaultType: 0,
  291. position: {
  292. right: "10px",
  293. top: "10px"
  294. }
  295. })
  296. );
  297. map.addControl(
  298. new AMap.ControlBar({
  299. position: {
  300. right: "10px",
  301. top: "120px"
  302. }
  303. })
  304. );
  305. });
  306. // MarkerCluster = new AMap.MarkerCluster(map, [], {
  307. // // 聚合网格像素大小
  308. // gridSize: 80,
  309. // maxZoom: 14,
  310. // renderMarker(ctx) {
  311. // const { marker, data } = ctx;
  312. // if (Array.isArray(data) && data[0]) {
  313. // const { driver, plateNumber, orientation } = data[0];
  314. // const content = `<img style="transform: scale(1) rotate(${
  315. // 360 - Number(orientation)
  316. // }deg);" src='${car}' />`;
  317. // marker.setContent(content);
  318. // marker.setLabel({
  319. // direction: "bottom",
  320. // //设置文本标注偏移量
  321. // offset: new AMap.Pixel(-4, 0),
  322. // //设置文本标注内容
  323. // content: `<div> ${plateNumber}(${driver})</div>`
  324. // });
  325. // marker.setOffset(new AMap.Pixel(-18, -10));
  326. // marker.on("click", ({ lnglat }) => {
  327. // map.setZoom(13); //设置地图层级
  328. // map.setCenter(lnglat);
  329. // });
  330. // }
  331. // }
  332. // });
  333. complete();
  334. })
  335. .catch(() => {
  336. mapSet.loading = false;
  337. throw "地图加载失败,请重新加载";
  338. });
  339. });
  340. const handleResize = () => {
  341. if (map) {
  342. map.resize(); // 调用高德地图的resize方法重新计算尺寸
  343. }
  344. };
  345. onMounted(() => {
  346. const ci = setInterval(() => {
  347. if (vGlobal.isInited()) {
  348. clearInterval(ci);
  349. createMenus();
  350. handleMenuSelect(firstMenuId);
  351. console.log("anchors", vGlobal.vecAnchor);
  352. window.addEventListener("resize", handleResize);
  353. for (const v of vGlobal.vecAnchor) {
  354. const marker = new AMap.Marker({
  355. extData: {
  356. id: v.id,
  357. isOnline: v.isOnline
  358. },
  359. position: [v.x, v.y],
  360. icon: v.isOnline ? PicCar : PicCarDisable,
  361. offset: new AMap.Pixel(-18, -10),
  362. label: {
  363. direction: "bottom",
  364. //设置文本标注偏移量
  365. offset: new AMap.Pixel(-4, 0),
  366. //设置文本标注内容
  367. content: v.isCharging
  368. ? `<div> A[#${v.id}](充电中)</div>`
  369. : `<div> A[${v.id}](${v.percentBattery})</div>`
  370. }
  371. });
  372. marker.on("click", ({ lnglat }) => {
  373. // map.setZoom(13); //设置地图层级
  374. // map.setCenter(lnglat);
  375. const anc = vGlobal.getAnchor(marker.getExtData()["id"]);
  376. if (!anc) return;
  377. //信息窗体的内容
  378. var content = [
  379. `<div style="font-size: 14px; "><b style="font-size: 16px; ">A[#${anc.id}]</b>`,
  380. `<b>经度:</b> ${lnglat.lng} <b>纬度:</b> ${lnglat.lat}`,
  381. `<b>电池${anc.isCharging ? "状态" : "电量"}:</b> ${anc.isCharging ? "充电中" : anc.percentBattery + "%"}`,
  382. "</div>"
  383. ];
  384. //创建 infoWindow 实例
  385. var infoWindow = new AMap.InfoWindow({
  386. content: content.join("<br>"), //传入字符串拼接的 DOM 元素
  387. anchor: "top-left"
  388. });
  389. //打开信息窗体
  390. infoWindow.open(map, marker.getPosition()); //map 为当前地图的实例,map.getCenter() 用于获取地图中心点坐标。
  391. });
  392. marker.setMap(map);
  393. markerAnchors.push(marker);
  394. }
  395. }
  396. }, 100);
  397. console.log("onMounted");
  398. scheduleId = setInterval(() => {
  399. schedule();
  400. }, 1000);
  401. });
  402. onUnmounted(() => {
  403. window.removeEventListener("resize", handleResize);
  404. if (map) {
  405. // 销毁地图实例
  406. map.destroy() && map.clearEvents("click");
  407. }
  408. clearInterval(scheduleId);
  409. console.log("onUnmounted");
  410. scheduleId = null;
  411. });
  412. // 地图创建完成(动画关闭)
  413. const complete = (): void => {
  414. if (map) {
  415. map.on("complete", () => {
  416. mapSet.loading = false;
  417. });
  418. }
  419. };
  420. </script>
  421. <style scoped>
  422. .custom-menu {
  423. width: 100%;
  424. }
  425. :deep(.el-sub-menu .el-menu-item.is-active) {
  426. color: #0557aa !important;
  427. background-color: #e6f7ff !important;
  428. }
  429. /* 子菜单选中样式 */
  430. :deep(.el-menu-item.is-active) {
  431. color: #0557aa !important;
  432. background-color: #e6f7ff !important;
  433. font-weight: 600;
  434. }
  435. /* 父菜单激活样式 - 当子菜单选中时 */
  436. :deep(.parent-active .el-sub-menu__title) {
  437. color: #0557aa !important;
  438. font-weight: bold;
  439. }
  440. /* 父菜单 hover 样式保持一致 */
  441. :deep(.el-sub-menu__title:hover) {
  442. color: #0557aa !important;
  443. }
  444. #mapview {
  445. height: calc(100vh - 48px);
  446. width: 100%;
  447. grid-area: 1 / 1 / -1 / -1;
  448. }
  449. /* 新增根元素高度设置 */
  450. :root {
  451. height: 100%;
  452. }
  453. /* 确保父级容器高度正确传递 */
  454. :deep(html),
  455. :deep(body) {
  456. height: 100%;
  457. margin: 0;
  458. padding: 0;
  459. }
  460. /* 调整网格容器样式 */
  461. :deep(.grid-container) {
  462. /* 假设地图外层容器添加class="grid-container" */
  463. display: grid;
  464. height: 100%;
  465. }
  466. .float-stat {
  467. width: 200px;
  468. height: fit-content;
  469. background: #ffffff;
  470. grid-area: 1 / 1 / -1 / -1;
  471. z-index: 10;
  472. margin-top: 10px;
  473. margin-left: 10px;
  474. border-radius: 2px;
  475. opacity: 0.9;
  476. box-shadow: rgba(0, 0, 0, 0.6) 0 2px 2px;
  477. display: flex;
  478. flex-direction: row;
  479. }
  480. .float-stat-item {
  481. width: 100%;
  482. display: flex;
  483. flex-direction: row;
  484. }
  485. .float-stat-item-key {
  486. font-weight: 900;
  487. width: 80px;
  488. }
  489. .float-stat-item-value {
  490. font-weight: 600;
  491. width: 50px;
  492. text-align: right;
  493. }
  494. :deep(.amap-marker-label) {
  495. border: none !important;
  496. }
  497. :deep(.amap-copyright) {
  498. display: none !important;
  499. }
  500. :deep(.amap-logo) {
  501. display: none !important;
  502. }
  503. </style>