zjk
2026-04-16 6efebad511514f0bbe79994c1074949536411ffa
feat(share-trip): 新增行程分享页面的UI与状态管理
2个文件已添加
3个文件已修改
1099 ■■■■■ 已修改文件
src/robotaxi/assets/share-contact.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/share-line.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.less 562 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.tsx 533 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/share-contact.png
src/robotaxi/assets/share-line.png
src/robotaxi/pages/share-trip/index.less
@@ -6,6 +6,59 @@
  position: relative;
}
.share-trip-expired {
  width: 100vw;
  height: 100vh;
  background: #ffffff;
  display: flex;
  flex-direction: column;
  .expired-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding-top: 408px;
    .expired-img {
      width: 254px;
      height: 254px;
      margin-bottom: 18px;
    }
    .expired-text {
      font-size: 30px;
      line-height: 46px;
      color: rgba(0, 0, 0, 0.4);
    }
    .expired-sub-text {
      font-size: 30px;
      line-height: 46px;
      color: rgba(0, 0, 0, 0.4);
    }
    .expired-btn {
      width: 264px;
      height: 96px;
      /* 自动布局 */
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      border-radius: 16px;
      box-sizing: border-box;
      /* 按钮色/面性线性按钮 */
      border: 2px solid #181916;
      padding: 25px 0px;
      font-size: 30px;
      line-height: 46px;
      color: #181916;
      margin-top: 74px;
    }
  }
}
.select-city {
  position: fixed;
  top: 96px;
@@ -30,6 +83,73 @@
  &-inner {
    position: relative;
  }
}
.share-contact {
  position: absolute;
  bottom: calc(100% + 28px);
  right: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  border-radius: 16px;
  background: #ffffff;
  box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
  z-index: 11;
  transition: bottom 0.3s ease; /* 可选,平滑过渡效果 */
  &.has-tip {
    bottom: calc(100% + 74px + 28px); /* tip-box 向上偏移了 74px */
  }
  .share-contact-icon {
    /* 自动布局子元素 */
    width: 64px;
    height: 64px;
    z-index: 0;
  }
}
.car-time {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  font-size: 24px;
  color: rgba(0, 0, 0, 0.9);
}
.car-time-num {
  font-size: 36px;
  line-height: 40px;
  color: #6d7fd9;
}
.car-time-tip {
  margin-left: 6px;
  font-size: 24px;
  line-height: 34px;
  color: rgba(0, 0, 0, 0.9);
}
.calling-car-head {
  position: relative;
  z-index: 9;
  font-size: 32px;
  font-weight: 500;
  color: rgba(0, 0, 0, 0.9);
  .calling-car-head-title {
    display: flex;
    align-items: center;
  }
  .calling-car-head-title-eta {
    font-size: 40px;
    font-weight: 500;
    color: #6d7fd9;
    margin: 0 4px;
    line-height: 44px;
  }
}
@@ -83,7 +203,7 @@
.calling-car {
  position: relative;
  z-index: 9;
  padding: 40px 42px 88px 34px;
  padding: 40px 24px 80px 24px;
  background: #f2f3f7;
  border-top: 2px solid #fff;
  border-radius: 40px 40px 0 0;
@@ -158,16 +278,18 @@
  .car-card {
    position: relative;
    z-index: 1;
    margin-top: 28px;
    margin-top: 32px;
    border-radius: 24px;
    background: #ffffff;
    padding: 31.29px 32px 36px 33px;
    padding: 32px 16px 32px 36px;
    .car-card-line {
      margin-top: 29.29px;
      margin-bottom: 36px;
      height: 1px;
      background-color: rgba(0, 0, 0, 0.07);
      margin-top: 11px;
      margin-bottom: 29px;
      margin-left: 6px;
      width: 624px;
      height: 0.01px;
      background-color: rgba(0, 0, 0, 0.1);
    }
    &-detail {
@@ -179,6 +301,7 @@
        font-size: 48px;
        line-height: 64px;
        letter-spacing: normal;
        font-weight: 500;
        color: rgba(0, 0, 0, 0.9);
      }
@@ -201,243 +324,222 @@
      &-img {
        flex-shrink: 0;
        width: 300px;
        height: 141.43px;
        height: 140px;
      }
    }
    &-temp {
      display: flex;
      align-items: center;
      justify-content: space-between;
    .car-card-temp {
      &-list {
        width: 100%;
        padding-right: 22px;
        display: flex;
        flex-direction: column;
        position: relative;
      }
      &-title {
        font-size: 28px;
      &-img {
        position: absolute;
        top: 28px;
        left: 5px;
        width: 2px;
        height: 47px;
        transform: translateX(50%);
      }
      &-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        &-left {
          display: flex;
          align-items: center;
          .dot {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            margin-right: 12px;
            flex-shrink: 0;
            &.start {
              background: #768aff;
            }
            &.end {
              background: #8bb654;
            }
          }
        }
      }
      .car-card-temp-title {
        font-size: 24px;
        line-height: 40px;
        letter-spacing: normal;
        color: rgba(0, 0, 0, 0.9);
      }
      &-content {
        display: flex;
        align-items: center;
        gap: 24px;
        &-text {
          font-size: 56px;
          line-height: 72px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.9);
          text-align: center;
        }
        &-img {
          width: 56px;
          height: 56px;
        }
      }
    }
    &-arrive {
      margin-top: 36px;
      display: flex;
      align-items: center;
      gap: 30px;
      height: 156px;
      text-align: center;
      font-size: 24px;
      line-height: 40px;
      color: rgba(0, 0, 0, 0.9);
      &-item {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 8px;
        padding: 22px 0;
        &:first-child {
          position: relative;
          border-radius: 16px;
          background: #ffffff;
          box-sizing: border-box;
          border: 2px solid rgba(0, 0, 0, 0.15);
        }
        &:last-child {
          border-radius: 16px;
          background: #ffffff;
          box-sizing: border-box;
          border: 2px solid rgba(0, 0, 0, 0.15);
        }
        &-img {
          width: 64px;
          height: 64px;
        }
      }
    }
    &-end {
      &-particulars {
        display: flex;
        align-items: center;
        justify-content: space-between;
        &-money {
          display: flex;
          align-items: center;
          view:nth-child(1) {
            font-size: 48px;
            line-height: 40px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
          }
          view:nth-child(2) {
            margin: 0 4px;
            font-size: 24px;
            line-height: 36px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
          }
          view:nth-child(3) {
            font-size: 28px;
            line-height: 36px;
            letter-spacing: normal;
            text-decoration: line-through;
            color: rgba(0, 0, 0, 0.4);
          }
          view:nth-child(4) {
            margin-left: 8px;
            border-radius: 8px;
            box-sizing: border-box;
            border: 2px solid #f58900;
            padding: 0 12px;
            line-height: 34px;
            font-size: 20px;
            font-weight: 500;
            letter-spacing: normal;
            color: #f58900;
          }
        }
        &-view {
          display: flex;
          align-items: center;
          font-size: 24px;
          letter-spacing: normal;
        &.disabled-text {
          color: rgba(0, 0, 0, 0.4);
          image {
            margin-left: 4px;
            width: 32px;
            height: 32px;
            transition: transform 0.3s ease-in-out;
            &.rotate {
              transform: rotate(90deg);
            }
          }
        }
      }
      .particulars-detail {
        border-radius: 16px;
        background: #f5f6fa;
        display: flex;
        flex-direction: column;
        gap: 16px;
        overflow: auto;
        max-height: 0;
        margin-top: 0;
        margin-bottom: 0;
        padding: 0 32px;
        opacity: 0;
        transition: all 0.3s ease-in-out;
        &.expand {
          max-height: 600px;
          margin-top: 24px;
          margin-bottom: 16px;
          padding: 24px 32px;
          opacity: 1;
        }
        &-item {
          width: 100%;
          &-title {
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-size: 24px;
            line-height: 40px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
            .color1 {
              color: #f58900;
            }
          }
          &-content {
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-size: 20px;
            line-height: 36px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.4);
          }
        }
      }
      &-cost {
        margin-top: 32px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        &-money {
          font-size: 24px;
          line-height: 44px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.9);
        }
        &-view {
          display: flex;
          align-items: center;
          font-size: 24px;
          letter-spacing: normal;
      .car-card-temp-time {
        font-size: 24px;
        line-height: 40px;
        color: rgba(0, 0, 0, 0.9);
        &.disabled-text {
          color: rgba(0, 0, 0, 0.4);
          image {
            margin-left: 4px;
            width: 32px;
            height: 32px;
          }
        }
      }
      &-pay {
        margin-left: 30px;
        margin-right: 36px;
        margin-top: 60px;
        border-radius: 16px;
        background: linear-gradient(97deg, #8475ff 0%, #7dd6ff 103%);
        line-height: 96px;
        text-align: center;
        font-size: 30px;
        letter-spacing: normal;
        color: #ffffff;
      }
    }
  }
}
.custom-callout {
    position: relative;
    z-index: 999;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
}
.pd-6-10 {
    padding: 8px 20px !important;
}
.waiting-callout-content {
    z-index: 2;
    position: absolute;
    top: 240px;
    left: 50%;
    transform: translateX(-50%);
}
.custom-callout-content {
    display: flex;
    align-items: center;
    white-space: nowrap;
    border-radius: 16px;
    background: rgba(255, 255, 255, 1);
    font-size: 24px;
    font-weight: 400;
    color: rgba(0, 0, 0, 0.9);
    line-height: 38px;
    box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2);
    padding: 8px 20px;
}
.txt-center {
    text-align: center;
}
.car-time {
    font-size: 24px;
    letter-spacing: normal;
    display: flex;
    align-items: center;
    justify-content: center;
}
.car-time-num {
    font-size: 36px;
    letter-spacing: normal;
    color: #6D7FD9;
    margin-bottom: 4px;
}
.custom-callout-icon {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    display: block;
}
.car-callout-android {
    z-index: 9;
    pointer-events: none;
}
.waiting-callout {
    position: relative;
    z-index: 999;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    overflow: visible; // 确保辐射动画不被裁剪
    min-width: 700px;
    height: 600px;
}
.waiting-radar {
    position: absolute;
    bottom: 50px;
    left: 0;
    width: 100%;
    height: 350px;
    pointer-events: none;
    z-index: 1;
}
.waiting-radar-img {
    position: fixed;
    width: 700px;
    height: 350px;
    top: 48%;
    left: 50%;
    transform: translate(-50%, -48%);
}
.waiting-callout-bg {
    position: relative;
    height: 70px;
    min-width: 155px;
    text-align: center;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    width: fit-content;
}
.waiting-callout-bg-img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
}
.waiting-callout-bg-text {
    position: relative;
    z-index: 100;
    font-size: 22px;
    font-weight: 400;
    letter-spacing: normal;
    color: #FFFFFF;
    line-height: 48px;
    padding: 0 20px;
    box-sizing: border-box;
    white-space: nowrap;
}
.waiting-callout-txt {
    z-index: 200;
    border-radius: 16px;
    color: rgba(0, 0, 0, 0.9);
    font-size: 24px;
    font-weight: 400;
    background: rgba(255, 255, 255, 1);
    padding: 8px 20px;
    line-height: 48px;
    display: flex;
}
.pr-6 {
    padding-right: 6px;
}
.pr-0 {
    padding-right: 0;
}
src/robotaxi/pages/share-trip/index.tsx
@@ -1,85 +1,460 @@
import Taro from "@tarojs/taro";
import { View, Text, Image, Map } from "@tarojs/components";
import { useState, useEffect } from "react";
import { View, Image, Map } from "@tarojs/components";
import { useState, useEffect, useMemo, useRef } from "react";
import "./index.less";
import homeIcon from "../../assets/back-home.png"; // 占位,建议换成首页小房子图标
import pointer from "../../assets/pointer.png"; // 车辆图标
import shareContactIcon from "../../assets/share-contact.png"; // 客服占位图
import contactServiceIcon from "../../assets/contact-service.png"; // 客服占位图
import shareLine from "../../assets/share-line.png"; // 分享线占位图
// 地图标记状态管理
import useMapMarkers from '../begin-route/hooks/use-map-markers';
// 车辆追踪状态管理
import useVehicleTracking from '../begin-route/hooks/use-vehicle-tracking';
// 路线渲染状态管理
import useRouteManager from "../begin-route/hooks/use-route-manager";
// 地图覆盖物渲染委托
import useViewDelegates from "../begin-route/hooks/use-view-delegates";
export default function ShareTrip() {
  const [tripData, setTripData] = useState<any>(null);
  const [longitude, setLongitude] = useState(113.32452);
  const [latitude, setLatitude] = useState(23.10229);
  const [startSite, setStartSite] = useState<any>(null); // 上车点
  const [endSite, setEndSite] = useState<any>({}); // 下车点
  const [isExpired, setIsExpired] = useState(false); // 是否已过期(大于24小时)
  const [showReminder, setShowReminder] = useState(true); // 模拟是否显示提醒
  const [tripStatus, setTripStatus] = useState<number>(1); // 行程状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功 10:取消订单时页面只展示一个上车点
  const tripStatusRef = useRef(tripStatus);
  // 行程状态显示规则配置:showPickup-显示上车点,showDropoff-显示下车点,showCar-显示车辆,showRoute-显示路线,showWalking-显示步行路线,viewport-视口类型,translateCar-车辆是否平移
  const STATUS_RULES: Record<number, any> = {
    1: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false },
    2: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    3: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true },
    6: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    7: { showPickup: false, showDropoff: true, showCar: false, showRoute: false, showWalking: false, viewport: 'dropoff', translateCar: false },
    8: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
    9: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
    10: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
  };
  const statusRule = useMemo(() => STATUS_RULES[tripStatus] || STATUS_RULES[1], [tripStatus]); // 当前行程状态对应的显示规则
  const shouldShowRoute = statusRule.showRoute;
  const isAndroid = useMemo(() => {
    try {
      const info = Taro.getSystemInfoSync();
      return info?.platform === 'android' || /android/i.test(info?.system || '');
    } catch (error) {
      return false;
    }
  }, []);
  const mapLongitude = longitude ?? 116.397128;
  const mapLatitude = latitude ?? 39.916527;
  const vehicleMarkerId = !isAndroid && tripStatus === 6 ? 30 : 3;
  const vehicleState = useVehicleTracking({
      statusRule,
      vehicleMarkerId
    }); // 车辆追踪状态容器
    const {
      vehicleLocation,
      vehicleDirection,
      setVehicleDirection,
      updateVehicleLocationThrottled,
    } = vehicleState; // 车辆状态解构
   const markerState = useMapMarkers({
      tripStatus,
      statusRule,
      isAndroid,
      startSite,
      endSite,
      vehicleLocation,
      vehicleDirection,
      vehicleMarkerId
    }); // 地图标记状态容器
    const {
      markers
    } = markerState; // 标记状态解构
  const routeState = useRouteManager({
    isAndroid,
    tripStatus,
    tripStatusRef,
    statusRule,
    shouldShowRoute,
    startSite,
    updateVehicleLocationThrottled,
    setVehicleDirection
  });
  const {
    polyline,
    applyRoutePoints,
    routeDistance,
    routeDuration,
    setRouteDistance,
    setRouteDuration
  } = routeState;
  const applyRoutePointsRef = useRef(applyRoutePoints);
  const updateVehicleLocationRef = useRef(updateVehicleLocationThrottled);
  const setVehicleDirectionRef = useRef(setVehicleDirection);
  const mockScene = useMemo(() => {
    const drivingTrack = [
      { longitude: 104.055501, latitude: 30.586747 }, { longitude: 104.05551, latitude: 30.586678 }, { longitude: 104.055537, latitude: 30.58657 }, { longitude: 104.055548, latitude: 30.58656 }, { longitude: 104.055821, latitude: 30.586565 }, { longitude: 104.056191, latitude: 30.58657 },
      { longitude: 104.056191, latitude: 30.58657 }, { longitude: 104.056202, latitude: 30.586426 }, { longitude: 104.056218, latitude: 30.586189 }, { longitude: 104.056234, latitude: 30.585938 }, { longitude: 104.056256, latitude: 30.58568 }, { longitude: 104.056261, latitude: 30.585562 },
      { longitude: 104.056261, latitude: 30.585562 }, { longitude: 104.055741, latitude: 30.58554 }, { longitude: 104.055467, latitude: 30.585524 }, { longitude: 104.05477, latitude: 30.585492 }, { longitude: 104.054501, latitude: 30.585476 }, { longitude: 104.054405, latitude: 30.585476 },
      { longitude: 104.053869, latitude: 30.585438 }, { longitude: 104.053488, latitude: 30.585417 }, { longitude: 104.053359, latitude: 30.585412 }, { longitude: 104.053074, latitude: 30.585396 }, { longitude: 104.05117, latitude: 30.585288 }, { longitude: 104.050693, latitude: 30.585267 },
      { longitude: 104.050328, latitude: 30.585245 }, { longitude: 104.050221, latitude: 30.58524 }, { longitude: 104.049604, latitude: 30.585208 }, { longitude: 104.049046, latitude: 30.585176 }, { longitude: 104.048853, latitude: 30.585165 }, { longitude: 104.04859, latitude: 30.585154 },
      { longitude: 104.048354, latitude: 30.585133 }, { longitude: 104.048273, latitude: 30.585122 }, { longitude: 104.048134, latitude: 30.585117 }, { longitude: 104.048005, latitude: 30.585133 }, { longitude: 104.047565, latitude: 30.585111 }, { longitude: 104.046616, latitude: 30.585068 },
      { longitude: 104.046616, latitude: 30.585068 }, { longitude: 104.046637, latitude: 30.585938 }, { longitude: 104.046643, latitude: 30.586061 }, { longitude: 104.046669, latitude: 30.586366 }, { longitude: 104.046723, latitude: 30.586753 }, { longitude: 104.046798, latitude: 30.587128 },
      { longitude: 104.046911, latitude: 30.587777 }, { longitude: 104.046954, latitude: 30.587965 }, { longitude: 104.046986, latitude: 30.58811 }, { longitude: 104.046653, latitude: 30.588319 }, { longitude: 104.046653, latitude: 30.588319 }, { longitude: 104.045741, latitude: 30.588276 },
      { longitude: 104.044942, latitude: 30.588255 }, { longitude: 104.043156, latitude: 30.588212 }, { longitude: 104.042185, latitude: 30.588191 }, { longitude: 104.041493, latitude: 30.588169 }, { longitude: 104.040527, latitude: 30.588137 }, { longitude: 104.039921, latitude: 30.588121 },
      { longitude: 104.038483, latitude: 30.588089 }, { longitude: 104.037518, latitude: 30.588089 }, { longitude: 104.037298, latitude: 30.588083 }, { longitude: 104.037121, latitude: 30.588072 }, { longitude: 104.035951, latitude: 30.588051 }, { longitude: 104.035565, latitude: 30.588072 },
      { longitude: 104.035511, latitude: 30.588078 }, { longitude: 104.0352, latitude: 30.58811 }, { longitude: 104.034599, latitude: 30.588164 }, { longitude: 104.034213, latitude: 30.588212 }, { longitude: 104.033843, latitude: 30.588276 }, { longitude: 104.033382, latitude: 30.5884 },
      { longitude: 104.033382, latitude: 30.5884 }, { longitude: 104.033452, latitude: 30.588587 }, { longitude: 104.033602, latitude: 30.58892 }, { longitude: 104.033763, latitude: 30.589279 }, { longitude: 104.034015, latitude: 30.590776 }, { longitude: 104.034026, latitude: 30.591709 },
      { longitude: 104.034031, latitude: 30.592219 }, { longitude: 104.033966, latitude: 30.593786 }, { longitude: 104.033961, latitude: 30.59459 }, { longitude: 104.033977, latitude: 30.595604 }, { longitude: 104.033961, latitude: 30.595931 }, { longitude: 104.033972, latitude: 30.596924 },
      { longitude: 104.033972, latitude: 30.596951 }, { longitude: 104.033972, latitude: 30.596951 }, { longitude: 104.034423, latitude: 30.596972 }, { longitude: 104.034423, latitude: 30.596972 }, { longitude: 104.034423, latitude: 30.596924 }, { longitude: 104.034433, latitude: 30.596768 },
      { longitude: 104.034444, latitude: 30.595443 }, { longitude: 104.034449, latitude: 30.594295 }, { longitude: 104.034481, latitude: 30.594172 }, { longitude: 104.034594, latitude: 30.594022 }, { longitude: 104.034755, latitude: 30.593871 }, { longitude: 104.034755, latitude: 30.593871 },
      { longitude: 104.033339, latitude: 30.592729 }, { longitude: 104.032915, latitude: 30.592417 }, { longitude: 104.032792, latitude: 30.592326 }, { longitude: 104.03262, latitude: 30.592187 }, { longitude: 104.032084, latitude: 30.591758 }, { longitude: 104.031402, latitude: 30.591211 },
      { longitude: 104.031343, latitude: 30.591168 }, { longitude: 104.030957, latitude: 30.590857 }, { longitude: 104.030769, latitude: 30.590706 }, { longitude: 104.030281, latitude: 30.590309 }, { longitude: 104.030169, latitude: 30.590218 }, { longitude: 104.030104, latitude: 30.590164 },
      { longitude: 104.02975, latitude: 30.58988 }, { longitude: 104.028828, latitude: 30.58914 }, { longitude: 104.027964, latitude: 30.588448 }, { longitude: 104.027175, latitude: 30.587809 }, { longitude: 104.026794, latitude: 30.587509 }, { longitude: 104.026639, latitude: 30.587386 },
      { longitude: 104.026639, latitude: 30.587386 }, { longitude: 104.026623, latitude: 30.587327 }, { longitude: 104.026558, latitude: 30.587219 }, { longitude: 104.026172, latitude: 30.586806 }, { longitude: 104.02614, latitude: 30.586764 }, { longitude: 104.026124, latitude: 30.586704 },
      { longitude: 104.02518, latitude: 30.585938 }, { longitude: 104.020545, latitude: 30.582214 }, { longitude: 104.019338, latitude: 30.581244 }, { longitude: 104.018721, latitude: 30.580745 }, { longitude: 104.018554, latitude: 30.580611 }, { longitude: 104.017111, latitude: 30.579468 },
      { longitude: 104.016731, latitude: 30.579194 }, { longitude: 104.016307, latitude: 30.578921 }, { longitude: 104.015937, latitude: 30.578701 }, { longitude: 104.015556, latitude: 30.578508 }, { longitude: 104.015175, latitude: 30.57832 }, { longitude: 104.014628, latitude: 30.578073 },
      { longitude: 104.014, latitude: 30.577848 }, { longitude: 104.013217, latitude: 30.577617 }, { longitude: 104.012664, latitude: 30.577483 }, { longitude: 104.011731, latitude: 30.577306 }, { longitude: 104.003899, latitude: 30.575938 }, { longitude: 104.002306, latitude: 30.575659 },
      { longitude: 104.001474, latitude: 30.575509 }, { longitude: 103.998926, latitude: 30.575085 }, { longitude: 103.998213, latitude: 30.574951 }, { longitude: 103.996582, latitude: 30.574661 }, { longitude: 103.995933, latitude: 30.574549 }, { longitude: 103.982232, latitude: 30.572146 },
      { longitude: 103.979244, latitude: 30.57162 }, { longitude: 103.978327, latitude: 30.571464 }, { longitude: 103.977592, latitude: 30.571351 }, { longitude: 103.977093, latitude: 30.571292 }, { longitude: 103.975339, latitude: 30.570965 }, { longitude: 103.974609, latitude: 30.570831 },
      { longitude: 103.973644, latitude: 30.570665 }, { longitude: 103.972834, latitude: 30.570536 }, { longitude: 103.972024, latitude: 30.570413 }, { longitude: 103.972024, latitude: 30.570413 }, { longitude: 103.971949, latitude: 30.570391 }, { longitude: 103.971788, latitude: 30.570364 },
      { longitude: 103.971353, latitude: 30.570327 }, { longitude: 103.970994, latitude: 30.570322 }, { longitude: 103.970725, latitude: 30.570327 }, { longitude: 103.970436, latitude: 30.570343 }, { longitude: 103.970151, latitude: 30.570375 }, { longitude: 103.969513, latitude: 30.570483 },
      { longitude: 103.968515, latitude: 30.57067 }, { longitude: 103.967807, latitude: 30.570826 }, { longitude: 103.967807, latitude: 30.570826 }, { longitude: 103.966986, latitude: 30.571024 }, { longitude: 103.966707, latitude: 30.571105 }, { longitude: 103.966053, latitude: 30.57133 },
      { longitude: 103.965243, latitude: 30.571609 }, { longitude: 103.964986, latitude: 30.571706 }, { longitude: 103.964691, latitude: 30.571824 }, { longitude: 103.964309, latitude: 30.571952 }, { longitude: 103.963966, latitude: 30.572076 }, { longitude: 103.963891, latitude: 30.572102 },
      { longitude: 103.963687, latitude: 30.572162 }, { longitude: 103.963548, latitude: 30.572215 }, { longitude: 103.963146, latitude: 30.572355 }, { longitude: 103.962518, latitude: 30.572575 }, { longitude: 103.961981, latitude: 30.572784 }, { longitude: 103.961418, latitude: 30.572998 },
      { longitude: 103.961418, latitude: 30.572998 }, { longitude: 103.961225, latitude: 30.5731 }, { longitude: 103.96108, latitude: 30.57317 }, { longitude: 103.960157, latitude: 30.573481 }, { longitude: 103.959401, latitude: 30.573728 }, { longitude: 103.959031, latitude: 30.573808 },
      { longitude: 103.958956, latitude: 30.573824 }, { longitude: 103.958382, latitude: 30.573894 }, { longitude: 103.95828, latitude: 30.573766 }, { longitude: 103.95828, latitude: 30.573766 }, { longitude: 103.958135, latitude: 30.573261 }, { longitude: 103.95791, latitude: 30.572816 },
      { longitude: 103.957422, latitude: 30.571749 }, { longitude: 103.957229, latitude: 30.571319 }, { longitude: 103.956955, latitude: 30.570729 }, { longitude: 103.95681, latitude: 30.570418 }, { longitude: 103.956751, latitude: 30.570284 }, { longitude: 103.956569, latitude: 30.569914 },
      { longitude: 103.956435, latitude: 30.569619 }, { longitude: 103.956309, latitude: 30.569347 }
    ];
    const pickupSite = {
      name: "上车点",
      latitude: drivingTrack[0].latitude,
      longitude: drivingTrack[0].longitude
    };
    const dropoffSite = {
      name: "目的地",
      latitude: drivingTrack[drivingTrack.length - 1].latitude,
      longitude: drivingTrack[drivingTrack.length - 1].longitude
    };
    // 使用路线起点附近的一小段倒放,模拟车辆靠近上车点
    const pickupTrack = drivingTrack.slice(0, 10).reverse().map(item => ({ ...item }));
    const centerPoint = drivingTrack[Math.floor(drivingTrack.length / 2)];
    return {
      pickupSite,
      dropoffSite,
      pickupTrack,
      drivingTrack,
      centerPoint
    };
  }, []);
  const getDirectionByPoints = (from: { latitude: number; longitude: number }, to?: { latitude: number; longitude: number }) => {
    if (!from || !to) return 0;
    const deltaLng = to.longitude - from.longitude;
    const deltaLat = to.latitude - from.latitude;
    const angle = Math.atan2(deltaLng, deltaLat) * (180 / Math.PI);
    return ((angle % 360) + 360) % 360;
  };
  useEffect(() => {
    // TODO: 替换为真实的获取分享行程数据的接口
    setTimeout(() => {
    tripStatusRef.current = tripStatus;
  }, [tripStatus]);
  useEffect(() => {
    applyRoutePointsRef.current = applyRoutePoints;
    updateVehicleLocationRef.current = updateVehicleLocationThrottled;
    setVehicleDirectionRef.current = setVehicleDirection;
  }, [applyRoutePoints, updateVehicleLocationThrottled, setVehicleDirection]);
  useEffect(() => {
    setShowReminder(false);
    setStartSite(mockScene.pickupSite);
    setEndSite(mockScene.dropoffSite);
    setLongitude(mockScene.centerPoint.longitude);
    setLatitude(mockScene.centerPoint.latitude);
    let stage = "pickup";
    let pointIndex = 0;
    const syncMockTripData = (nextStatus: number, etaMinutes: number) => {
      const remainDistance =
        nextStatus === 4
          ? { value: Math.max(0.5, Number((etaMinutes * 0.8).toFixed(1))), unit: "km" as const }
          : nextStatus === 6
            ? { value: Math.max(0.5, Number(((mockScene.drivingTrack.length - pointIndex) * 0.6).toFixed(1))), unit: "km" as const }
            : { value: 0, unit: "km" as const };
      const remainDuration = nextStatus === 7 ? 0 : Math.max(60, etaMinutes * 60);
      setTripStatus(nextStatus);
      setRouteDistance(remainDistance);
      setRouteDuration(remainDuration);
      setTripData({
        etaMinutes: 4,
        distanceText: "3.6公里",
        etaMinutes,
        distanceText: `${remainDistance.value}${remainDistance.unit}`,
        plateNumber: "粤PXV7029",
        vehicleColor: "白色",
        vehicleCode: "03",
        vehicleImage: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/car-g9.png", // 占位图
        startName: "广州岭南职业技术学院-(公交站)",
        endName: "小鹏科技园2号楼",
        carLat: 23.10229,
        carLng: 113.32452
        vehicleImage: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/car-g9.png",
        startName: mockScene.pickupSite.name,
        endName: mockScene.dropoffSite.name,
        tripStatus: nextStatus,
        boardTime: nextStatus >= 6 ? "14:56" : null,
        arriveTime: nextStatus === 7 ? "15:10" : null
      });
    }, 1000);
  }, []);
    };
    // 初始化一段路线,先验证 hooks 产出的 polyline 是否可用
    applyRoutePointsRef.current(mockScene.pickupTrack);
    updateVehicleLocationRef.current(
      mockScene.pickupTrack[0].latitude,
      mockScene.pickupTrack[0].longitude,
      true
    );
    syncMockTripData(4, 4);
    const intervalId = setInterval(() => {
      if (stage === "pickup") {
        const currentPoint = mockScene.pickupTrack[pointIndex];
        const nextPoint = mockScene.pickupTrack[pointIndex + 1];
        const remainPoints = mockScene.pickupTrack.slice(pointIndex);
        if (currentPoint) {
          applyRoutePointsRef.current(remainPoints.length ? remainPoints : [currentPoint]);
          updateVehicleLocationRef.current(currentPoint.latitude, currentPoint.longitude, true);
          setVehicleDirectionRef.current(getDirectionByPoints(currentPoint, nextPoint));
          syncMockTripData(4, Math.max(1, mockScene.pickupTrack.length - pointIndex - 1));
        }
        if (pointIndex >= mockScene.pickupTrack.length - 1) {
          stage = "driving";
          pointIndex = 0;
          applyRoutePointsRef.current(mockScene.drivingTrack);
          syncMockTripData(6, 0);
          return;
        }
        pointIndex += 1;
        return;
      }
      if (stage === "driving") {
        const currentPoint = mockScene.drivingTrack[pointIndex];
        const nextPoint = mockScene.drivingTrack[pointIndex + 1];
        const remainPoints = mockScene.drivingTrack.slice(pointIndex);
        if (currentPoint) {
          applyRoutePointsRef.current(remainPoints.length ? remainPoints : [currentPoint]);
          updateVehicleLocationRef.current(currentPoint.latitude, currentPoint.longitude, true);
          setVehicleDirectionRef.current(getDirectionByPoints(currentPoint, nextPoint));
          syncMockTripData(6, 0);
        }
        if (pointIndex >= mockScene.drivingTrack.length - 1) {
          stage = "arrived";
          syncMockTripData(7, 0);
          return;
        }
        pointIndex += 1;
      }
    }, 2000);
    return () => clearInterval(intervalId);
  }, [mockScene, setRouteDistance, setRouteDuration]);
  const goHome = () => {
    Taro.reLaunch({ url: "/pages/robotaxi/home/index" });
  };
  const { renderMapOverlays } = useViewDelegates({
    isAndroid,
    tripStatus,
    markers,
    shouldShowWaitingRadar: false,
    waitingRadarVisible: false,
    waitingRadarStyle: {},
    waitingRadarSrc: "",
    carCalloutVisible: false,
    carCalloutStyle: {},
    arriveCalloutVisible: false,
    arriveCalloutStyle: {},
    routeDistance,
    routeDuration,
    customCalloutIconSrc: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png",
    startSite,
    endSite,
    callImmediately: () => {},
    fareEstimate: {},
    handleOpenPassengerNotice: () => {},
    walkingTime: 0,
    isCallDisabled: false,
    handleOpenCancelPopupByTripStatus: () => {},
    handleOpenCancelPopupWithType: () => {},
    setTripStatus,
    finishCountdownEnabled: false,
    orderId: "",
    vehicleInfo: {},
    handleFinishCountdownTimeout: () => {},
    resetDetailVersion: 0,
    priceUpdateVersion: 0,
    evaluateSubmitVersion: 0,
    evaluateCloseVersion: 0,
    handleOpenAnnouncementsPopup: () => {},
    handleOpenOverRoutePopup: () => {},
    handlePay: () => {},
    handleOpenEvaluatePopup: () => {},
    handleToggleDetailFade: () => {},
    isRouteSelectExpanded: false,
    onToggleRouteSelect: () => {},
    trunkStatus: false
  });
  const markers = tripData
    ? [
        {
          id: 1,
          latitude: tripData.carLat,
          longitude: tripData.carLng,
          iconPath: pointer,
          width: 32,
          height: 32,
          anchor: { x: 0.5, y: 0.5 },
          callout: {
            content: `${tripData.distanceText} ${tripData.etaMinutes}分钟`,
            color: "#6D7FD9",
            fontSize: 12,
            borderRadius: 12,
            bgColor: "#ffffff",
            padding: 6,
            display: "ALWAYS"
          }
        }
      ]
    : [];
  // const renderMarkers = () => {
  //   if (!tripData) return [];
  //   const markersList: any[] = [];
  //   // 上车点 (等待接驾、行程中显示)
  //   if (tripData.tripStatus === 1 || tripData.tripStatus === 2) {
  //     markersList.push({
  //       id: 1,
  //       latitude: tripData.startLat,
  //       longitude: tripData.startLng,
  //       iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-up.png",
  //       width: 22,
  //       height: 40,
  //       anchor: { x: 0.5, y: 1 },
  //       zIndex: 1,
  //       name: tripData.startName
  //     });
  //   }
  //   // 下车点 (始终显示)
  //   markersList.push({
  //     id: 2,
  //     latitude: tripData.endLat,
  //     longitude: tripData.endLng,
  //     iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-down.png",
  //     width: 22,
  //     height: 40,
  //     anchor: { x: 0.5, y: 1 },
  //     zIndex: 1,
  //     name: tripData.endName
  //   });
  //   // 车辆 (等待接驾、行程中显示)
  //   if (
  //     vehicleLocation &&
  //     (tripData.tripStatus === 1 || tripData.tripStatus === 2)
  //   ) {
  //     markersList.push({
  //       id: 3,
  //       latitude: vehicleLocation.latitude,
  //       longitude: vehicleLocation.longitude,
  //       iconPath: carIcon,
  //       width: 46,
  //       height: 71,
  //       anchor: { x: 0.5, y: 0.5 },
  //       rotate: vehicleDirection,
  //       zIndex: 2,
  //       callout:
  //         tripData.tripStatus === 1
  //           ? {
  //               content: `${tripData.distanceText} ${tripData.etaMinutes}分钟`,
  //               color: "#6D7FD9",
  //               fontSize: 12,
  //               borderRadius: 12,
  //               bgColor: "#ffffff",
  //               padding: 6,
  //               display: "ALWAYS"
  //             }
  //           : undefined
  //     });
  //   }
  //   return markersList;
  // };
  // const markers = renderMarkers();
  const handleContactService = () => {
    // 唤起客服操作
    Taro.makePhoneCall({
      phoneNumber: "400-000-0000" // TODO: 替换为实际客服电话
    }).catch(() => {});
  };
  if (isExpired) {
    return (
      <View className="share-trip-expired">
        <View className="expired-content">
          <Image className="expired-img" src={shareContactIcon} />
          <View className="expired-text">行程结束已超过24小时</View>
          <View className="expired-sub-text">查看行程信息,请联系客服</View>
          <View className="expired-btn" onClick={handleContactService}>
            联系客服
          </View>
        </View>
      </View>
    );
  }
  return (
    <View className="home-container">
      <Map
      {/* <Map
        className="map-container"
        longitude={longitude}
        latitude={latitude}
        scale={14}
        markers={markers}
        polyline={tripData?.tripStatus === 1 || tripData?.tripStatus === 2 ? polyline : []}
        style={{ width: "100%", height: "100%" }}
      > */}
      <Map
        id="map"
        longitude={mapLongitude}
        latitude={mapLatitude}
        scale={14}
        markers={markers}
        polyline={polyline}
        style={{ width: "100%", height: "87vh" }}
      >
        {renderMapOverlays()}
        {/* 顶部返回/首页按钮 */}
        <View onClick={goHome} className="select-city">
          <Image className="select-city-icon" src={homeIcon} />
        </View>
        <View className="container">
          {/* 提醒 */}
          <View className="tip-box">
            <View className="tip-box-content">
              <View className="tip-box-text">
                开启提醒后,亲友行程动态秒通知
              </View>
              <View className="tip-box-btn">去开启</View>
            </View>
          {/* 联系客服悬浮按钮 */}
          <View className={`share-contact ${showReminder ? "has-tip" : ""}`}>
            <Image className="share-contact-icon" src={contactServiceIcon} />
          </View>
          {/* 提醒 */}
          {showReminder && (
            <View className="tip-box">
              <View className="tip-box-content">
                <View className="tip-box-text">
                  开启提醒后,亲友行程动态秒通知
                </View>
                <View
                  className="tip-box-btn"
                  onClick={() => setShowReminder(false)}
                >
                  去开启
                </View>
              </View>
            </View>
          )}
          <View className="container-inner">
            <View catchMove onClick={e => e.stopPropagation()}>
              <View className="calling-car">
@@ -87,6 +462,23 @@
                  className="calling-car-bg"
                  src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png"
                />
                <View className="calling-car-head">
                  {tripData?.tripStatus === 4 && (
                    <View className="calling-car-head-title">
                      车辆还有
                      <View className="calling-car-head-title-eta">{`${tripData.etaMinutes}`}</View>
                      分钟到达上车点
                    </View>
                  )}
                  {tripData?.tripStatus === 6 && (
                    <View className="calling-car-head-title">
                      乘客已上车,正在前往目的地
                    </View>
                  )}
                  {tripData?.tripStatus === 7 && (
                    <View className="calling-car-head-title">已到达目的地</View>
                  )}
                </View>
                <View className="car-card">
                  <View className="car-card-detail">
                    <View>
@@ -102,6 +494,57 @@
                      src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png"
                    />
                  </View>
                  <View className="car-card-line"></View>
                  <View className="car-card-temp">
                    <View className="car-card-temp-list">
                      <Image className="car-card-temp-img" src={shareLine} />
                      {/* 起点 */}
                      <View className="car-card-temp-row">
                        <View className="car-card-temp-row-left">
                          <View className="dot start"></View>
                          <View
                            className={`car-card-temp-title ${
                              tripData?.tripStatus === 6 ||
                              tripData?.tripStatus === 7
                                ? "disabled-text"
                                : ""
                            }`}
                          >
                            起点:{tripData?.startName}
                          </View>
                        </View>
                        {(tripData?.tripStatus === 6 ||
                          tripData?.tripStatus === 7) &&
                          tripData?.boardTime && (
                            <View className="car-card-temp-time disabled-text">
                              {tripData.boardTime} 上车
                            </View>
                          )}
                      </View>
                      {/* 终点 */}
                      <View
                        className="car-card-temp-row"
                        style={{ marginTop: "12px" }}
                      >
                        <View className="car-card-temp-row-left">
                          <View className="dot end"></View>
                          <View
                            className={`car-card-temp-title ${
                              tripData?.tripStatus === 7 ? "disabled-text" : ""
                            }`}
                          >
                            终点:{tripData?.endName}
                          </View>
                        </View>
                        {tripData?.tripStatus === 7 && tripData?.arriveTime && (
                          <View className="car-card-temp-time disabled-text">
                            {tripData.arriveTime} 到达
                          </View>
                        )}
                      </View>
                    </View>
                  </View>
                </View>
              </View>
            </View>
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
@@ -29,8 +29,8 @@
    setLoading(true);
    try {
      const res = await getCollectPoints({
        current: page,
        size: 4,
        pageNum: page,
        pageSize: 4,
      });
      if (res && res.records) {
        setList(prev => page === 1 ? res.records : [...prev, ...res.records]);