TongzhouTrafficMap.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. <template>
  2. <div class="map-wrapper">
  3. <div ref="mapContainer" class="map-container"></div>
  4. <div class="map-legend" :style="privateStyle.legend" v-if="(!mode || mode === '路口')"
  5. :class="{ 'legend-hidden': !legendVisible }">
  6. <div class="legend-header">
  7. <div class="legend-title">图例</div>
  8. <div class="legend-close-btn" @click="toggleLegend">✕</div>
  9. </div>
  10. <div class="legend-list">
  11. <div class="legend-item all-select" @click="toggleAll">
  12. <div class="legend-dot"
  13. :style="{ backgroundColor: isAllSelected ? '#fff' : 'transparent', border: '1px solid #fff' }"></div>
  14. <div class="legend-label" style="font-weight: bold;">全选</div>
  15. </div>
  16. <div v-for="item in statusConfig" class="legend-item" @click="toggleRouteVisible(item.name)" :key="item.name"
  17. :class="{ 'is-inactive': !activeLegends.includes(item.name) }"
  18. v-if="!mode || (mode === '路口' && !['干线协调', '勤务路线'].includes(item.name))">
  19. <div class="legend-dot"
  20. :style="{ backgroundColor: ['离线', '降级', '故障'].includes(item.name) ? 'transparent' : item.color }"
  21. :class="{ 'special-route': ['干线协调', '勤务路线'].includes(item.name), 'is-status-wrapper': ['离线', '降级', '故障'].includes(item.name) }">
  22. <span v-if="!['离线', '降级', '故障'].includes(item.name)">
  23. {{ item.name.charAt(0) }}
  24. </span>
  25. <img v-else
  26. :src="require(`@/assets/images/icon_${item.name === '离线' ? 'lixian' : item.name === '降级' ? 'jiangji' : 'guzhang'}.png`)"
  27. class="status-icon" />
  28. </div>
  29. <div class="legend-label">{{ item.name }}</div>
  30. </div>
  31. </div>
  32. </div>
  33. <div class="legend-show-btn" v-if="(!mode || mode === '路口') && !legendVisible" @click="toggleLegend">
  34. <div class="legend-show-icon">☰</div>
  35. </div>
  36. </div>
  37. </template>
  38. <script>
  39. import AMapLoader from '@amap/amap-jsapi-loader';
  40. export default {
  41. name: "TrafficMap",
  42. props: {
  43. amapKey: { type: String, default: '您的Key' },
  44. securityJsCode: { type: String, default: '您的安全密钥' },
  45. mode: { type: String, default: '', validator: (value) => ['', '路口', '干线', '特勤'].includes(value) }
  46. },
  47. data() {
  48. return {
  49. AMap: null,
  50. map: null,
  51. infoWindow: null,
  52. trafficLayer: null,
  53. routeGroups: {},
  54. polylines: [],
  55. privateStyle: {
  56. legend: {}
  57. },
  58. legendVisible: true,
  59. activeLegends: ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"],
  60. // 核心修正:增加生命周期标识,防止组件销毁后异步回调继续执行
  61. _isDestroyed: false,
  62. // 状态类型配置
  63. statusConfig: [
  64. { name: "中心计划", color: "#004CDE", type: "normal" },
  65. { name: "干线协调", color: "#13C373", type: "route" },
  66. { name: "勤务路线", color: "#BC301D", type: "route" },
  67. { name: "定周期控制", color: "#3296FA", type: "normal" },
  68. { name: "感应控制", color: "#FF864C", type: "normal" },
  69. { name: "自适应控制", color: "#9F6EFE", type: "normal" },
  70. { name: "手动控制", color: "#EB9F36", type: "normal" },
  71. { name: "特殊控制", color: "#A26218", type: "normal" },
  72. { name: "离线", color: "#7A7A7A", type: "abnormal" },
  73. { name: "降级", color: "#D9C13B", type: "abnormal" },
  74. { name: "故障", color: "#FF3938", type: "abnormal" }
  75. ],
  76. // 真实路口数据
  77. intersectionData: [],
  78. // 按状态分类的路口数据
  79. statusIntersections: {}
  80. };
  81. },
  82. mounted() {
  83. this._isDestroyed = false; // 重置标识
  84. this.loadMapData().then(() => {
  85. this.classifyIntersectionsByStatus();
  86. this.updateMapByMode();
  87. this.initAMap();
  88. });
  89. if (this.$route.path === '/home') {
  90. this.privateStyle.legend = { right: "25%" };
  91. }
  92. },
  93. watch: {
  94. mode: {
  95. handler() {
  96. this.updateMapByMode();
  97. this.updateMapDisplay();
  98. },
  99. immediate: false
  100. }
  101. },
  102. beforeDestroy() {
  103. // 1. 立即设置销毁状态
  104. this._isDestroyed = true;
  105. // 2. 关闭弹窗
  106. if (this.infoWindow) {
  107. try {
  108. this.infoWindow.close();
  109. } catch (e) {
  110. console.warn('关闭信息窗口时出错:', e);
  111. }
  112. this.infoWindow = null;
  113. }
  114. // 3. 清理覆盖物引用
  115. if (this.routeGroups) {
  116. Object.values(this.routeGroups).forEach(overlays => {
  117. if (Array.isArray(overlays)) {
  118. overlays.forEach(o => {
  119. try {
  120. if (o.setMap) o.setMap(null);
  121. } catch (e) {
  122. console.warn('清理覆盖物时出错:', e);
  123. }
  124. });
  125. }
  126. });
  127. this.routeGroups = {};
  128. }
  129. // 4. 销毁地图实例并清空引用
  130. if (this.map) {
  131. try {
  132. this.map.destroy();
  133. } catch (e) {
  134. console.warn('销毁地图实例时出错:', e);
  135. }
  136. this.map = null;
  137. }
  138. // 5. 清理其他引用
  139. this.AMap = null;
  140. },
  141. computed: {
  142. isAllSelected() {
  143. return this.activeLegends.length === this.statusConfig.length;
  144. }
  145. },
  146. methods: {
  147. // 动态加载地图数据
  148. async loadMapData() {
  149. try {
  150. const mapDataModule = await import('@/mock/map_data_gaode.json');
  151. this.intersectionData = mapDataModule.default || [];
  152. console.log('地图数据加载成功,共', this.intersectionData.length, '个路口');
  153. } catch (error) {
  154. console.error('地图数据加载失败:', error);
  155. this.intersectionData = [];
  156. }
  157. },
  158. // 检查地图环境是否安全可用
  159. isMapReady() {
  160. return !this._isDestroyed && this.map && typeof this.map.add === 'function';
  161. },
  162. // 将真实路口数据按状态类型分类
  163. classifyIntersectionsByStatus() {
  164. // 首先为勤务路线和干线协调各分配4条数据
  165. const routeData = this.intersectionData.slice(0, 8);
  166. const remainingData = this.intersectionData.slice(8);
  167. // 计算正常状态数据需要分配的状态类型数量(11-2-3=6)
  168. const normalStatusCount = 6;
  169. const abnormalStatusCount = 3;
  170. const chunkSize = Math.floor(remainingData.length / (normalStatusCount + abnormalStatusCount));
  171. // 计算异常状态的最大数量(不超过10个)
  172. const maxAbnormalCount = 10;
  173. this.statusIntersections = {
  174. "中心计划": remainingData.slice(0, chunkSize),
  175. "干线协调": routeData.slice(0, 4),
  176. "勤务路线": routeData.slice(4, 8),
  177. "定周期控制": remainingData.slice(chunkSize, chunkSize * 2),
  178. "感应控制": remainingData.slice(chunkSize * 2, chunkSize * 3),
  179. "自适应控制": remainingData.slice(chunkSize * 3, chunkSize * 4),
  180. "手动控制": remainingData.slice(chunkSize * 4, chunkSize * 5),
  181. "特殊控制": remainingData.slice(chunkSize * 5, chunkSize * 6),
  182. "离线": remainingData.slice(chunkSize * 6, Math.min(chunkSize * 7, chunkSize * 6 + maxAbnormalCount)),
  183. "降级": remainingData.slice(chunkSize * 7, Math.min(chunkSize * 8, chunkSize * 7 + maxAbnormalCount)),
  184. "故障": remainingData.slice(chunkSize * 8, Math.min(chunkSize * 9, chunkSize * 8 + maxAbnormalCount))
  185. };
  186. },
  187. updateMapByMode() {
  188. switch (this.mode) {
  189. case '路口':
  190. this.activeLegends = ["中心计划", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"];
  191. break;
  192. case '干线':
  193. this.activeLegends = ["干线协调"];
  194. break;
  195. case '特勤':
  196. this.activeLegends = ["勤务路线"];
  197. break;
  198. default:
  199. this.activeLegends = this.statusConfig.map(item => item.name);
  200. }
  201. },
  202. updateMapDisplay() {
  203. if (this.infoWindow) this.infoWindow.close();
  204. if (!this.isMapReady()) return;
  205. Object.keys(this.routeGroups).forEach(name => {
  206. const overlays = this.routeGroups[name];
  207. if (overlays && overlays.length > 0) {
  208. if (this.activeLegends.includes(name)) {
  209. this.map.add(overlays);
  210. } else {
  211. this.map.remove(overlays);
  212. }
  213. }
  214. });
  215. },
  216. async initAMap() {
  217. if (this._isDestroyed) return;
  218. window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
  219. try {
  220. // 固定加载需要的插件
  221. const AMap = await AMapLoader.load({
  222. key: this.amapKey,
  223. version: "2.0",
  224. plugins: ['AMap.Driving']
  225. });
  226. // 异步回来后,首先检查组件是否还在
  227. if (this._isDestroyed) return;
  228. this.AMap = AMap;
  229. this.map = new AMap.Map(this.$refs.mapContainer, {
  230. zoom: 14.5,
  231. mapStyle: "amap://styles/darkblue",
  232. center: [116.663, 39.905],
  233. });
  234. this.map.on('complete', () => {
  235. if (!this._isDestroyed) {
  236. this.drawStaticRoutes();
  237. this.enableTrafficLayer();
  238. }
  239. });
  240. } catch (err) {
  241. console.error('地图加载失败:', err);
  242. }
  243. },
  244. enableTrafficLayer() {
  245. if (!this.isMapReady() || !this.AMap.TileLayer) return;
  246. this.trafficLayer = new this.AMap.TileLayer.Traffic({
  247. zIndex: 10,
  248. autoRefresh: true
  249. });
  250. this.map.add(this.trafficLayer);
  251. },
  252. drawStaticRoutes() {
  253. if (!this.isMapReady()) return;
  254. this.statusConfig.forEach((config, index) => {
  255. // 使用箭头函数保持 this 指向 Vue 实例
  256. setTimeout(() => {
  257. if (!this.isMapReady()) return;
  258. try {
  259. const intersections = this.statusIntersections[config.name] || [];
  260. const markers = [];
  261. let polyline = null;
  262. // 路线逻辑:对于路线类型,创建连接线
  263. if (["干线协调", "勤务路线"].includes(config.name)) {
  264. if (intersections.length >= 2) {
  265. const path = intersections.map(item => [item["位置-经度"], item["位置-纬度"]]);
  266. polyline = new this.AMap.Polyline({
  267. path: path,
  268. strokeColor: config.color,
  269. strokeWeight: 6,
  270. strokeOpacity: 0.6,
  271. zIndex: 15
  272. });
  273. }
  274. }
  275. // 为每个路口创建标记
  276. intersections.forEach((item, idx) => {
  277. const position = [item["位置-经度"], item["位置-纬度"]];
  278. const markerConfig = {
  279. ...config,
  280. name: config.name,
  281. id: item["路口编号"],
  282. road: item["路口名称"],
  283. time: new Date().toLocaleString('zh-CN')
  284. };
  285. markers.push(this.createTrafficLightMarker(position, markerConfig));
  286. });
  287. const overlays = [...markers, polyline].filter(Boolean);
  288. this.routeGroups[config.name] = overlays;
  289. if (this.isMapReady() && this.activeLegends.includes(config.name)) {
  290. this.map.add(overlays);
  291. }
  292. } catch (e) {
  293. console.warn('处理路线数据时出错:', e);
  294. }
  295. }, index * 200);
  296. });
  297. },
  298. createTrafficLightMarker(position, config) {
  299. if (!position || !config) return null;
  300. try {
  301. const isAbnormal = ["离线", "降级", "故障"].includes(config.name);
  302. const lng = Number(position[0] || position.lng);
  303. const lat = Number(position[1] || position.lat);
  304. // 验证坐标有效性
  305. if (isNaN(lng) || isNaN(lat)) return null;
  306. // 3. 【视觉优化】调整 Marker 大小比例
  307. // 异常状态图标略大(为了警示),普通点位略小且半透明
  308. const size = isAbnormal ? '18px' : '14px';
  309. const opacity = isAbnormal ? '1' : '0.85';
  310. const shadow = isAbnormal ? `0 0 10px ${config.color || '#999'}` : `0 0 5px ${config.color || '#999'}`;
  311. const marker = new this.AMap.Marker({
  312. position: [lng, lat],
  313. zIndex: isAbnormal ? 110 : 100, // 异常图标显示在更上层
  314. content: `
  315. <div class="pure-light-node ${isAbnormal ? 'breathe abnormal-node' : ''}"
  316. style="
  317. width: ${size};
  318. height: ${size};
  319. background: ${config.color || '#999'};
  320. box-shadow: ${shadow};
  321. opacity: ${opacity};
  322. border: 1.5px solid rgba(255,255,255,0.7);
  323. font-size: 12px;
  324. display: flex;
  325. justify-content: center;
  326. align-items: center;
  327. color: #fff;
  328. padding: 8px;
  329. ">
  330. <span style="transform: scale(0.7); font-weight: bold;">${config.name.charAt(0)}</span>
  331. </div>
  332. `,
  333. offset: new this.AMap.Pixel(-8, -8),
  334. extData: {
  335. ...config,
  336. position: [lng, lat],
  337. statusColor: config.color || '#999',
  338. statusLabel: isAbnormal ? config.name : "正常运行",
  339. road: config.road || '未知路口',
  340. time: config.time || new Date().toLocaleString('zh-CN')
  341. }
  342. });
  343. marker.on('click', (e) => {
  344. if (!this._isDestroyed) {
  345. this.openLightInfo(e.target.getExtData(), e.lnglat);
  346. this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
  347. }
  348. });
  349. return marker;
  350. } catch (e) {
  351. console.warn('创建标记时出错:', e);
  352. return null;
  353. }
  354. },
  355. openLightInfo(data, position) {
  356. if (!this.isMapReady()) return;
  357. const infoWindowId = `info-window-${Date.now()}`;
  358. const content = `
  359. <div class="custom-info-card" id="${infoWindowId}">
  360. <div class="close-btn" data-id="${infoWindowId}">✕</div>
  361. <div class="card-header">
  362. <div class="status-dot" style="background: ${data.statusColor}">
  363. <span>${data.name.charAt(0)}</span>
  364. </div>
  365. <span class="status-text">${data.statusLabel}</span>
  366. </div>
  367. <div class="card-body">
  368. <div class="info-line"><span class="label">路口:</span><span class="value">${data.road}</span></div>
  369. <div class="info-line"><span class="label">路口编号:</span><span class="value digital">${data.id || 'N/A'}</span></div>
  370. <div class="info-line"><span class="label">发生时间:</span><span class="value digital">${data.time}</span></div>
  371. </div>
  372. </div>
  373. `;
  374. if (!this.infoWindow) {
  375. this.infoWindow = new this.AMap.InfoWindow({
  376. isCustom: true,
  377. offset: new this.AMap.Pixel(0, -20)
  378. });
  379. }
  380. this.infoWindow.setContent(content);
  381. this.infoWindow.open(this.map, position);
  382. // 添加关闭按钮事件监听器
  383. setTimeout(() => {
  384. const closeBtn = document.querySelector(`#${infoWindowId} .close-btn`);
  385. if (closeBtn) {
  386. closeBtn.addEventListener('click', () => {
  387. if (this.infoWindow) this.infoWindow.close();
  388. });
  389. }
  390. }, 100);
  391. },
  392. toggleAll() {
  393. const targetState = !this.isAllSelected;
  394. if (!this.isMapReady()) return;
  395. if (targetState) {
  396. this.activeLegends = this.statusConfig.map(item => item.name);
  397. Object.values(this.routeGroups).forEach(overlays => {
  398. if (overlays && overlays.length > 0) this.map.add(overlays);
  399. });
  400. } else {
  401. this.activeLegends = [];
  402. Object.values(this.routeGroups).forEach(overlays => {
  403. if (overlays && overlays.length > 0) this.map.remove(overlays);
  404. });
  405. if (this.infoWindow) this.infoWindow.close();
  406. }
  407. },
  408. toggleRouteVisible(name) {
  409. if (!this.isMapReady()) return;
  410. const overlays = this.routeGroups[name] || [];
  411. const index = this.activeLegends.indexOf(name);
  412. if (index > -1) {
  413. this.activeLegends.splice(index, 1);
  414. this.map.remove(overlays);
  415. if (this.infoWindow) this.infoWindow.close();
  416. } else {
  417. this.activeLegends.push(name);
  418. this.map.add(overlays);
  419. }
  420. },
  421. focusByLocation(targetPos) {
  422. if (!this.isMapReady() || !targetPos || targetPos.length !== 2) return;
  423. let foundMarker = null;
  424. Object.values(this.routeGroups).forEach(group => {
  425. const marker = group.find(item => {
  426. if (!(item instanceof this.AMap.Marker)) return false;
  427. const pos = item.getExtData().position;
  428. return Math.abs(pos[0] - targetPos[0]) < 0.0001 && Math.abs(pos[1] - targetPos[1]) < 0.0001;
  429. });
  430. if (marker) foundMarker = marker;
  431. });
  432. if (foundMarker) {
  433. const finalPos = foundMarker.getPosition();
  434. this.map.setZoomAndCenter(17, finalPos, false, 500);
  435. setTimeout(() => {
  436. if (!this._isDestroyed) this.openLightInfo(foundMarker.getExtData(), finalPos);
  437. }, 600);
  438. }
  439. },
  440. toggleLegend() {
  441. this.legendVisible = !this.legendVisible;
  442. }
  443. }
  444. };
  445. </script>
  446. <style scoped>
  447. .map-wrapper {
  448. width: 100%;
  449. height: 100vh;
  450. position: relative;
  451. background: #010813;
  452. }
  453. .map-container {
  454. width: 100%;
  455. height: 100%;
  456. }
  457. ::v-deep .pure-light-node.breathe {
  458. animation: light-breathe 2s infinite ease-in-out;
  459. }
  460. ::v-deep .pure-light-node span {
  461. display: flex;
  462. transform: scale(0.75);
  463. align-items: center;
  464. }
  465. ::v-deep .pure-light-node:hover {
  466. transform: scale(1.4);
  467. filter: brightness(1.2);
  468. }
  469. @keyframes light-breathe {
  470. 0% {
  471. transform: scale(0.9);
  472. opacity: 0.8;
  473. }
  474. 50% {
  475. transform: scale(1.1);
  476. opacity: 1;
  477. }
  478. 100% {
  479. transform: scale(0.9);
  480. opacity: 0.8;
  481. }
  482. }
  483. ::v-deep .close-btn {
  484. position: absolute;
  485. top: 10px;
  486. right: 12px;
  487. color: #8da6c7;
  488. cursor: pointer;
  489. font-size: 16px;
  490. transition: color 0.3s;
  491. line-height: 1;
  492. z-index: 10;
  493. }
  494. ::v-deep .close-btn:hover {
  495. color: #ffffff;
  496. }
  497. ::v-deep .custom-info-card {
  498. position: relative;
  499. background: rgba(10, 15, 24, 0.95);
  500. border-radius: 10px;
  501. padding: 12px 16px;
  502. min-width: 200px;
  503. border: 1px solid rgba(255, 255, 255, 0.1);
  504. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  505. color: #fff;
  506. }
  507. ::v-deep .card-header {
  508. display: flex;
  509. align-items: center;
  510. margin-bottom: 10px;
  511. }
  512. ::v-deep .status-dot {
  513. width: 18px;
  514. height: 18px;
  515. border-radius: 50%;
  516. display: flex;
  517. justify-content: center;
  518. align-items: center;
  519. margin-right: 8px;
  520. font-size: 12px;
  521. padding: 12px;
  522. box-sizing: border-box;
  523. }
  524. ::v-deep .status-dot span {
  525. transform: scale(0.75);
  526. }
  527. ::v-deep .status-text {
  528. font-size: 15px;
  529. font-weight: bold;
  530. }
  531. ::v-deep .info-line {
  532. display: flex;
  533. margin-bottom: 6px;
  534. font-size: 13px;
  535. align-items: center;
  536. }
  537. ::v-deep .label {
  538. color: #8da6c7;
  539. white-space: nowrap;
  540. }
  541. ::v-deep .value {
  542. color: #ffffff;
  543. }
  544. ::v-deep .digital {
  545. font-family: 'Consolas', monospace;
  546. }
  547. .map-legend {
  548. position: absolute;
  549. bottom: 30px;
  550. right: 40px;
  551. background: rgba(5, 22, 45, 0.9);
  552. border: 1px solid #1e4d8e;
  553. padding: 15px;
  554. border-radius: 6px;
  555. z-index: 100;
  556. transition: all 0.3s ease-in-out;
  557. opacity: 1;
  558. max-width: 200px;
  559. overflow: hidden;
  560. }
  561. .map-legend.legend-hidden {
  562. opacity: 0;
  563. transform: translateX(calc(100% + 20px));
  564. pointer-events: none;
  565. }
  566. .legend-header {
  567. display: flex;
  568. justify-content: space-between;
  569. align-items: center;
  570. margin-bottom: 12px;
  571. border-bottom: 1px solid #1e4d8e;
  572. padding-bottom: 8px;
  573. }
  574. .legend-close-btn {
  575. color: #8da6c7;
  576. cursor: pointer;
  577. font-size: 16px;
  578. transition: color 0.3s;
  579. line-height: 1;
  580. }
  581. .legend-close-btn:hover {
  582. color: #ffffff;
  583. }
  584. .legend-show-btn {
  585. position: absolute;
  586. bottom: 30px;
  587. right: 20px;
  588. background: rgba(5, 22, 45, 0.9);
  589. border: 1px solid #1e4d8e;
  590. width: 40px;
  591. height: 40px;
  592. border-radius: 6px 0 0 6px;
  593. display: flex;
  594. justify-content: center;
  595. align-items: center;
  596. cursor: pointer;
  597. z-index: 99;
  598. transition: all 0.3s ease-in-out;
  599. }
  600. .legend-show-btn:hover {
  601. background: rgba(10, 30, 60, 0.9);
  602. border-color: #3a75c4;
  603. }
  604. .legend-show-icon {
  605. color: #8da6c7;
  606. font-size: 18px;
  607. transition: color 0.3s;
  608. }
  609. .legend-show-btn:hover .legend-show-icon {
  610. color: #ffffff;
  611. }
  612. .legend-title {
  613. color: #fff;
  614. font-size: 14px;
  615. }
  616. .legend-item {
  617. display: flex;
  618. align-items: center;
  619. margin-bottom: 10px;
  620. cursor: pointer;
  621. transition: 0.3s;
  622. }
  623. .legend-item.is-inactive {
  624. opacity: 0.2;
  625. filter: grayscale(1);
  626. }
  627. .legend-dot {
  628. margin-right: 10px;
  629. display: flex;
  630. justify-content: center;
  631. align-items: center;
  632. }
  633. .legend-dot:not(.is-status-wrapper) {
  634. width: 20px;
  635. height: 20px;
  636. border-radius: 50%;
  637. }
  638. .legend-dot span {
  639. display: inline-block;
  640. width: 20px;
  641. height: 20px;
  642. font-size: 12px;
  643. color: #FFF;
  644. text-align: center;
  645. transform: scale(0.75);
  646. line-height: 20px;
  647. }
  648. .legend-dot.special-route {
  649. position: relative;
  650. overflow: visible !important;
  651. z-index: 1;
  652. border: none !important;
  653. border-radius: 50%;
  654. }
  655. .legend-dot.special-route span {
  656. position: relative;
  657. z-index: 3;
  658. font-weight: bold;
  659. }
  660. .legend-dot.special-route::after {
  661. content: "";
  662. position: absolute;
  663. top: 50%;
  664. left: 50%;
  665. width: 150%;
  666. height: 3px;
  667. background-color: inherit;
  668. opacity: 0.8;
  669. transform: translate(-50%, -50%) rotate(-45deg);
  670. pointer-events: none;
  671. z-index: 0;
  672. }
  673. .legend-dot.special-route::before {
  674. content: "";
  675. position: absolute;
  676. top: 0;
  677. left: 0;
  678. width: 100%;
  679. height: 100%;
  680. border-radius: 50%;
  681. z-index: 2;
  682. background-color: inherit;
  683. opacity: 1;
  684. }
  685. .legend-label {
  686. flex: 1;
  687. color: #d0d9e2;
  688. font-size: 13px;
  689. }
  690. .legend-status {
  691. font-size: 11px;
  692. color: #5b7da8;
  693. }
  694. .all-select {
  695. border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  696. padding-bottom: 12px;
  697. margin-bottom: 12px !important;
  698. }
  699. .all-select .legend-dot {
  700. border-radius: 2px;
  701. transition: all 0.3s;
  702. width: 10px;
  703. height: 10px;
  704. }
  705. .legend-dot.is-status-wrapper {
  706. width: 28px;
  707. height: 28px;
  708. background-color: transparent !important;
  709. box-shadow: none !important;
  710. border: none !important;
  711. margin-right: 0;
  712. transform: translateX(-15%);
  713. }
  714. .status-icon {
  715. width: 100%;
  716. height: 100%;
  717. object-fit: contain;
  718. display: block;
  719. }
  720. ::v-deep .pure-light-node {
  721. width: 16px;
  722. height: 16px;
  723. border-radius: 50%;
  724. border: 2px solid rgba(255, 255, 255, 0.8);
  725. cursor: pointer;
  726. transition: all 0.3s;
  727. display: flex;
  728. justify-content: center;
  729. align-items: center;
  730. color: #fff;
  731. pointer-events: auto;
  732. }
  733. /* 异常状态增加稍微剧烈一点的呼吸感,但缩小范围 */
  734. @keyframes light-breathe {
  735. 0% {
  736. transform: scale(0.9);
  737. opacity: 0.8;
  738. }
  739. 50% {
  740. transform: scale(1.1);
  741. opacity: 1;
  742. }
  743. 100% {
  744. transform: scale(0.9);
  745. opacity: 0.8;
  746. }
  747. }
  748. /* 首页展示时,如果觉得还是太密,可以给非异常节点降权 */
  749. ::v-deep .pure-light-node:not(.abnormal-node) {
  750. border: 1px solid rgba(255, 255, 255, 0.4);
  751. }
  752. </style>