Unverified Commit d61f833e authored by Sparkf's avatar Sparkf 🏙️
Browse files

add ETA to RealtimeMapv2

parent e9da4a28
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ import BusAnnouncement from "./components/BusAnnouncement.vue";
import BusETAdemo from "./components/BusETAdemo.vue";
import TabView from "./components/TabView.vue";
import RealtimeMap from './components/RealtimeMap.vue'
import RealtimeMapv2 from './components/RealtimeMapv2.vue'
import CampusMap from './components/CampusMap.vue'
import WeatherSpan from './components/weather-span.vue'
import BusChartVue from './components/BusChartVue.vue'
@@ -20,6 +21,7 @@ export default defineClientConfig({
    app.component("BusETAdemo", BusETAdemo)
    app.component("TabView", TabView)
    app.component("RealtimeMap", RealtimeMap)
    app.component("RealtimeMapv2", RealtimeMapv2)
    app.component("CampusMap", CampusMap)
    app.component("WeatherSpan", WeatherSpan)
    app.component("AdSenseInline", AdSenseInline)
+2 −0
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ import {
  ConfigProvider,
  Select,
  Card,
  Collapse,
  Spin,
  Empty,
  Tag,
@@ -71,6 +72,7 @@ export default {
    'a-config-provider': ConfigProvider,
    'a-select': Select,
    'a-card': Card,
    'a-collapse': Collapse,
    'a-spin': Spin,
    'a-empty': Empty,
    'a-tag': Tag,
+0 −21
Original line number Diff line number Diff line
@@ -25,27 +25,6 @@ export default {
    map_text_colour: "#000000",
    bus_location_data_api: [],
    bus_location_data_display: [],
    bus_plate_hash: {
      "298": {"plate": "粤BDF298"},
      "363": {"plate": "粤BDF363"},
      "8040": {"plate": "粤BDF040"},
      "8147": {"plate": "粤BDF147"},
      "8267": {"plate": "粤BDF267"},
      "8330": {"plate": "粤BDF330"},
      "8335": {"plate": "粤BDF335"},
      "8338": {"plate": "粤BDF338"},
      "8345": {"plate": "粤BDF345"},
      "8365": {"plate": "粤BDF365"},
      "8371": {"plate": "粤BDF371"},
      "8411": {"plate": "粤BDF411"},
      "8421": {"plate": "粤BDF421"},
      "8430": {"plate": "粤BDF430"},
      "8447": {"plate": "粤BDE447"},
      "8458": {"plate": "粤BDF458"},
      "8470": {"plate": "粤BDF470"},
      "8471": {"plate": "粤BDF471"},
      "18447": {"plate": "粤BDF447"}
    },
    geojson_line_1: [[113.99739584160139, 22.610765716856296], [113.99773288526481, 22.610577804306548], [113.99824514560808, 22.609808813574297], [113.99899240486543, 22.608186143427105], [113.99899684055141, 22.608138484311375], [113.99876376465981, 22.606584405046835], [113.9987454049567, 22.606539955896604], [113.9983737643716, 22.606046923598996], [113.99771305207776, 22.60586005946536], [113.996462081496, 22.60628164325351], [113.99540699384544, 22.60655007340282], [113.99537457846512, 22.606374521317747], [113.99533345805837, 22.60631869878032], [113.99491868014185, 22.60615102642152], [113.99446803578164, 22.605922922486105], [113.993628105381, 22.605432281601264], [113.99361283376653, 22.605399929510398], [113.99427914343141, 22.604297776694622], [113.99455101208882, 22.604066349285663], [113.99481121659123, 22.60381079129223], [113.99549648383878, 22.60304833084697], [113.99570726599393, 22.60298417975627], [113.99623267681147, 22.60298349183956], [113.99686127008054, 22.603078201644227], [113.9977201426685, 22.603401409146592], [113.99788215074415, 22.603408536156227], [113.99794417914393, 22.60338145776082], [113.9980463074806, 22.603268395934986], [113.99816665521513, 22.602614977726315], [113.99835030416712, 22.60216721735362], [113.99856193554633, 22.601842951121434], [113.99881928525701, 22.6015215956419], [113.9988344317295, 22.601194082723367], [113.99895072783376, 22.600790214181995], [113.9990602569799, 22.600602634774226], [113.99921879840926, 22.600476730796977], [113.99919877407567, 22.60043325336671], [113.99802751236375, 22.600038565633355], [113.99775839514615, 22.599792681543633], [113.99730187808832, 22.598960946075003], [113.99590977592047, 22.5977773063799], [113.99576459924215, 22.59751983095157], [113.99543299558397, 22.59717440812865], [113.9951938686417, 22.5966914370548], [113.99501583215859, 22.59639577584673], [113.99463229185628, 22.596104860560743], [113.99420385593578, 22.595969591352997], [113.99378584507669, 22.595967937443213], [113.99320018866534, 22.59621976224022], [113.99265363550316, 22.596738860232236], [113.99200633643709, 22.59716671060916], [113.99157366427197, 22.5971462468509], [113.99088482201618, 22.596941863390235], [113.99038484213679, 22.596998494112995], [113.99032102143205, 22.597043977909916], [113.99019829041364, 22.59753578294312], [113.99030286538937, 22.59863964935582], [113.99048378356784, 22.59907317753902], [113.99052006299269, 22.59911721823263], [113.99075599101839, 22.599276010611053], [113.99126127611241, 22.599535900836155], [113.9917686585336, 22.6000105337692], [113.99191211157755, 22.60053218120178], [113.99183362959826, 22.601016496848214], [113.99144872111891, 22.60153952301182], [113.99077751816642, 22.602118998056795], [113.99043902651476, 22.60239072790739], [113.99010219314502, 22.60332391993846]],
    geojson_line_2: [[113.9973489079054, 22.61067741494136], [113.9976555155949, 22.61051444868976], [113.9981618912118, 22.60975341767644], [113.9988979465801, 22.60815331611633], [113.9986648706885, 22.60659923685179], [113.9983207115444, 22.60613169032316], [113.9977156537446, 22.60596002561628], [113.9964905851169, 22.6063775771138], [113.9953277666447, 22.60667341560261], [113.995278497141, 22.60640224097063], [113.9934784038899, 22.60548092977822], [113.99386414857948, 22.604784676421257], [113.99419945106074, 22.60426804504929], [113.99385306765333, 22.603677155592816], [113.99298710913483, 22.60350336409337], [113.99279885728298, 22.603280910653833], [113.99233952276445, 22.60296808489611], [113.99204584987558, 22.60262745159568], [113.9915262747645, 22.601612498395962], [113.9917196221943, 22.601431625928], [113.9919172423369, 22.60102266627295], [113.9920105698998, 22.6005318195689], [113.9918969233072, 22.59993661748595], [113.9914596783045, 22.59960492109923]],
    gate_geojson: {},
+575 −0
Original line number Diff line number Diff line
<template>
  <div class="map-wrapper">
    <div class="map-container" ref="mapContainer"></div>
    <span v-if="timeDrift" class="time-drift-display">Time Drift: {{ timeDrift }}s</span>
  </div>
</template>

<script>
import maplibre from 'maplibre-gl';
import axios from 'axios';
import * as turf from '@turf/turf';
import { Protocol } from 'pmtiles';

export default {
  name: 'BusMap',

  data() {
    return {
      // --- Configuration ---
      apiBaseUrl_RealTimeData: 'https://bus.sustcra.com/api/v2',
      apiBaseUrl: 'https://bus.sustcra.com/api/v3',
      map: null,
      activePopup: null,

      // --- Map Styles & Assets ---
      mapStyleLight: 'https://bus.sustcra.com/static/protomaps/pmtiles-style/pmtiles-light.json',
      mapStyleDark: 'https://bus.sustcra.com/static/protomaps/pmtiles-style/pmtiles-dark.json',
      mapTextColor: '#000000',
      mapTextHaloColor: '#FFFFFF', // 为文字描边颜色创建一个状态

      // --- Data Placeholders ---
      // 在这里填入你的线路GeoJSON坐标数据
      geojson_XYBS1: [],
      geojson_XYBS2: [],
      geojson_SEV1: [],

      // --- State Management ---
      busLocations: [],
      busMarkers: [],
      unifiedStationsLookup: {}, // { "lng,lat": { name, routes: [{...}] } }
      allStationsGeoJSON: { type: 'FeatureCollection', features: [] },
      bldgGeoJSON: {},
      gateGeoJSON: {},
      timeDrift: 0,
      busUpdateTimer: null,
      themeChangeHandler: null, // 用于存储主题变化的处理函数引用
    };
  },

  methods: {
    // --- 1. Initialization and Map Setup ---
    initializeMap(styleUrl) {
      const pmtilesProtocol = new Protocol();
      maplibre.addProtocol('pmtiles', pmtilesProtocol.tile);

      this.map = new maplibre.Map({
        container: this.$refs.mapContainer,
        style: styleUrl,
        center: [113.99373, 22.60308],
        zoom: 14.5,
        minZoom: 13,
      });

      this.setupMapControls();
    },

    setupMapControls() {
      this.map.addControl(new maplibre.NavigationControl(), 'top-left');
      this.map.addControl(new maplibre.FullscreenControl(), 'top-left');
      this.map.addControl(new maplibre.GeolocateControl({
        positionOptions: { enableHighAccuracy: true },
        trackUserLocation: true,
        showUserHeading: true,
      }), 'top-right');
      // 可以添加你之前的自定义控件,这里为了简洁暂时省略
    },

    // --- 2. Data Loading ---
    async loadMapData() {
      // 并发加载所有数据
      await Promise.all([
        this.loadAllStationData(),
        this.loadBuildingAndGateData(),
        this.loadGeoJSONLines(),
      ]);
      this.loadMapIconsAndLayers();
    },

    async loadAllStationData() {
      console.log('Fetching all station data...');
      try {
        const routesResponse = await axios.get(`${this.apiBaseUrl}/avail_route`);
        const allRoutes = routesResponse.data.routes;

        const stationPromises = allRoutes.map(route =>
            axios.get(`${this.apiBaseUrl}/${route.name}/${route.direction}/stations`)
        );

        const stationResults = await Promise.all(stationPromises);

        stationResults.forEach((result, index) => {
          const routeInfo = allRoutes[index];
          const features = result.data.features || [];

          features.forEach(feature => {
            const coords = feature.geometry.coordinates;
            const coordKey = coords.join(',');
            const stationName = feature.properties.name.replace(/\n/g, ' ');

            // 填充站点反向查找表
            if (!this.unifiedStationsLookup[coordKey]) {
              this.unifiedStationsLookup[coordKey] = {
                name: stationName,
                routes: [],
              };
            }
            this.unifiedStationsLookup[coordKey].routes.push({
              route_code: routeInfo.name,
              direction: routeInfo.direction,
              station_id: feature.properties.station_id,
            });

            // 添加唯一的站点到GeoJSON中用于显示
            // 检查是否已存在相同坐标的站点,避免重复图标
            if (!this.allStationsGeoJSON.features.some(f => f.geometry.coordinates.join(',') === coordKey)) {
              // 为了点击事件能获取到正确的坐标,我们把它也存入properties
              feature.properties.coordKey = coordKey;
              feature.properties.name = stationName;
              this.allStationsGeoJSON.features.push(feature);
            }
          });
        });

        console.log('Unified station database created.');
      } catch (error) {
        console.error('Failed to load station data:', error);
      }
    },

    async loadBuildingAndGateData() {
      try {
        const [bldgRes, gateRes] = await Promise.all([
          axios.get('https://bus.sustcra.com/geojson/sustech_bldg.json'),
          axios.get('https://bus.sustcra.com/geojson/sustech_gate.json')
        ]);
        this.bldgGeoJSON = bldgRes.data;
        this.gateGeoJSON = gateRes.data;
      } catch(error) {
        console.error("Failed to fetch building or gate geojson:", error);
      }
    },

    async loadMapIconsAndLayers() {
      const loadImage = (url) => this.map.loadImage(url);
      try {
        const [stationImg, bldgImg, gateImg] = await Promise.all([
          loadImage('https://bus.sustcra.com/station_icon.png'),
          loadImage('https://bus.sustcra.com/bldg_icon.png'),
          loadImage('https://bus.sustcra.com/gate_icon.png')
        ]);

        this.map.addImage('bus-station-icon', stationImg.data);
        this.map.addImage('bldg-icon', bldgImg.data);
        this.map.addImage('gate-icon', gateImg.data);

        this.setupMapLayers();
      } catch (error) {
        console.error('Error loading map icons:', error);
      }
    },

    async loadGeoJSONLines() {
      try {
        const [XYBS1Res, XYBS2Res, SEV1Res] = await Promise.all([
          axios.get('https://bus.sustcra.com/static/lines/XYBS1.json'),
          axios.get('https://bus.sustcra.com/static/lines/XYBS2.json'),
          axios.get('https://bus.sustcra.com/static/lines/SEV1.json'),
        ]);
        // console.log('XYBS1:', XYBS1Res.data);
        // console.log('XYBS2:', XYBS2Res.data);
        // console.log('SEV1:', SEV1Res.data);
        this.geojson_XYBS1 = XYBS1Res.data;
        this.geojson_XYBS2 = XYBS2Res.data;
        this.geojson_SEV1 = SEV1Res.data;
      } catch (error) {
        console.error("Failed to fetch GeoJSON lines:", error);
      }
    },


    setupMapLayers() {
      // 添加线路图层
      this.addRouteLayer('line1', this.geojson_XYBS1, '#f7911d');
      this.addRouteLayer('line2', this.geojson_XYBS2, '#29abe2');
      this.addRouteLayer('line3', this.geojson_SEV1, '#7030a1');

      // 添加站点、建筑和校门图层
      this.addSymbolLayer('stations', this.allStationsGeoJSON, 'bus-station-icon', 0.075, [0, 1.25]);
      this.addSymbolLayer('buildings', this.bldgGeoJSON, 'bldg-icon', 0.02, [0, 0.3], 11);
      this.addSymbolLayer('gates', this.gateGeoJSON, 'gate-icon', 0.05, [0, 0.6]);
    },

    // --- 3. Bus Tracking ---
    startBusTracking() {
      this.fetchBusLocations(); // 立即执行一次
      this.busUpdateTimer = setInterval(this.fetchBusLocations, 5000); // 每5秒刷新
    },

    // 计算公交车的方位角
    calculateBusAngle(startLat, startLng, destLat, destLng) {
      // 创建 Turf.js 的点
      const startPoint = turf.point([startLng, startLat]);
      const endPoint = turf.point([destLng, destLat]);

      // 计算方位角
      const bearing = turf.rhumbBearing(startPoint, endPoint);

      // Turf.js 返回的方位角是从北顺时针的角度
      return bearing;
    },
    findNearestPointOnSegment(point, segmentStart, segmentEnd) {
      const A = point[0] - segmentStart[0];
      const B = point[1] - segmentStart[1];
      const C = segmentEnd[0] - segmentStart[0];
      const D = segmentEnd[1] - segmentStart[1];

      const dot = A * C + B * D;
      const len_sq = C * C + D * D;
      const param = (len_sq !== 0) ? dot / len_sq : -1;

      let xx, yy;

      if (param < 0) {
        xx = segmentStart[0];
        yy = segmentStart[1];
      } else if (param > 1) {
        xx = segmentEnd[0];
        yy = segmentEnd[1];
      } else {
        xx = segmentStart[0] + param * C;
        yy = segmentStart[1] + param * D;
      }

      return [xx, yy];
    },
    findNearestSegment(busLocation, geojsonLine) {
      let closestSegmentStart = geojsonLine[0];
      let closestSegmentEnd = geojsonLine[1];
      let minDistance = Number.MAX_VALUE;

      // 遍历 GeoJSON 线路的每个线段
      for (let i = 0; i < geojsonLine.length - 1; i++) {
        const segmentStart = geojsonLine[i];
        const segmentEnd = geojsonLine[i + 1];
        const nearestPoint = this.findNearestPointOnSegment(busLocation, segmentStart, segmentEnd);
        const distance = this.calculateDistance(busLocation, nearestPoint);

        // 更新最近线段
        if (distance < minDistance) {
          minDistance = distance;
          closestSegmentStart = segmentStart;
          closestSegmentEnd = segmentEnd;
        }
      }

      // 确保线段的顺序与 GeoJSON 中的一致
      return [closestSegmentStart, closestSegmentEnd];
    },
    calculateDistance(point1, point2) {
      return Math.sqrt(Math.pow(point2[0] - point1[0], 2) + Math.pow(point2[1] - point1[1], 2));
    },

    async fetchBusLocations() {
      try {
        const response = await axios.get(`https://bus.sustcra.com/api/v2/monitor_osm/`);
        this.busLocations = response.data;
        const now = Date.now() / 1000;
        if (this.busLocations.length > 0 && this.busLocations[0].time_mt) {
          this.timeDrift = Math.round(now - this.busLocations[0].time_mt);
        }
        this.updateBusMarkers();
      } catch (error) {
        console.error("Failed to fetch bus locations:", error);
      }
    },

    updateBusMarkers() {
      this.busMarkers.forEach(marker => marker.remove());
      this.busMarkers = [];

      const now = Date.now() / 1000;
      this.busLocations.forEach(bus => {
        if (now - bus.time_mt < 150) { // 只显示150秒内有数据上报的车辆
          const busEl = document.createElement('div');
          busEl.className = 'bus-marker';

          const marker = new maplibre.Marker({ element: busEl, anchor: 'center' })
              .setLngLat([bus.lng, bus.lat])
              .setPopup(this.createBusInfoPopup(bus))
              .setRotation(bus.heading || 0)
              .addTo(this.map);

          this.busMarkers.push(marker);
        }
      });
    },

    // --- 4. Event Handlers & Interaction ---
    setupMapListeners() {
      this.map.on('click', 'stations-layer', this.onStationClick);
      this.map.on('mouseenter', 'stations-layer', () => this.map.getCanvas().style.cursor = 'pointer');
      this.map.on('mouseleave', 'stations-layer', () => this.map.getCanvas().style.cursor = '');
    },

    async onStationClick(e) {
      if (e.features.length === 0) return;

      const feature = e.features[0];
      const coordKey = feature.properties.coordKey;
      const stationInfo = this.unifiedStationsLookup[coordKey];
      const coordinates = feature.geometry.coordinates.slice();

      if (!stationInfo) return;

      // 如果已有弹窗,先关闭
      if (this.activePopup) {
        this.activePopup.remove();
      }

      const popupContent = document.createElement('div');
      popupContent.innerHTML = `
        <div class="popup-header">${stationInfo.name}</div>
        <div class="popup-body">正在查询...</div>
      `;

      this.activePopup = new maplibre.Popup({ offset: 25, closeButton: true })
          .setLngLat(coordinates)
          .setDOMContent(popupContent)
          .addTo(this.map);

      // 异步获取ETA数据
      const etas = await this.fetchEtasForStation(stationInfo);

      // 更新弹窗内容
      const popupBody = popupContent.querySelector('.popup-body');
      popupBody.innerHTML = this.createEtaListHtml(etas);
    },

    async fetchEtasForStation(stationInfo) {
      const etaPromises = stationInfo.routes.map(route =>
          axios.get(`${this.apiBaseUrl}/${route.route_code}/${route.direction}/${route.station_id}`)
      );

      try {
        const results = await Promise.all(etaPromises);
        return results.map(res => res.data).flat().sort((a,b) => a.eta_minutes - b.eta_minutes);
      } catch (error) {
        console.error("Failed to fetch ETAs:", error);
        return [];
      }
    },

    // --- 5. Helper Functions ---
    addRouteLayer(id, geojsonData, color) {
      // 添加 GeoJSON source
      this.map.addSource(id, {
        type: 'geojson',
        data: geojsonData
      });

      // 添加线图层
      this.map.addLayer({
        id: id + '-layer',
        type: 'line',
        source: id,
        layout: { 'line-join': 'round', 'line-cap': 'round' },
        paint: { 'line-color': color, 'line-width': 3 }
      });
    },


    addSymbolLayer(id, geojson, iconImage, iconSize, textOffset, textSize = 10) {
      this.map.addSource(id, { type: 'geojson', data: geojson });
      this.map.addLayer({
        id: id + '-layer', type: 'symbol', source: id,
        layout: {
          'icon-image': iconImage, 'icon-size': iconSize, 'icon-allow-overlap': true,
          'text-field': ['get', 'name'], 'text-size': textSize, 'text-offset': textOffset,
          'text-anchor': 'top',
        },
        paint: {
          'text-color': this.mapTextColor,
          'text-halo-color': this.mapTextHaloColor,
          'text-halo-width': 1
        }
      });
    },

    createBusInfoPopup(bus) {
      const directionMapL1 = { '0': '欣园 Joy Highland', '1': '工学院 COE' };
      const directionMapL2 = { '0': '欣园 Joy Highland', '1': '科研楼' };
      const lineNum = bus.route_code.slice(-1);
      const direction = lineNum === '1' ? directionMapL1[bus.route_dir] : directionMapL2[bus.route_dir];
      const color = lineNum === '1' ? '#f7911d': '#29abe2';

      const html = `
        <div class="bus-popup">
          <div class="plate">粤B${bus.id.slice(2)} (${bus.speed} km/h)</div>
          <div><span class="line-tag" style="background-color:${color}">Line ${lineNum}</span> To: <strong>${direction}</strong></div>
          <div>Next: <strong>${bus.next_station_string}</strong></div>
        </div>
      `;
      return new maplibre.Popup({ offset: 20 }).setHTML(html);
    },

    createEtaListHtml(etas) {
      if (etas.length === 0) {
        return '<div class="eta-item">暂无班车信息</div>';
      }

      return etas.map(eta => {
        const lineNum = eta.route_key.split('_')[0].slice(-1);
        const color = lineNum === '1' ? '#f7911d' : '#29abe2';
        return `
                <div class="eta-item">
                    <span class="line-tag" style="background-color:${color};">L${lineNum}</span>
                    <span class="plate-tag">粤B${eta.plate.slice(2)}</span>
<!--                    <span>下站 <strong>${eta.next_station}</strong></span>-->
                    <span class="eta-time">${eta.eta_minutes} Min.</span>
                </div>
            `;
      }).join('');
    },

    // --- 6. Lifecycle Hooks ---
    setupThemeListener() {
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

      // 根据当前主题设置文字和描边颜色
      if (mediaQuery.matches) {
        this.mapTextColor = '#FFFFFF';
        this.mapTextHaloColor = '#000000'; // 夜间模式使用黑色描边
      } else {
        this.mapTextColor = '#000000';
        this.mapTextHaloColor = '#FFFFFF'; // 日间模式使用白色描边
      }

      this.themeChangeHandler = (e) => {
        console.log(`Theme changed to ${e.matches ? 'Dark' : 'Light'}. Map will reload.`);
        window.location.reload();
      };
      mediaQuery.addEventListener('change', this.themeChangeHandler);
    }
  },

  async mounted() {
    this.setupThemeListener();

    const styleUrl = this.mapTextColor === '#FFFFFF' ? this.mapStyleDark : this.mapStyleLight;
    this.initializeMap(styleUrl);

    this.map.on('load', async () => {
      console.log('Map loaded.');
      await this.loadMapData();
      this.setupMapListeners();
      this.startBusTracking();
    });
  },

  unmounted() {
    clearInterval(this.busUpdateTimer);
    if (this.themeChangeHandler) {
      window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.themeChangeHandler);
    }
    if (this.map) {
      this.map.remove();
    }
  },
};
</script>

<style>
/* Global styles for map elements */
.map-wrapper {
  position: relative;
  width: 100%;
  height: 50vh; /* Give it a good height */
}

.map-container {
  width: 100%;
  height: 100%;
}

.time-drift-display {
  position: absolute;
  bottom: 10px;
  left: 10px;
  background-color: rgba(255, 255, 255, 0.7);
  color: #333;
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 12px;
  z-index: 10;
}

/* --- Bus Marker Style --- */
.bus-marker {
  width: 25px;
  height: 25px;
  background-image: url('https://bus.sustcra.com/bus-icon-view.png');
  background-size: contain;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* --- Popup Styles --- */
.maplibregl-popup-content {
  padding: 10px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}

.bus-popup {
  font-size: 13px;
  line-height: 1.6;
}
.bus-popup .plate {
  font-weight: bold;
  font-size: 15px;
}

.popup-header {
  color: black;
  font-size: 15px;
  font-weight: bold;
  border-bottom: 1px solid #eee;
  padding-bottom: 5px;
  margin-bottom: 8px;
}
.popup-body {
  color: black;
  max-height: 200px;
  overflow-y: auto;
}

.eta-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 13px;
  border-bottom: 1px solid #f0f0f0;
}
.eta-item:last-child {
  border-bottom: none;
}
.eta-item .eta-time {
  margin-left: auto;
  font-weight: bold;
  color: #f7911d;
}

.line-tag, .plate-tag {
  color: white;
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: bold;
  flex-shrink: 0;
}
.plate-tag {
  background-color: #555;
}
</style>
 No newline at end of file
+1 −1
Original line number Diff line number Diff line
@@ -15,7 +15,7 @@
      <div v-if="currentSelect === 'bus-location'">
        <div class="bus-location-hint" v-if="showMapChart"><b>位置每5秒自动刷新。</b>Location refreshes automatically every 5 seconds. <br><b>电瓶车暂未安装定位模块,地图中仅显示公交车的位置。</b>Shuttle Electric Vehicle (SEV) are not equipped with GPS modules yet, so only the Bus locations are shown on the map.
        </div>
        <RealtimeMap v-if="showMapChart" />
        <RealtimeMapv2 v-if="showMapChart" />
        <BusETAdemo />
        <BusChartVue />
      </div>