TongzhouTrafficMap.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <template>
  2. <div class="map-wrapper">
  3. <div ref="mapContainer" class="map-container"></div>
  4. <div class="map-header" v-if="initialized" :style="privateStyle.search">
  5. <div class="search-form">
  6. <input type="text" v-model="searchQuery" placeholder="请输入路段或设备名称" class="search-input"
  7. @keyup.enter="handleSearch" />
  8. <button class="search-btn" @click="handleSearch">查询</button>
  9. </div>
  10. <div class="action-box" @click="toggleAll">全选 ▾</div>
  11. </div>
  12. <div class="map-legend" v-if="initialized" :style="privateStyle.legend">
  13. <div class="legend-title">图例</div>
  14. <div class="legend-list">
  15. <div v-for="item in legendConfig" :key="item.type" class="legend-item"
  16. :class="{ 'is-hidden': !activeLegends.includes(item.type) }" @click="handleLegendClick(item.type)">
  17. <span class="legend-dot" :style="{ backgroundColor: item.color }"></span>
  18. <span class="legend-label">{{ item.label }}</span>
  19. </div>
  20. </div>
  21. </div>
  22. </div>
  23. </template>
  24. <script>
  25. import AMapLoader from '@amap/amap-jsapi-loader';
  26. export default {
  27. name: "TrafficMap",
  28. props: {
  29. amapKey: { type: String, default: '您的Key' },
  30. securityJsCode: { type: String, default: '您的安全密钥' }
  31. },
  32. data() {
  33. return {
  34. AMap: null,
  35. map: null,
  36. drivingInstances: [], // 存储路径规划实例
  37. infoWindow: null,
  38. initialized: false,
  39. searchQuery: '', // 搜索内容绑定
  40. activeLegends: [],
  41. legendConfig: [
  42. { type: 'all_selected', label: '全选', color: '#e5e5e5' },
  43. { type: 'center_plan', label: '中心计划', color: '#32c5ff' },
  44. { type: 'trunk_coord', label: '干线协调', color: '#3ee68d' },
  45. { type: 'service_route', label: '勤务路线', color: '#ffcc33' },
  46. { type: 'periodic', label: '定周期控制', color: '#00ccff' },
  47. { type: 'induction', label: '感应控制', color: '#7ed3b2' },
  48. { type: 'adaptive', label: '自适应控制', color: '#8ca1ff' },
  49. { type: 'manual', label: '手动控制', color: '#cc8d66' },
  50. { type: 'special', label: '特殊控制', color: '#ffb33b' },
  51. { type: 'offline', label: '离线', color: '#6d7791' },
  52. { type: 'degraded', label: '降级', color: '#c4a737' },
  53. { type: 'fault', label: '故障', color: '#ff4d4f' }
  54. ],
  55. overlayGroups: {},
  56. privateStyle: {
  57. search: {},
  58. legend: {}
  59. }
  60. };
  61. },
  62. mounted() {
  63. this.initAMap();
  64. // 自定义首页地图搜索和图例位置样式
  65. if (this.$route.path === '/home') {
  66. this.privateStyle.search = { top: "100px", left: "25%", right: "25%" };
  67. this.privateStyle.legend = { right: "25%" };
  68. }
  69. },
  70. beforeDestroy() {
  71. if (this.map) {
  72. this.map.destroy();
  73. }
  74. },
  75. methods: {
  76. async initAMap() {
  77. // 1. 配置安全密钥(必须在 load 之前)
  78. window._AMapSecurityConfig = {
  79. securityJsCode: this.securityJsCode,
  80. };
  81. try {
  82. // 2. 加载地图核心及插件
  83. const AMap = await AMapLoader.load({
  84. key: this.amapKey,
  85. version: "2.0",
  86. plugins: ['AMap.Driving']
  87. });
  88. this.AMap = AMap;
  89. // 3. 实例化地图
  90. this.map = new AMap.Map(this.$refs.mapContainer, {
  91. zoom: 14,
  92. mapStyle: "amap://styles/darkblue",
  93. viewMode: "3D",
  94. center: [116.661132, 39.902996], // 通州火车站中心
  95. });
  96. // 4. 等待地图加载完成后执行路径规划
  97. this.map.on('complete', () => {
  98. console.log("地图加载完成,开始绘制真实路网...");
  99. this.initialized = true;
  100. this.activeLegends = this.legendConfig.map(l => l.type);
  101. this.drawStaticRoutes();
  102. });
  103. } catch (err) {
  104. console.error('地图加载失败:', err);
  105. }
  106. },
  107. drawStaticRoutes() {
  108. // 定义 3横 3纵 坐标(确保起终点大致跨越通州核心区,让 Driving 自动找路)
  109. const routeConfigs = [
  110. // --- 横向 3 条 ---
  111. { name: "新华大街", start: [116.642, 39.910], end: [116.680, 39.911], color: "#32c5ff" },
  112. { name: "玉带河大街", start: [116.642, 39.902], end: [116.680, 39.903], color: "#3ee68d" },
  113. { name: "运河西大街", start: [116.642, 39.894], end: [116.680, 39.895], color: "#ffcc33" },
  114. // --- 纵向 3 条 ---
  115. { name: "车站路", start: [116.652, 39.915], end: [116.653, 39.889], color: "#fc8c23" },
  116. { name: "新华南路", start: [116.661, 39.916], end: [116.661, 39.889], color: "#8ca1ff" },
  117. { name: "东关大道", start: [116.671, 39.916], end: [116.671, 39.890], color: "#cc8d66" }
  118. ];
  119. routeConfigs.forEach(config => {
  120. // 为每一条路创建一个 Driving 实例
  121. const driving = new this.AMap.Driving({
  122. map: this.map, // 直接展现结果在地图上
  123. hideMarkers: false, // 显示起终点 Marker
  124. autoFitView: false, // 禁止自动缩放(防止多条线时地图乱跳)
  125. outlineColor: '#000', // 路线描边
  126. // 默认样式是蓝色,由于 Driving.search 不直接支持自定义颜色,
  127. // 这里我们先渲染默认路线。
  128. });
  129. // 执行搜索
  130. driving.search(config.start, config.end, (status, result) => {
  131. if (status === 'complete') {
  132. console.log(`${config.name} 绘制成功`);
  133. } else {
  134. console.error(`${config.name} 绘制失败:`, result);
  135. }
  136. });
  137. // this.drivingInstances.push(driving);
  138. });
  139. },
  140. // 搜索查询逻辑
  141. handleSearch() {
  142. if (!this.searchQuery.trim()) {
  143. alert("请输入查询内容");
  144. return;
  145. }
  146. console.log("正在执行搜索:", this.searchQuery);
  147. // 这里可以扩展具体的搜索逻辑,比如搜索 POI 或在已有 overlayGroups 中高亮匹配项
  148. alert(`已提交查询:${this.searchQuery}`);
  149. },
  150. drawTrafficScene() {
  151. const roads = [
  152. { type: 'center_plan', name: '新华南北路 - 中心控制段', path: [[116.665, 39.940], [116.665, 39.885]], hasDots: true },
  153. { type: 'trunk_coord', name: '通胡大街 - 干线协调(北)', path: [[116.620, 39.930], [116.650, 39.920], [116.700, 39.930]] },
  154. { type: 'trunk_coord', name: '运河东大街 - 干线协调(南)', path: [[116.615, 39.900], [116.660, 39.910], [116.710, 39.900]] },
  155. { type: 'service_route', name: '新华东街 - 勤务专用线', path: [[116.625, 39.905], [116.700, 39.905]] },
  156. { type: 'fault', name: '路县故城周边 - 设备异常', path: [[116.685, 39.920], [116.690, 39.905]] }
  157. ];
  158. roads.forEach(road => this.renderRoadWithStyle(road));
  159. },
  160. renderRoadWithStyle(road) {
  161. const config = this.legendConfig.find(l => l.type === road.type);
  162. const group = [];
  163. const glow = new this.AMap.Polyline({
  164. path: road.path, strokeColor: config.color, strokeOpacity: 0.15, strokeWeight: 20, map: this.map
  165. });
  166. const line = new this.AMap.Polyline({
  167. path: road.path, strokeColor: config.color, strokeWeight: 6, map: this.map
  168. });
  169. [glow, line].forEach(item => {
  170. item.on('click', (e) => this.openDetailWindow(road, e.lnglat));
  171. group.push(item);
  172. });
  173. if (road.hasDots) {
  174. const dots = this.calculatePathDots(road.path, 12);
  175. dots.forEach((pos, index) => {
  176. const marker = new this.AMap.Marker({
  177. position: pos,
  178. content: `<div class="pulse-dot"></div>`,
  179. offset: new this.AMap.Pixel(-6, -6),
  180. map: this.map
  181. });
  182. marker.on('click', (e) => this.openDetailWindow({ ...road, name: `${road.name}-监测点${index + 1}` }, e.lnglat));
  183. group.push(marker);
  184. });
  185. }
  186. this.overlayGroups[road.type] = (this.overlayGroups[road.type] || []).concat(group);
  187. },
  188. calculatePathDots(path, count) {
  189. const p1 = path[0], p2 = path[1];
  190. const dots = [];
  191. for (let i = 1; i < count; i++) {
  192. dots.push([p1[0] + (p2[0] - p1[0]) * (i / count), p1[1] + (p2[1] - p1[1]) * (i / count)]);
  193. }
  194. return dots;
  195. },
  196. openDetailWindow(data, lnglat) {
  197. const config = this.legendConfig.find(l => l.type === data.type);
  198. const content = `
  199. <div class="custom-info-card">
  200. <div class="card-header" style="background: ${config.color}22; border-left: 4px solid ${config.color}">
  201. <span class="title">${data.name}</span>
  202. </div>
  203. <div class="card-body">
  204. <div class="info-row"><span class="label">管控类型:</span><span class="value" style="color:${config.color}">${config.label}</span></div>
  205. <div class="info-row"><span class="label">当前状态:</span><span class="value" style="color:#3ee68d">运行中</span></div>
  206. </div>
  207. </div>
  208. `;
  209. this.infoWindow.setContent(content);
  210. this.infoWindow.open(this.map, lnglat);
  211. },
  212. handleLegendClick(type) {
  213. if (type === 'all_selected') return this.toggleAll();
  214. const isVisible = this.activeLegends.includes(type);
  215. this.activeLegends = isVisible ? this.activeLegends.filter(t => t !== type) : [...this.activeLegends, type];
  216. const group = this.overlayGroups[type];
  217. if (group) group.forEach(o => isVisible ? o.hide() : o.show());
  218. if (isVisible) this.infoWindow.close();
  219. },
  220. toggleAll() {
  221. const allTypes = this.legendConfig.map(l => l.type);
  222. const isShowingAll = this.activeLegends.length === allTypes.length;
  223. allTypes.forEach(type => {
  224. const group = this.overlayGroups[type];
  225. if (group) group.forEach(o => isShowingAll ? o.hide() : o.show());
  226. });
  227. this.activeLegends = isShowingAll ? [] : allTypes;
  228. this.infoWindow.close();
  229. }
  230. }
  231. };
  232. </script>
  233. <style scoped>
  234. .map-wrapper {
  235. position: relative;
  236. width: 100%;
  237. height: 100%;
  238. min-height: 500px;
  239. background: #010813;
  240. overflow: hidden;
  241. }
  242. .map-container {
  243. width: 100%;
  244. height: 100%;
  245. }
  246. /* 头部样式:增加了搜索表单布局 */
  247. .map-header {
  248. position: absolute;
  249. top: 55px;
  250. left: 50px;
  251. right: 50px;
  252. display: flex;
  253. justify-content: space-between;
  254. align-items: center;
  255. z-index: 10;
  256. }
  257. /* 搜索表单容器 */
  258. .search-form {
  259. display: flex;
  260. background: rgba(13, 35, 67, 0.9);
  261. border: 1px solid #1a4a8d;
  262. border-radius: 4px;
  263. overflow: hidden;
  264. }
  265. .search-input {
  266. background: transparent;
  267. border: none;
  268. padding: 10px 15px;
  269. color: #fff;
  270. width: 240px;
  271. outline: none;
  272. font-size: 13px;
  273. }
  274. .search-input::placeholder {
  275. color: #5b7da8;
  276. }
  277. .search-btn {
  278. background: #1a4a8d;
  279. border: none;
  280. color: #fff;
  281. padding: 0 20px;
  282. cursor: pointer;
  283. font-size: 13px;
  284. transition: background 0.2s;
  285. }
  286. .search-btn:hover {
  287. background: #2660b3;
  288. }
  289. .action-box {
  290. background: rgba(13, 35, 67, 0.9);
  291. border: 1px solid #1a4a8d;
  292. padding: 10px 18px;
  293. color: #fff;
  294. border-radius: 4px;
  295. font-size: 13px;
  296. cursor: pointer;
  297. }
  298. /* 图例与其它样式保持不变 */
  299. .map-legend {
  300. position: absolute;
  301. right: 50px;
  302. bottom: 40px;
  303. width: 170px;
  304. background: rgba(8, 20, 36, 0.95);
  305. border: 1px solid rgba(38, 74, 124, 0.8);
  306. border-radius: 12px;
  307. padding: 18px;
  308. z-index: 100;
  309. box-shadow: 0 0 25px rgba(0, 0, 0, 0.6);
  310. }
  311. .legend-title {
  312. color: #fff;
  313. font-size: 18px;
  314. margin-bottom: 18px;
  315. font-weight: bold;
  316. }
  317. .legend-item {
  318. display: flex;
  319. align-items: center;
  320. margin-bottom: 14px;
  321. cursor: pointer;
  322. transition: 0.2s;
  323. }
  324. .legend-item.is-hidden {
  325. opacity: 0.2;
  326. filter: grayscale(1);
  327. }
  328. .legend-dot {
  329. width: 12px;
  330. height: 12px;
  331. border-radius: 3px;
  332. margin-right: 12px;
  333. }
  334. .legend-label {
  335. color: #d0d9e2;
  336. font-size: 14px;
  337. }
  338. ::v-deep .pulse-dot {
  339. width: 12px;
  340. height: 12px;
  341. background: #fff;
  342. border-radius: 50%;
  343. box-shadow: 0 0 10px #fff, 0 0 20px rgba(50, 197, 255, 0.5);
  344. }
  345. </style>
  346. <style>
  347. /* 弹窗样式 */
  348. .custom-info-card {
  349. background: rgba(5, 22, 45, 0.98);
  350. border: 1px solid #1e4d8e;
  351. border-radius: 6px;
  352. width: 240px;
  353. color: #fff;
  354. overflow: hidden;
  355. }
  356. .card-header {
  357. padding: 12px 15px;
  358. font-size: 14px;
  359. font-weight: bold;
  360. }
  361. .card-body {
  362. padding: 15px;
  363. font-size: 13px;
  364. }
  365. .info-row {
  366. margin-bottom: 8px;
  367. display: flex;
  368. }
  369. .info-row .label {
  370. color: #8da6c7;
  371. width: 70px;
  372. }
  373. .amap-info-content {
  374. background: transparent !important;
  375. border: none !important;
  376. padding: 0 !important;
  377. }
  378. .amap-info-sharp {
  379. display: none !important;
  380. }
  381. </style>