Просмотр исходного кода

feat: 首页通州交通地图功能开发完成

sequoia tungfang дней назад: 2
Родитель
Сommit
2a4491a925
4 измененных файлов с 395 добавлено и 2 удалено
  1. 6 0
      package-lock.json
  2. 1 0
      package.json
  3. 377 0
      src/components/TongzhouTrafficMap.vue
  4. 11 2
      src/views/Home.vue

+ 6 - 0
package-lock.json

@@ -8,6 +8,7 @@
       "name": "traffic-signal-dashboard-vue2",
       "version": "0.1.0",
       "dependencies": {
+        "@amap/amap-jsapi-loader": "^1.0.1",
         "axios": "^1.13.5",
         "cesium": "^1.105.1",
         "china-map-geojson": "^1.0.4",
@@ -46,6 +47,11 @@
         "node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22 || 23 || 24 || 25"
       }
     },
+    "node_modules/@amap/amap-jsapi-loader": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
+      "integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw=="
+    },
     "node_modules/@babel/code-frame": {
       "version": "7.29.0",
       "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "axios": "^1.13.5",
     "cesium": "^1.105.1",
     "china-map-geojson": "^1.0.4",

+ 377 - 0
src/components/TongzhouTrafficMap.vue

@@ -0,0 +1,377 @@
+<template>
+  <div class="map-wrapper">
+    <div ref="mapContainer" class="map-container"></div>
+
+    <div class="map-header" v-if="initialized">
+      <div class="search-form">
+        <input type="text" v-model="searchQuery" placeholder="请输入路段或设备名称" class="search-input"
+          @keyup.enter="handleSearch" />
+        <button class="search-btn" @click="handleSearch">查询</button>
+      </div>
+      <div class="action-box" @click="toggleAll">全选 ▾</div>
+    </div>
+
+    <div class="map-legend" v-if="initialized">
+      <div class="legend-title">图例</div>
+      <div class="legend-list">
+        <div v-for="item in legendConfig" :key="item.type" class="legend-item"
+          :class="{ 'is-hidden': !activeLegends.includes(item.type) }" @click="handleLegendClick(item.type)">
+          <span class="legend-dot" :style="{ backgroundColor: item.color }"></span>
+          <span class="legend-label">{{ item.label }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import AMapLoader from '@amap/amap-jsapi-loader';
+
+export default {
+  name: "TrafficMap",
+  props: {
+    amapKey: { type: String, default: '您的Key' },
+    securityJsCode: { type: String, default: '您的安全密钥' }
+  },
+  data() {
+    return {
+      AMap: null,
+      map: null,
+      infoWindow: null,
+      initialized: false,
+      searchQuery: '', // 搜索内容绑定
+      activeLegends: [],
+      legendConfig: [
+        { type: 'center_plan', label: '中心计划', color: '#32c5ff' },
+        { type: 'trunk_coord', label: '干线协调', color: '#3ee68d' },
+        { type: 'service_route', label: '勤务路线', color: '#ffcc33' },
+        { type: 'periodic', label: '定周期控制', color: '#00ccff' },
+        { type: 'induction', label: '感应控制', color: '#7ed3b2' },
+        { type: 'adaptive', label: '自适应控制', color: '#8ca1ff' },
+        { type: 'manual', label: '手动控制', color: '#cc8d66' },
+        { type: 'special', label: '特殊控制', color: '#ffb33b' },
+        { type: 'offline', label: '离线', color: '#6d7791' },
+        { type: 'degraded', label: '降级', color: '#c4a737' },
+        { type: 'fault', label: '故障', color: '#ff4d4f' }
+      ],
+      overlayGroups: {}
+    };
+  },
+  mounted() {
+    this.initAMap();
+  },
+  beforeDestroy() {
+    if (this.map) {
+      this.map.destroy();
+    }
+  },
+  methods: {
+    async initAMap() {
+      window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
+      try {
+        const AMap = await AMapLoader.load({
+          key: this.amapKey,
+          version: "2.0",
+          plugins: ['AMap.InfoWindow', 'AMap.Polyline', 'AMap.Marker']
+        });
+
+        this.AMap = AMap;
+        this.map = new AMap.Map(this.$refs.mapContainer, {
+          zoom: 13,
+          center: [116.6612, 39.9125],
+          mapStyle: "amap://styles/darkblue",
+          viewMode: "3D"
+        });
+
+        this.infoWindow = new AMap.InfoWindow({
+          isCustom: true,
+          offset: new AMap.Pixel(0, -15)
+        });
+
+        this.map.on("complete", () => {
+          this.initialized = true;
+          this.activeLegends = this.legendConfig.map(l => l.type);
+          this.drawTrafficScene();
+        });
+
+        this.map.on('click', () => this.infoWindow && this.infoWindow.close());
+
+      } catch (e) {
+        console.error("地图加载失败:", e);
+      }
+    },
+
+    // 搜索查询逻辑
+    handleSearch() {
+      if (!this.searchQuery.trim()) {
+        alert("请输入查询内容");
+        return;
+      }
+      console.log("正在执行搜索:", this.searchQuery);
+      // 这里可以扩展具体的搜索逻辑,比如搜索 POI 或在已有 overlayGroups 中高亮匹配项
+      alert(`已提交查询:${this.searchQuery}`);
+    },
+
+    drawTrafficScene() {
+      const roads = [
+        { type: 'center_plan', name: '新华南北路 - 中心控制段', path: [[116.665, 39.940], [116.665, 39.885]], hasDots: true },
+        { type: 'trunk_coord', name: '通胡大街 - 干线协调(北)', path: [[116.620, 39.930], [116.650, 39.920], [116.700, 39.930]] },
+        { type: 'trunk_coord', name: '运河东大街 - 干线协调(南)', path: [[116.615, 39.900], [116.660, 39.910], [116.710, 39.900]] },
+        { type: 'service_route', name: '新华东街 - 勤务专用线', path: [[116.625, 39.905], [116.700, 39.905]] },
+        { type: 'fault', name: '路县故城周边 - 设备异常', path: [[116.685, 39.920], [116.690, 39.905]] }
+      ];
+      roads.forEach(road => this.renderRoadWithStyle(road));
+    },
+
+    renderRoadWithStyle(road) {
+      const config = this.legendConfig.find(l => l.type === road.type);
+      const group = [];
+
+      const glow = new this.AMap.Polyline({
+        path: road.path, strokeColor: config.color, strokeOpacity: 0.15, strokeWeight: 20, map: this.map
+      });
+
+      const line = new this.AMap.Polyline({
+        path: road.path, strokeColor: config.color, strokeWeight: 6, map: this.map
+      });
+
+      [glow, line].forEach(item => {
+        item.on('click', (e) => this.openDetailWindow(road, e.lnglat));
+        group.push(item);
+      });
+
+      if (road.hasDots) {
+        const dots = this.calculatePathDots(road.path, 12);
+        dots.forEach((pos, index) => {
+          const marker = new this.AMap.Marker({
+            position: pos,
+            content: `<div class="pulse-dot"></div>`,
+            offset: new this.AMap.Pixel(-6, -6),
+            map: this.map
+          });
+          marker.on('click', (e) => this.openDetailWindow({ ...road, name: `${road.name}-监测点${index + 1}` }, e.lnglat));
+          group.push(marker);
+        });
+      }
+      this.overlayGroups[road.type] = (this.overlayGroups[road.type] || []).concat(group);
+    },
+
+    calculatePathDots(path, count) {
+      const p1 = path[0], p2 = path[1];
+      const dots = [];
+      for (let i = 1; i < count; i++) {
+        dots.push([p1[0] + (p2[0] - p1[0]) * (i / count), p1[1] + (p2[1] - p1[1]) * (i / count)]);
+      }
+      return dots;
+    },
+
+    openDetailWindow(data, lnglat) {
+      const config = this.legendConfig.find(l => l.type === data.type);
+      const content = `
+        <div class="custom-info-card">
+          <div class="card-header" style="background: ${config.color}22; border-left: 4px solid ${config.color}">
+            <span class="title">${data.name}</span>
+          </div>
+          <div class="card-body">
+            <div class="info-row"><span class="label">管控类型:</span><span class="value" style="color:${config.color}">${config.label}</span></div>
+            <div class="info-row"><span class="label">当前状态:</span><span class="value" style="color:#3ee68d">运行中</span></div>
+          </div>
+        </div>
+      `;
+      this.infoWindow.setContent(content);
+      this.infoWindow.open(this.map, lnglat);
+    },
+
+    handleLegendClick(type) {
+      const isVisible = this.activeLegends.includes(type);
+      this.activeLegends = isVisible ? this.activeLegends.filter(t => t !== type) : [...this.activeLegends, type];
+      const group = this.overlayGroups[type];
+      if (group) group.forEach(o => isVisible ? o.hide() : o.show());
+      if (isVisible) this.infoWindow.close();
+    },
+
+    toggleAll() {
+      const allTypes = this.legendConfig.map(l => l.type);
+      const isShowingAll = this.activeLegends.length === allTypes.length;
+      allTypes.forEach(type => {
+        const group = this.overlayGroups[type];
+        if (group) group.forEach(o => isShowingAll ? o.hide() : o.show());
+      });
+      this.activeLegends = isShowingAll ? [] : allTypes;
+      this.infoWindow.close();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.map-wrapper {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  min-height: 500px;
+  background: #010813;
+  overflow: hidden;
+}
+
+.map-container {
+  width: 100%;
+  height: 100%;
+}
+
+/* 头部样式:增加了搜索表单布局 */
+.map-header {
+  position: absolute;
+  top: 20px;
+  left: 20px;
+  right: 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  z-index: 10;
+}
+
+/* 搜索表单容器 */
+.search-form {
+  display: flex;
+  background: rgba(13, 35, 67, 0.9);
+  border: 1px solid #1a4a8d;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.search-input {
+  background: transparent;
+  border: none;
+  padding: 10px 15px;
+  color: #fff;
+  width: 240px;
+  outline: none;
+  font-size: 13px;
+}
+
+.search-input::placeholder {
+  color: #5b7da8;
+}
+
+.search-btn {
+  background: #1a4a8d;
+  border: none;
+  color: #fff;
+  padding: 0 20px;
+  cursor: pointer;
+  font-size: 13px;
+  transition: background 0.2s;
+}
+
+.search-btn:hover {
+  background: #2660b3;
+}
+
+.action-box {
+  background: rgba(13, 35, 67, 0.9);
+  border: 1px solid #1a4a8d;
+  padding: 10px 18px;
+  color: #fff;
+  border-radius: 4px;
+  font-size: 13px;
+  cursor: pointer;
+}
+
+/* 图例与其它样式保持不变 */
+.map-legend {
+  position: absolute;
+  right: 25px;
+  bottom: 40px;
+  width: 170px;
+  background: rgba(8, 20, 36, 0.95);
+  border: 1px solid rgba(38, 74, 124, 0.8);
+  border-radius: 12px;
+  padding: 18px;
+  z-index: 100;
+  box-shadow: 0 0 25px rgba(0, 0, 0, 0.6);
+}
+
+.legend-title {
+  color: #fff;
+  font-size: 18px;
+  margin-bottom: 18px;
+  font-weight: bold;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 14px;
+  cursor: pointer;
+  transition: 0.2s;
+}
+
+.legend-item.is-hidden {
+  opacity: 0.2;
+  filter: grayscale(1);
+}
+
+.legend-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 3px;
+  margin-right: 12px;
+}
+
+.legend-label {
+  color: #d0d9e2;
+  font-size: 14px;
+}
+
+::v-deep .pulse-dot {
+  width: 12px;
+  height: 12px;
+  background: #fff;
+  border-radius: 50%;
+  box-shadow: 0 0 10px #fff, 0 0 20px rgba(50, 197, 255, 0.5);
+}
+</style>
+
+<style>
+/* 弹窗样式 */
+.custom-info-card {
+  background: rgba(5, 22, 45, 0.98);
+  border: 1px solid #1e4d8e;
+  border-radius: 6px;
+  width: 240px;
+  color: #fff;
+  overflow: hidden;
+}
+
+.card-header {
+  padding: 12px 15px;
+  font-size: 14px;
+  font-weight: bold;
+}
+
+.card-body {
+  padding: 15px;
+  font-size: 13px;
+}
+
+.info-row {
+  margin-bottom: 8px;
+  display: flex;
+}
+
+.info-row .label {
+  color: #8da6c7;
+  width: 70px;
+}
+
+.amap-info-content {
+  background: transparent !important;
+  border: none !important;
+  padding: 0 !important;
+}
+
+.amap-info-sharp {
+  display: none !important;
+}
+</style>

+ 11 - 2
src/views/Home.vue

@@ -2,7 +2,14 @@
   <DashboardLayout>
     <!-- 地图 -->
     <template #map>
-      <div id="map-container" class="map-layer"></div>
+      <div id="map-container" class="map-layer">
+        <div class="map-canvas" :style="mapBgStyle">
+              <TongzhouTrafficMap
+              amapKey="db2da7e3e248c3b2077d53fc809be63f" 
+              securityJsCode="a7413c674852c5eaf01d90813c5b7ef6"
+              />
+        </div>
+      </div>
     </template>
     <!-- 天气 -->
     <template #header-left>
@@ -150,6 +157,7 @@ import AlarmMessageList from '@/components/ui/AlarmMessageList.vue';
 import TechTable from '@/components/ui/TechTable.vue';
 import MapLegend from '@/components/ui/MapLegend.vue';
 import DropdownSelect from '@/components/ui/DropdownSelect.vue';
+import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 
 
 const mockDeviceData = {
@@ -194,7 +202,8 @@ export default {
     AlarmMessageList,
     TechTable,
     MapLegend,
-    DropdownSelect
+    DropdownSelect,
+    TongzhouTrafficMap
   },
   data() {
     return {