hejianhao
2026-02-11 587c9e6e6373a480e372f19fa10df1752368ce26
feat(robotaxi): 新增优惠券管理、WebSocket集成与行程状态处理

- 新增优惠券管理功能,包括列表、领取、详情查看
- 集成WebSocket连接管理,支持订单状态、车辆位置、路线更新等实时消息
- 实现行程状态与订单状态的映射逻辑,完善各状态下的UI展示
- 新增起终点运营区域校验,确保在同一电子围栏内
- 优化地图标记渲染逻辑,根据行程状态动态显示上车点、车辆位置和目的地
- 新增等待时间、路线时长等接口调用,完善行程预估信息
- 更新隐私协议弹窗,增加两项隐私说明条款
- 修复客服电话接口路径和响应字段映射问题
4个文件已添加
15个文件已修改
1248 ■■■■■ 已修改文件
config/dev.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/api.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/privacy-popup/index.less 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/privacy-popup/index.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.tsx 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/coupon.ts 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/customer-service.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon-lost-efficacy.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon-used.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/customer-service/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/README.md 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/announcements-popup.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 517 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/coupon/index.tsx 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/websocket.ts 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/dev.js
@@ -16,7 +16,8 @@
const CAR_CONTROL_BIZ_HOST = '"https://apps-biz.deploy-test.xiaopeng.com"';
const COMMON_OSS_HOST = '"https://spre.xiaopeng.com"'; // 官渠通用 oss 资源前缀
const ADMIN_OSS_HOST = '"https://pre-xps01.xiaopeng.com"'; // 运营后台上传的额文件的 oss 域名
const ROBOTAXI_HOST = '"http://192.168.110.85:8000"'; // 打车接口域名
const ROBOTAXI_HOST = '"http://192.168.110.80:8000"'; // 打车接口域名
const ROBOTAXI_WS_HOST = '"ws://192.168.110.34:8888/websocket"';
const XP_APPID_MAP = {
  weapp: '"xxxxxxx"', // 用户中心登录appId
@@ -83,6 +84,7 @@
    COMMON_OSS_HOST,
    ADMIN_OSS_HOST,
    ROBOTAXI_HOST,
    ROBOTAXI_WS_HOST,
  },
  mini: {
    // webpackChain (chain) {
src/constants/api.ts
@@ -38,6 +38,8 @@
export const _LDX_RESERVE_URL = LDX_RESERVE_URL
//@ts-ignore
export const _ROBOTAXI_HOST = ROBOTAXI_HOST
//@ts-ignore
export const _ROBOTAXI_WS_HOST = ROBOTAXI_WS_HOST
// pic CDN
export const CDN = 'https://s.xiaopeng.com/xp-fe/mini'
src/pages/robotaxi/home/components/privacy-popup/index.less
@@ -53,7 +53,7 @@
      .link-item {
        display: flex;
        align-items: center;
        align-items: flex-start;
        margin-bottom: 8px;
      }
@@ -65,12 +65,14 @@
      }
      .item-name {
        flex-shrink: 0;
        flex: 1;
        font-size: 24px;
        font-weight: normal;
        line-height: 40px;
        letter-spacing: normal;
        color: rgba(0, 0, 0, 0.6);
        word-break: break-word;
        white-space: normal;
      }
    }
@@ -103,4 +105,4 @@
    }
  }
}
}
src/pages/robotaxi/home/components/privacy-popup/index.tsx
@@ -19,8 +19,11 @@
    0: false,
    1: false,
    2: false,
    3: false,
  });
  const [isAllAgree, setIsAllAgree] = useState(false);
  const AGREE_NAME = ['用户服务协议', '小鹏Robotaxi 平台隐私政策', '关于小鹏Robotaxi平台敏感个人信息处理情况的说明', '关于小鹏Robotaxi平台向第三方提供个人信息情况的说明']
  useEffect(() => {
    if (show && !visible) {
@@ -79,6 +82,7 @@
      0: !isAllAgree,
      1: !isAllAgree,
      2: !isAllAgree,
      3: !isAllAgree,
    })
  }
@@ -103,14 +107,14 @@
              <View className='link-item' key={key}>
                <Image onClick={checkItem(key)} className='item-radio' src={isAgree[key] ? check : uncheck}
                  mode='aspectFill' />
                <Text className='item-name'>《 {key === '0' ? '用户服务协议' : '小鹏Robotaxi 平台隐私政策'} 》</Text>
                <View className='item-name'>《 {AGREE_NAME[key]} 》</View>
              </View>
            ))
          }
          <View className='link-item'>
            <Image onClick={checkAll} className='item-radio' src={isAllAgree ? check : uncheck}
              mode='aspectFill' />
            <Text className='item-name'>全部勾选</Text>
            <View className='item-name'>全部勾选</View>
          </View>
        </View>
src/pages/robotaxi/home/index.tsx
@@ -22,8 +22,6 @@
} from '../../../utils/Robotaxi-public-method';
import { getGeofence, getBannerList } from '../../../robotaxi/api/home';
import { getOrderDistance } from '../../../robotaxi/api/select-map-site';
import wsManager from '../../../utils/websocket';
import { checkOtp } from '../../../utils/tools';
export default function Home() {
@@ -61,8 +59,8 @@
  const DEFAULT_LONGITUDE = 116.397128; // 北京故宫经度
  const [selectedCity, setSelectedCity] = useState(Taro.getStorageSync('selectedCity') || {
    name: '北京市',
    code: '110000',
    name: '',
    code: '',
  }); // 选中的城市
  const ORDER_STATUS = {
@@ -74,26 +72,10 @@
  useDidShow(() => {
    // 检查登录状态并连接 WebSocket
    checkOtp().then(otp => {
      if (otp) {
        // wsManager.connect('wss://your-websocket-server.com/ws');
      }
    });
    // 从本地存储中获取选中的城市
    const selectedCity = Taro.getStorageSync('selectedCity');
    if (selectedCity) {
      setSelectedCity(selectedCity);
    } else {
      setSelectedCity({
        name: '北京市',
        code: '110000',
      })
      Taro.setStorageSync('selectedCity', {
        name: '北京市',
        code: '110000',
      });
    }
    // 从本地存储中获取行程状态
    const tripStatus = Taro.getStorageSync('tripStatus');
@@ -106,6 +88,16 @@
          setIsLocationAuthDenied(true);
          setLatitude(DEFAULT_LATITUDE);
          setLongitude(DEFAULT_LONGITUDE);
          if (!selectedCity || !selectedCity.code) {
            setSelectedCity({
              name: '北京市',
              code: '110000',
            })
            Taro.setStorageSync('selectedCity', {
              name: '北京市',
              code: '110000',
            });
          }
        } else {
          // 检查缓存中是否有 startSite
          const startSite = Taro.getStorageSync('startSite');
@@ -118,6 +110,24 @@
            // 初始化时也调用接口检查运营区状态和更新站点
            updateMapData(startSite.latitude, startSite.longitude);
            if (!selectedCity || !selectedCity.code) {
              getAddressByLocation(startSite.longitude, startSite.latitude)
                .then((data) => {
                  if (data?.regeocodeData?.addressComponent) {
                    const { city, province, adcode } = data.regeocodeData.addressComponent;
                    const cityName = (city && typeof city === 'string' && city.length > 0) ? city : province;
                    const cityCode = convertAdcodeToCityCode(adcode, cityName, province);
                    const cityInfo = {
                      name: cityName,
                      code: cityCode
                    };
                    setSelectedCity(cityInfo);
                    Taro.setStorageSync('selectedCity', cityInfo);
                  }
                })
                .catch(() => {
                });
            }
          } else {
            getLocationInfo(true);
          }
@@ -159,7 +169,6 @@
      if (res && Array.isArray(res)) {
        const newPolygons = formatGeofencePolygons(res);
        console.log(newPolygons);
        setPolygons(newPolygons);
      } else {
@@ -219,9 +228,9 @@
        try {
          // 更新地图数据
          const { newMarkers } = await updateMapData(res.latitude, res.longitude);
          const { newMarkers, sites } = await updateMapData(res.latitude, res.longitude);
          const { targetMarker, minDistance } = findNearestMarker(res.latitude, res.longitude, newMarkers);
          const { targetMarker, minDistance } = findNearestMarker(res.latitude, res.longitude, sites.length > 0 ? sites : newMarkers);
          if (minDistance < adsorptionDistance && targetMarker) {
            setIsAdsorbed(true);
@@ -349,10 +358,10 @@
          try {
            // 更新地图数据
            const { newMarkers } = await updateMapData(lat, lng);
            const { newMarkers, sites } = await updateMapData(lat, lng);
            // 自动吸附逻辑:查找最近的推荐上车点
            const { targetMarker, minDistance } = findNearestMarker(lat, lng, newMarkers);
            const nearestCandidates = sites.length > 0 ? sites : newMarkers;
            const { targetMarker, minDistance } = findNearestMarker(lat, lng, nearestCandidates);
            // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环
            if ((causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') && targetMarker && minDistance < adsorptionDistance) {
@@ -446,7 +455,16 @@
  }
  const toSeleteSitePage = (type: string) => {
    Taro.setStorageSync('startSite', currentSite);
    if (!electronicAreaId) {
      Taro.showToast({
        title: '请先移动到运营区内',
        icon: 'none'
      });
      return;
    }
    if (type == 'end') {
      Taro.setStorageSync('startSite', currentSite);
    }
    Taro.navigateTo({
      url: `/robotaxi/pages/site-search/index?pageType=${type}&electronicAreaId=${electronicAreaId}&longitude=${longitude}&latitude=${latitude}`
    })
@@ -465,17 +483,6 @@
  }
  const toBeginRoute = () => {
    if (currentSite && currentSite.name) {
      Taro.setStorageSync('startSite', currentSite);
    }
    Taro.setStorageSync('endSite', {
      latitude: 30.571100,
      longitude: 104.065200,
      name: '中国南方电网·3号门'
    });
    Taro.navigateTo({
      url: '/robotaxi/pages/begin-route/index',
    })
  }
  const toCoupon = () => {
@@ -556,7 +563,7 @@
                  {
                    (!isOperatingArea || isAdsorbed) && (
                      <View className="toolTip-bg">
                        <Text className="toolTip-bg-text">{!isOperatingArea ? '不在运营区' : '限停3分钟'}</Text>
                        <Text className="toolTip-bg-text">{!isOperatingArea ? '不在运营区' : `限停${currentSite.time}分钟`}</Text>
                      </View>
                    )
                  }
src/robotaxi/api/begin-route.tsx
@@ -8,4 +8,22 @@
        method: 'POST',
        payload
    });
}
// 保存订单
export const saveOrder = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + '/operation/base/v1/mini-program/order/save',
        method: 'POST',
        payload
    });
}
// 获取等待时间
export const getWaitTime = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + '/operation/base/v1/mini-program/pricing/getWaitTime',
        method: 'POST',
        payload
    });
}
src/robotaxi/api/coupon.ts
New file
@@ -0,0 +1,80 @@
import request from '../../utils/request';
import { _ROBOTAXI_HOST } from '../../constants/api';
export interface CouponItem {
    couponId: string;
    title: string;
    discountAmount: number;
    expireTime: string;
    status: number; // 1-未使用 2-已使用 3-已过期 0待领取
}
export interface CouponDetail {
    couponId: string;
    title: string;
    expireTime: string;
    validTimeRange: string;
    applicableCities: string[];
    usageInstructions: string;
}
export interface GetCouponListPayload {
    pageNum: number;
    pageSize: number;
    status?: number | string;
}
export interface GetCouponListResponse {
    list: CouponItem[];
    total: number;
}
// 优惠券列表
export const getCouponList = async (payload: GetCouponListPayload): Promise<GetCouponListResponse> => {
    const res = await request({
        url: _ROBOTAXI_HOST + '/account/api/v1/coupon/list',
        method: 'POST',
        payload
    });
    // 手动转换下划线字段为驼峰
    const list = (res.list || []).map((item: any) => ({
        couponId: item.coupon_id,
        title: item.title,
        discountAmount: item.discount_amount,
        expireTime: item.expire_time,
        status: item.status,
    }));
    return {
        total: res.total || 0,
        list
    };
}
// 领取优惠券
export const receiveCoupon = async (payload: { couponId: string }) => {
    return request({
        url: _ROBOTAXI_HOST + '/account/api/v1/coupon/receive',
        method: 'POST',
        payload
    });
}
// 优惠券详情
export const getCouponDetail = async (couponId: string): Promise<CouponDetail> => {
    const res = await request({
        url: _ROBOTAXI_HOST + `/account/api/v1/coupon/detail/${couponId}`,
        method: 'POST'
    });
    // 手动转换下划线字段为驼峰
    return {
        couponId: res.coupon_id,
        title: res.title,
        expireTime: res.expire_time,
        validTimeRange: res.valid_time_range,
        applicableCities: res.applicable_cities || [],
        usageInstructions: res.usage_instructions
    };
}
src/robotaxi/api/customer-service.ts
@@ -3,7 +3,7 @@
export const getServicePhone = async () => {
    return request({
        url: _ROBOTAXI_HOST + '/base/v1/mini-program/support/phone',
        url: _ROBOTAXI_HOST + '/operation/base/v1/mini-program/support/phone',
        method: 'GET'
    });
}
src/robotaxi/assets/coupon-lost-efficacy.png
src/robotaxi/assets/coupon-used.png
src/robotaxi/components/customer-service/index.tsx
@@ -17,7 +17,7 @@
  useEffect(() => {
    if (show && !visible) {
      getServicePhone().then((res) => {
        setPhoneNumber(res?.data?.phone_number);
        setPhoneNumber(res?.msg);
        setTimeout(() => {
          Taro.hideTabBar({ animation: true });
          setVisible(true);
src/robotaxi/pages/begin-route/README.md
New file
@@ -0,0 +1,22 @@
socke消息status:0=已取消,1=待接单,2=待出发,3=接驾中,4=待客上车,5=乘客已上车,6=服务中,7=待客下车,8=待付款,9=待评价,10=已完成
页面tripStatus:1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功
对应关系(status:tripStatus、tripStatus)
    0:弹出showCancelPopup,tripStatus:1
    1:3,判断是否有首次标识passengerNoticeShowed来弹出PassengerNotice
    2:3
    3:4
    4:5,弹出AnnouncementsPopup
    5:6
    6:6
    7:7
    8:8
    9:9
    10:9
tripStatus不同状态标点和线路渲染逻辑,路线都是从socket消息来,车辆点位也是
4:渲染上车点、车辆点位、车辆位置公里、路线
5、6:渲染车辆点位、车辆位置公里、下车点、路线
7:渲染车辆点位、下车点
8、9::渲染车辆上车点、下车点、路线
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -8,6 +8,7 @@
import activeGood from '../../../assets/active-good.png'
import activeBad from '../../../assets/active-bad.png'
import activeNormal from '../../../assets/active-normal.png'
import { getWaitTime } from '../../../api/begin-route';
type Props = {
    tripStatus?: number;
@@ -25,31 +26,13 @@
    const [tempStatus, setTempStatus] = useState(19);
    const [showDetail, setShowDetail] = useState(false);
    const [activeIndex, setActiveIndex] = useState(0) // 默认选中"很满意"
    // 使用 useRef 保存最新的 updateStatus 回调,避免因父组件重绘导致定时器重置
    const updateStatusRef = useRef(updateStatus);
    useEffect(() => {
        updateStatusRef.current = updateStatus;
    }, [updateStatus]);
    // 自动更新状态逻辑
    useEffect(() => {
        let timer: any = null;
        if (tripStatus >= 3 && tripStatus < 7) {
            timer = setInterval(() => {
                if (updateStatusRef.current) {
                    updateStatusRef.current();
                }
            }, 5000);
        }
        return () => {
            if (timer) clearInterval(timer);
        };
    }, [tripStatus]);
    const [waitMinutes, setWaitMinutes] = useState<number | null>(null);
    const [routeDuration, setRouteDuration] = useState<number | null>(null);
    const [limitMinutes, setLimitMinutes] = useState<number | null>(null);
    const titleMap = {
        3: <>预计<View className="time">6</View>分钟内有车应答</>,
        4: <>车辆还有<View className="time">15</View>分钟到达</>,
        3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
        4: <>车辆还有<View className="time">{routeDuration}</View>分钟到达</>,
        5: <>车辆已到达上车点<View className="time">04:49</View></>,
        6: <>预计<View className="time">45</View>分钟抵达目的地</>,
        7: <>已到达目的地<View className="time">04:49</View></>,
@@ -59,12 +42,42 @@
    const descMap = {
        3: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' },
        4: { text: '此处限停5分钟,请尽快前往上车点', className: 'calling-car-content-warning' },
        4: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: 'calling-car-content-warning' } : null,
        5: { text: '此处限停,请在倒计时结束前到达上车点', className: 'calling-car-content-desc' },
        6: { text: '行程已开始,请全程系好安全带', className: 'calling-car-content-desc' },
        7: { text: '倒计时结束时车辆将自动驶离,请带好物品安全下车', className: 'calling-car-content-desc' },
        8: { text: '订单待支付,请核对后完成支付', className: 'calling-car-content-desc' }
    }
    useEffect(() => {
        let isActive = true;
        if (tripStatus === 3) {
            const s = Taro.getStorageSync('startSite');
            const payload = {
                latitude: s?.latitude,
                longitude: s?.longitude
            };
            getWaitTime(payload).then((res) => {
                const minutes = Number(res);
                if (isActive && !Number.isNaN(minutes)) {
                    setWaitMinutes(minutes);
                }
            }).catch(() => { });
        } else if (tripStatus === 4) {
            const duration = Number(Taro.getStorageSync('orderRouteDuration'));
            if (!Number.isNaN(duration)) {
                setRouteDuration(duration);
            }
            const s = Taro.getStorageSync('startSite');
            const time = Number(s?.time);
            if (!Number.isNaN(time)) {
                setLimitMinutes(time);
            }
        }
        return () => {
            isActive = false;
        };
    }, [tripStatus]);
    // 切换评价选项
    const changeEvaluate = (index: number) => {
@@ -109,7 +122,7 @@
                                车身编码: 03
                            </View>
                        </View>
                        <Image className="car-card-detail-img" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e76e8fd6-1f52-482f-b9c0-6a026f4d2797.png' />
                        <Image className="car-card-detail-img" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/825d7fa2-6cf6-4a31-aaac-3a4de806e9a3.png' />
                    </View>
                    {
                        tripStatus == 4 && <View>
@@ -273,4 +286,4 @@
            }
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/css-folder/announcements-popup.less
@@ -12,7 +12,7 @@
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        background: rgba(0, 0, 0, 0);
    }
    .announcements-content {
src/robotaxi/pages/begin-route/index.tsx
@@ -18,7 +18,9 @@
import coupon from '../../../robotaxi/assets/coupon.png'
import suggest from '../../../robotaxi/assets/suggest.png'
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import { queryRouteAndFare } from '../../../robotaxi/api/begin-route';
import wsManager from '../../../utils/websocket';
import { saveOrder, queryRouteAndFare } from '../../../robotaxi/api/begin-route';
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
export default function SelectMapSite() {
@@ -39,27 +41,174 @@
  const [showEvaluatePopup, setShowEvaluatePopup] = useState(false); // 评价弹窗是否显示
  const [routePoints, setRoutePoints] = useState<any[]>([]); // 路线点集合
  const [fareEstimate, setFareEstimate] = useState<any>({}); // 预估费用信息
  const lastRequestParams = useRef<string>(''); // 记录上一次请求参数,防止重复调用
  const [vehicleLocation, setVehicleLocation] = useState<{ latitude: number, longitude: number } | null>(null);
  const [routeDistance, setRouteDistance] = useState<number | null>(null);
  const [routeDuration, setRouteDuration] = useState<number | null>(null);
  const tripStatusRef = useRef(tripStatus);
  const routeRequestKeyRef = useRef<string>('');
  const shouldShowRoute = [4, 5, 6, 8, 9].includes(tripStatus);
  const authSuccessBound = useRef(false); // 认证成功是否绑定
  const messageBound = useRef(false); // 消息是否绑定
  const orderStatusBound = useRef(false); // 订单状态是否绑定
  const orderRouteBound = useRef(false); // 订单路线是否绑定
  const vehicleLocationBound = useRef(false);
  const authSuccessHandlerRef = useRef<(data: any) => void>(); // 认证成功处理函数
  const messageHandlerRef = useRef<(data: any) => void>(); // 消息处理函数
  const orderStatusHandlerRef = useRef<(data: any) => void>(); // 订单状态处理函数
  const orderRouteHandlerRef = useRef<(data: any) => void>(); // 订单路线处理函数
  const vehicleLocationHandlerRef = useRef<(data: any) => void>();
  // 应用路线点
  const applyRoutePoints = (points: any[]) => {
    if (Array.isArray(points) && points.length > 0) {
      setRoutePoints(points);
    } else {
      setRoutePoints([]);
    }
  };
  // 获取存储的路线点
  const getStoredRoutePoints = (status?: number) => {
    const cached = Taro.getStorageSync('orderRoutePoints') || {};
    const preferredType = typeof status === 'number' && status >= 6 ? 2 : 1;
    const preferred = cached[String(preferredType)];
    if (Array.isArray(preferred) && preferred.length > 0) {
      return preferred;
    }
    const latestType = Taro.getStorageSync('orderRouteLatestType');
    const latest = cached[String(latestType)];
    if (Array.isArray(latest) && latest.length > 0) {
      return latest;
    }
    if (Array.isArray(cached.latest) && cached.latest.length > 0) {
      return cached.latest;
    }
    const keys = Object.keys(cached);
    for (const key of keys) {
      if (Array.isArray(cached[key]) && cached[key].length > 0) {
        return cached[key];
      }
    }
    return [];
  };
  const requestRouteAndFare = (originLat: number, originLng: number, destLat: number, destLng: number) => {
    const requestKey = `${originLng},${originLat}-${destLng},${destLat}`;
    if (requestKey === routeRequestKeyRef.current) {
      return;
    }
    routeRequestKeyRef.current = requestKey;
    const selectedCity = Taro.getStorageSync('selectedCity');
    const openCityId = selectedCity ? selectedCity.id : 0;
    const params = {
      destination: {
        latitude: destLat,
        longitude: destLng
      },
      openCityId: openCityId,
      origin: {
        latitude: originLat,
        longitude: originLng
      },
    };
    queryRouteAndFare(params).then((res: any) => {
      if (res?.fareEstimate) {
        setFareEstimate(res.fareEstimate);
      }
    }).catch(() => {
    });
  };
  // 认证成功处理函数
  useEffect(() => {
    authSuccessHandlerRef.current = () => {
      setTripStatus(2);
      if (!messageBound.current && messageHandlerRef.current) {
        wsManager.on('message', messageHandlerRef.current);
        messageBound.current = true;
      }
    };
    messageHandlerRef.current = (data) => {
      console.log('ws message:', data);
    };
    orderStatusHandlerRef.current = (payload) => {
      const mappedTripStatus = payload?.tripStatus;
      if (typeof mappedTripStatus === 'number') {
        setTripStatus(mappedTripStatus);
      }
      if (payload?.clearRoute) {
        setRoutePoints([]);
        setPolyline([]);
      }
      if (payload?.showCancelPopup) {
        setShowCancelPopup(true);
      }
      if (payload?.showAnnouncementsPopup) {
        setShowAnnouncementsPopup(true);
      }
      if (payload?.showPassengerNotice) {
        if (Taro.getStorageSync('passengerNoticeShowed') !== 'true') {
          setShowPassengerNotice(true);
          Taro.setStorageSync('passengerNoticeShowed', 'true');
        }
        if (Taro.getStorageSync('passengerNoticeCheckedTime')) {
          const checkedTime = Number(Taro.getStorageSync('passengerNoticeCheckedTime'));
          if (Date.now() - checkedTime > 3 * 30 * 24 * 60 * 60 * 1000) {
            setShowPassengerNotice(true);
          }
        }
      }
    };
    orderRouteHandlerRef.current = (payload) => {
      const points = payload?.points || [];
      applyRoutePoints(points);
      const distance = Number(payload?.content?.distance);
      const duration = Number(payload?.content?.duration);
      if (!Number.isNaN(distance)) {
        setRouteDistance(distance);
      }
      if (!Number.isNaN(duration)) {
        setRouteDuration(duration);
      }
    };
    vehicleLocationHandlerRef.current = (payload) => {
      const latitude = payload?.latitude;
      const longitude = payload?.longitude;
      if (typeof latitude === 'number' && typeof longitude === 'number') {
        if ([4, 5, 6, 7].includes(tripStatusRef.current)) {
          setVehicleLocation({ latitude, longitude });
        }
      }
    };
    return () => {
      if (authSuccessBound.current && authSuccessHandlerRef.current) {
        wsManager.off('auth_success', authSuccessHandlerRef.current);
        authSuccessBound.current = false;
      }
      if (messageBound.current && messageHandlerRef.current) {
        wsManager.off('message', messageHandlerRef.current);
        messageBound.current = false;
      }
      if (orderStatusBound.current && orderStatusHandlerRef.current) {
        wsManager.off('order_status', orderStatusHandlerRef.current);
        orderStatusBound.current = false;
      }
      if (orderRouteBound.current && orderRouteHandlerRef.current) {
        wsManager.off('order_route', orderRouteHandlerRef.current);
        orderRouteBound.current = false;
      }
      if (vehicleLocationBound.current && vehicleLocationHandlerRef.current) {
        wsManager.off('vehicle_location', vehicleLocationHandlerRef.current);
        vehicleLocationBound.current = false;
      }
    };
  }, []);
  // 监听行程状态,处理等待计时
  useEffect(() => {
    if (tripStatus === 4) {
      if (Taro.getStorageSync('passengerNoticeShowed') !== 'true') {
        setShowPassengerNotice(true);
        Taro.setStorageSync('passengerNoticeShowed', 'true');
      }
      if (Taro.getStorageSync('passengerNoticeCheckedTime')) {
        const checkedTime = Number(Taro.getStorageSync('passengerNoticeCheckedTime'));
        if (Date.now() - checkedTime > 3 * 30 * 24 * 60 * 60 * 1000) {
          setShowPassengerNotice(true);
        }
      }
    }
    if (tripStatus === 5) {
      setShowAnnouncementsPopup(true)
    }
    Taro.setStorageSync('tripStatus', tripStatus);
    tripStatusRef.current = tripStatus;
    let timer: any = null;
    if (tripStatus === 2 || tripStatus === 3) {
      // 状态为2或3时,将地图中心定位到上车点
@@ -81,6 +230,24 @@
    };
  }, [tripStatus]);
  useEffect(() => {
    const storedPoints = getStoredRoutePoints(tripStatus);
    applyRoutePoints(storedPoints);
  }, [tripStatus]);
  useEffect(() => {
    if (shouldShowRoute && routePoints.length > 0) {
      setPolyline([{
        points: routePoints,
        color: '#6D7FD9',
        width: 10,
        arrowLine: true,
      }]);
    } else {
      setPolyline([]);
    }
  }, [tripStatus, routePoints]);
  // 页面初始化
  useDidShow(() => {
    const s = Taro.getStorageSync('startSite');
@@ -98,118 +265,93 @@
    } else {
      getLocationInfo();
    }
    const storedPoints = getStoredRoutePoints(tripStatus);
    applyRoutePoints(storedPoints);
    const storedDistance = Number(Taro.getStorageSync('orderRouteDistance'));
    if (!Number.isNaN(storedDistance)) {
      setRouteDistance(storedDistance);
    }
    const storedDuration = Number(Taro.getStorageSync('orderRouteDuration'));
    if (!Number.isNaN(storedDuration)) {
      setRouteDuration(storedDuration);
    }
    const originLng = s?.longitude || longitude;
    const originLat = s?.latitude || latitude;
    if (originLng && originLat && e?.longitude && e?.latitude) {
      requestRouteAndFare(originLat, originLng, e.latitude, e.longitude);
    }
  });
  // 规划路线
  useEffect(() => {
    const originLng = startSite?.longitude || longitude;
    const originLat = startSite?.latitude || latitude;
    if (!originLng || !originLat || !endSite.longitude || !endSite.latitude) return;
    const requestKey = `${originLng},${originLat}-${endSite.longitude},${endSite.latitude}`;
    if (requestKey === lastRequestParams.current) {
      return;
    if (originLng && originLat && endSite?.longitude && endSite?.latitude) {
      requestRouteAndFare(originLat, originLng, endSite.latitude, endSite.longitude);
    }
    lastRequestParams.current = requestKey;
    const selectedCity = Taro.getStorageSync('selectedCity');
    const openCityId = selectedCity ? selectedCity.id : 0;
    const params = {
      destination: {
        latitude: endSite.latitude,
        longitude: endSite.longitude
      },
      openCityId: openCityId,
      origin: {
        latitude: originLat,
        longitude: originLng
      },
    };
    queryRouteAndFare(params).then((res: any) => {
      if (res && res.route && res.route.polyline) {
        const points = res.route.polyline;
        setRoutePoints(points);
        setFareEstimate(res.fareEstimate || {});
        setPolyline([{
          points: points,
          color: '#6D7FD9',
          width: 10,
          arrowLine: true,
        }]);
      }
    }).catch(err => {
      console.error('规划失败', err);
    });
  }, [endSite.longitude, endSite.latitude, startSite?.longitude, startSite?.latitude, longitude, latitude])
  }, [startSite?.longitude, startSite?.latitude, endSite?.longitude, endSite?.latitude, longitude, latitude]);
  // 生成标记点
  useEffect(() => {
    if (routePoints.length > 0) {
      const startPoint = routePoints[0];
      const endPoint = routePoints[routePoints.length - 1];
      // 检查 storage 中的 startSite 是否存在
      const s = Taro.getStorageSync('startSite');
      if ([1, 2, 3, 4, 7, 8].includes(tripStatus)) {
        const isCar = [4, 7].includes(tripStatus);
        const startMarker: any = {
          id: Date.now() + 1,
          latitude: startPoint.latitude,
          longitude: startPoint.longitude,
          iconPath: isCar
            ? 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e51e92e6-700d-4e83-b3e0-14e6f18e0b9a.png'
            : 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png',
          width: isCar ? 40 : 22,
          height: isCar ? 20 : 40,
          anchor: { x: 0.5, y: 1 },
          zIndex: 2,
        };
        if (tripStatus === 4) {
          startMarker.name = '车辆位置';
          startMarker.customCallout = { anchorY: 0, anchorX: 0, display: 'ALWAYS' };
        } else if (tripStatus !== 7) {
          startMarker.name = s?.name || '当前位置';
          startMarker.customCallout = { anchorY: 8, anchorX: 0, display: 'ALWAYS' };
        }
        setMarkers([
          // 起点指针
          startMarker,
          // 终点指针
          {
            id: Date.now() + 2,
            latitude: endPoint.latitude,
            longitude: endPoint.longitude,
            iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png',
            width: 22,
            height: 40,
            anchor: { x: 0.5, y: 1 },
            zIndex: 2,
            name: tripStatus === 7 ? '已到达' : endSite.name,
            customCallout: {
              anchorY: 8,
              anchorX: 0,
              display: 'ALWAYS'
            }
          },
        ])
    const s = Taro.getStorageSync('startSite');
    const pickupMarker = s?.latitude && s?.longitude ? {
      id: Date.now() + 1,
      latitude: s.latitude,
      longitude: s.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png',
      width: 22,
      height: 40,
      anchor: { x: 0.5, y: 1 },
      zIndex: 2,
      name: s?.name || '上车点',
      customCallout: {
        anchorY: 8,
        anchorX: 0,
        display: 'ALWAYS'
      }
    }
  }, [tripStatus, routePoints, startSite?.name, endSite.name])
    } : null;
    const dropoffMarker = endSite?.latitude && endSite?.longitude ? {
      id: Date.now() + 2,
      latitude: endSite.latitude,
      longitude: endSite.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png',
      width: 22,
      height: 40,
      anchor: { x: 0.5, y: 1 },
      zIndex: 2,
      name: endSite.name || '目的地',
      customCallout: {
        anchorY: 8,
        anchorX: 0,
        display: 'ALWAYS'
      }
    } : null;
    const carMarker = vehicleLocation ? {
      id: Date.now() + 3,
      latitude: vehicleLocation.latitude,
      longitude: vehicleLocation.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e51e92e6-700d-4e83-b3e0-14e6f18e0b9a.png',
      width: 40,
      height: 20,
      anchor: { x: 0.5, y: 1 },
      zIndex: 3,
      name: '车辆位置',
      customCallout: { anchorY: 0, anchorX: 0, display: 'ALWAYS' }
    } : null;
  // 调整视角
    const includePickup = [1, 2, 3, 4, 5, 8, 9].includes(tripStatus);
    const includeDropoff = [1, 6, 7, 8, 9].includes(tripStatus);
    const includeCar = [4, 5, 6, 7].includes(tripStatus);
    const nextMarkers: any[] = [];
    if (includePickup && pickupMarker) nextMarkers.push(pickupMarker);
    if (includeCar && carMarker) nextMarkers.push(carMarker);
    if (includeDropoff && dropoffMarker) nextMarkers.push(dropoffMarker);
    setMarkers(nextMarkers);
  }, [tripStatus, startSite?.name, startSite?.latitude, startSite?.longitude, endSite.name, endSite?.latitude, endSite?.longitude, vehicleLocation])
  useEffect(() => {
    if (routePoints.length > 0) {
    if (shouldShowRoute && routePoints.length > 0) {
      const mapCtx = Taro.createMapContext('map');
      if (tripStatus === 1 || tripStatus === 8 || tripStatus === 7 || tripStatus === 9) {
        // 状态为1(发布行程)或8(行程已结束)时,展示整条路线
      if ([8, 9].includes(tripStatus)) {
        mapCtx.includePoints({
          points: routePoints,
          padding: [50, 50, 500, 50]
@@ -229,6 +371,16 @@
    return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
  }
  const formatDateTime = (date: Date) => {
    const y = date.getFullYear();
    const mo = String(date.getMonth() + 1).padStart(2, '0');
    const d = String(date.getDate()).padStart(2, '0');
    const h = String(date.getHours()).padStart(2, '0');
    const mi = String(date.getMinutes()).padStart(2, '0');
    const s = String(date.getSeconds()).padStart(2, '0');
    return `${y}-${mo}-${d} ${h}:${mi}:${s}`;
  };
  // 获取用户位置信息
  const getLocationInfo = () => {
    Taro.getLocation({ type: 'gcj02' })
@@ -244,13 +396,9 @@
    mapCtx.moveToLocation();
  }
  const handleCloseCustomerService = () => {
    setShowCustomerService(false);
  };
  const openService = () => {
    setShowCustomerService(true);
  }
  const setPopupVisible = (setter: (value: boolean) => void, visible: boolean) => () => setter(visible);
  const handleCloseCustomerService = setPopupVisible(setShowCustomerService, false);
  const openService = setPopupVisible(setShowCustomerService, true);
  const toBack = () => {
@@ -264,7 +412,7 @@
  }
  // 立即呼叫
  const callImmediately = () => {
  const callImmediately = async () => {
    if (!endSite.name) {
      Taro.showToast({
        title: '请选择站点',
@@ -272,14 +420,54 @@
      })
      return
    }
    setTripStatus(2)
    // 校验上车点是否存在
    const s = Taro.getStorageSync('startSite');
    if (s && s.latitude && s.longitude) {
      // 移动地图中心到上车点
      setLatitude(s.latitude);
      setLongitude(s.longitude);
    if (!s?.latitude || !s?.longitude || !endSite?.latitude || !endSite?.longitude) {
      Taro.showToast({
        title: '请先选择起终点',
        icon: 'none',
      })
      return
    }
    const selectedCity = Taro.getStorageSync('selectedCity');
    const openCityId = selectedCity?.id || 0;
    const waitingTime = Number(fareEstimate?.waitingTime || 0);
    const estimatedTime = formatDateTime(new Date(Date.now() + waitingTime * 60 * 1000));
    const payload = {
      endPointId: endSite.id ?? endSite.pointId ?? endSite.point_id ?? 0,
      openCityId: openCityId,
      placementAddress: s?.address || s?.name || '',
      placementLat: s?.latitude,
      placementLon: s?.longitude,
      startPointId: s?.id ?? s?.pointId ?? s?.point_id ?? 0,
      xpElectronicFenceAreaId: s?.electronicAreaId || endSite?.electronicAreaId || '',
      estimatedTime: estimatedTime,
    };
    try {
      await saveOrder(payload);
    } catch (error) {
      Taro.showToast({
        title: '下单失败,请重试',
        icon: 'none',
      })
      return
    }
    if (!authSuccessBound.current && authSuccessHandlerRef.current) {
      wsManager.on('auth_success', authSuccessHandlerRef.current);
      authSuccessBound.current = true;
    }
    if (!orderStatusBound.current && orderStatusHandlerRef.current) {
      wsManager.on('order_status', orderStatusHandlerRef.current);
      orderStatusBound.current = true;
    }
    if (!orderRouteBound.current && orderRouteHandlerRef.current) {
      wsManager.on('order_route', orderRouteHandlerRef.current);
      orderRouteBound.current = true;
    }
    if (!vehicleLocationBound.current && vehicleLocationHandlerRef.current) {
      wsManager.on('vehicle_location', vehicleLocationHandlerRef.current);
      vehicleLocationBound.current = true;
    }
    wsManager.connect(_ROBOTAXI_WS_HOST);
  }
  // 确认取消
@@ -289,29 +477,11 @@
  }
  // 关闭取消弹窗
  const handleCloseCancelPopup = () => {
    setShowCancelPopup(false);
  }
  // 打开取消弹窗
  const handleOpenCancelPopup = () => {
    setShowCancelPopup(true);
  }
  // 关闭安全注意事项弹窗
  const handleCloseAnnouncementsPopup = () => {
    setShowAnnouncementsPopup(false);
  }
  // 打开结束行程弹窗
  const handleOpenOverRoutePopup = () => {
    setShowOverRoutePopup(true);
  }
  // 关闭结束行程弹窗
  const handleCloseOverRoutePopup = () => {
    setShowOverRoutePopup(false);
  }
  const handleCloseCancelPopup = setPopupVisible(setShowCancelPopup, false);
  const handleOpenCancelPopup = setPopupVisible(setShowCancelPopup, true);
  const handleCloseAnnouncementsPopup = setPopupVisible(setShowAnnouncementsPopup, false);
  const handleOpenOverRoutePopup = setPopupVisible(setShowOverRoutePopup, true);
  const handleCloseOverRoutePopup = setPopupVisible(setShowOverRoutePopup, false);
  // 确认结束行程
  const handleConfirmOverRoute = () => {
@@ -325,42 +495,27 @@
  }
  // 关闭乘车需知弹窗
  const handleClosePassengerNotice = () => {
    setShowPassengerNotice(false);
  }
  const handleClosePassengerNotice = setPopupVisible(setShowPassengerNotice, false);
  // 打开评价弹窗
  const handleOpenEvaluatePopup = () => {
    setShowEvaluatePopup(true);
  }
  // 关闭评价弹窗
  const handleCloseEvaluatePopup = () => {
    setShowEvaluatePopup(false);
  }
  const handleOpenEvaluatePopup = setPopupVisible(setShowEvaluatePopup, true);
  const handleCloseEvaluatePopup = setPopupVisible(setShowEvaluatePopup, false);
  // 确认评价
  const handleConfirmEvaluate = () => {
    setShowEvaluatePopup(false);
  }
  const handleConfirmEvaluate = handleCloseEvaluatePopup;
  const toSelectSite = (id: number) => {
    if (id === 1) {
      Taro.navigateTo({
        url: '/robotaxi/pages/site-search/index?pageType=start'
      })
    } else {
      Taro.navigateTo({
        url: '/robotaxi/pages/site-search/index?pageType=end'
      })
    }
  const toSelectSite = (pageType: 'start' | 'end') => {
    Taro.navigateTo({
      url: `/robotaxi/pages/site-search/index?pageType=${pageType}`
    })
  }
  // 点击气泡
  const handleCalloutTap = (e) => {
    if (tripStatus === 1) {
      // 仅在发布行程时,点击标记用于选择起点或终点
      const markerId = e.detail.markerId;
      toSelectSite(markerId);
      toSelectSite(markerId === 1 ? 'start' : 'end');
    }
  }
@@ -420,12 +575,12 @@
                  </CoverView>
                }
                {
                  item.name === '车辆位置' &&
                  (item.name === '车辆位置' && [4, 5, 6].includes(tripStatus)) &&
                  <CoverView className="custom-callout-content">
                    <CoverView className="car-time">
                      <CoverView className="car-time-num">3</CoverView>
                      <CoverView className="car-time-num">{routeDistance ?? 0}</CoverView>
                      <CoverView>公里</CoverView>
                      <CoverView className="car-time-num">2</CoverView>
                      <CoverView className="car-time-num">{routeDuration ?? 0}</CoverView>
                      <CoverView>分钟</CoverView>
                    </CoverView>
                  </CoverView>
src/robotaxi/pages/coupon/index.tsx
@@ -1,72 +1,84 @@
import Taro from '@tarojs/taro';
import { Component, useState, useEffect } from 'react';
import Taro, { useReachBottom } from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { View, Text, Image } from '@tarojs/components';
import './index.less';
import icon from '../../assets/Subtract.png'
import close from '../../assets/close.png'
import { getCouponList, receiveCoupon, getCouponDetail, CouponItem, CouponDetail } from '../../../robotaxi/api/coupon';
export default function CouponPage() {
  const [couponStatus, setCouponStatus] = useState([
    { label: '全部', value: '1' },
    { label: '未使用', value: '2' },
    { label: '已使用', value: '3' },
    { label: '已失效', value: '4' }])
  const [activeStatus, setActiveStatus] = useState('1')
  const [couponList, setCouponList] = useState([
    {
      id: '1',
      name: '优惠券名称',
      time: '2026-03-04 12:12:01',
      price: '20'
    }, {
      id: '2',
      name: '优惠券名称',
      time: '2026-03-04 12:12:01',
      price: '20'
    }, {
      id: '3',
      name: '优惠券名称',
      time: '2026-03-04 12:12:01',
      price: '20'
    }, {
      id: '4',
      name: '优惠券名称',
      time: '2026-03-04 12:12:01',
      price: '20'
    }, {
      id: '5',
      name: '优惠券名称',
      time: '2026-03-04 12:12:01',
      price: '20'
    }, {
      id: '6',
      name: '优惠券名称',
      time: '2026-03-04 12:12:01',
      price: '20'
    }, {
      id: '7',
      name: '优惠券名称',
      time: '2026-03-04 12:12:01',
      price: '20'
    }])
  const [animationData, setAnimationData] = useState(null)
  const [couponStatus] = useState([
    { label: '全部', value: '' },
    { label: '待领取', value: 0 },
    { label: '未使用', value: 1 },
    { label: '已使用', value: 2 },
    { label: '已失效', value: 3 }])
  const [activeStatus, setActiveStatus] = useState<string | number>('')
  const [couponList, setCouponList] = useState<CouponItem[]>([])
  const [animationData, setAnimationData] = useState<any>(null)
  const [visible, setVisible] = useState(false)
  const [pageNum, setPageNum] = useState(1)
  const [hasMore, setHasMore] = useState(true)
  const [currentCoupon, setCurrentCoupon] = useState<CouponDetail | null>(null)
  const pageSize = 10
  const getList = async (page: number, status: string | number, isRefresh = false) => {
    try {
      const res = await getCouponList({ pageNum: page, pageSize, status });
      const newList = res.list || [];
      if (isRefresh) {
        setCouponList(newList);
      } else {
        setCouponList(prev => [...prev, ...newList]);
      }
      // 如果返回数量少于 pageSize,说明没有更多数据了
      setHasMore(newList.length >= pageSize);
    } catch (e) {
      console.error(e);
      // 出错时如果是刷新,清空列表
      if (isRefresh) setCouponList([]);
    }
  }
  useEffect(() => {
    // 切换 tab 时重置并加载第一页
    setPageNum(1);
    setHasMore(true);
    getList(1, activeStatus, true);
  }, [activeStatus])
  useReachBottom(() => {
    if (hasMore) {
      const nextPage = pageNum + 1;
      setPageNum(nextPage);
      getList(nextPage, activeStatus);
    }
  })
  const changeStatus = (value) => {
  const changeStatus = (value: string | number) => {
    setActiveStatus(value)
  }
  const openPopup = () => {
    const animation = Taro.createAnimation({
      duration: 300,
      timingFunction: 'ease-out'
    });
    animation.translateY(0).step();
    setAnimationData(animation.export())
    setVisible(true)
  const openPopup = async (item: CouponItem) => {
    try {
      Taro.showLoading({ title: '加载中...' });
      const detail = await getCouponDetail(item.couponId);
      setCurrentCoupon(detail);
      Taro.hideLoading();
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
      });
      animation.translateY(0).step();
      setAnimationData(animation.export())
      setVisible(true)
    } catch (error) {
      Taro.hideLoading();
      Taro.showToast({ title: '获取详情失败', icon: 'none' });
    }
  }
  const closePopup = () => {
    const animation = Taro.createAnimation({
      duration: 300,
@@ -75,7 +87,19 @@
    animation.translateY(500).step();
    setAnimationData(animation.export())
    setVisible(false)
  }
  const handleReceive = async (e, item: CouponItem) => {
    e.stopPropagation();
    try {
      await receiveCoupon({ couponId: item.couponId });
      Taro.showToast({ title: '领取成功', icon: 'success' });
      // 领取成功后刷新列表
      getList(1, activeStatus, true);
      setPageNum(1);
    } catch (error) {
      Taro.showToast({ title: '领取失败', icon: 'none' });
    }
  }
  return (
@@ -87,28 +111,37 @@
      </View>
      <View className='coupon-card'>
        {couponList.map((item, index) => {
          return <View className='coupon-item' key={item.id || index}>
          return <View className='coupon-item' key={item.couponId || index}>
            <View className='coupon-price'>
              <Text className='price'>{item.price}</Text>
              <Text className='price'>{item.discountAmount}</Text>
              <Text className='unit'>元</Text>
            </View>
            <View className='coupon-info'>
              <Text className='name'>{item.name}</Text>
              <Text className='time'>{item.time}到期</Text>
              <Text className='name'>{item.title}</Text>
              <Text className='time'>{item.expireTime}到期</Text>
              <View className='more' onClick={() => openPopup(item)}>
                <Text className='more-detail'>查看详情</Text>
                <Image className='more-img' src={icon}></Image>
              </View>
            </View>
            <View className='receive'>领取</View>
            {/* 状态 1-未使用 2-已使用 3-已过期 0待领取 */}
            {item.status === 0 ? (
                <View className='receive' onClick={(e) => handleReceive(e, item)}>领取</View>
            ) : item.status === 1 ? (
                <View className='receive' style={{backgroundColor: '#ccc'}}>未使用</View>
            ) : item.status === 2 ? (
                <View className='receive' style={{backgroundColor: '#f5f5f5', color: '#999'}}>已使用</View>
            ) : (
                <View className='receive' style={{backgroundColor: '#f5f5f5', color: '#999'}}>已过期</View>
            )}
          </View>
        })}
      </View>
      {/* 优惠券说明 */}
      {visible && <View className='privacy-popup' catchMove>
        <View className='privacy-mask'></View>
      {visible && currentCoupon && <View className='privacy-popup' catchMove>
        <View className='privacy-mask' onClick={closePopup}></View>
        <View className='privacy-content' animation={animationData}>
          {/* 头部:标题 + 关闭*/}
          <View className='privacy-header'>
@@ -118,19 +151,19 @@
          {/* 内容文案 */}
          <View className='remark-card'>
            <Text className='remark-label'>有效时间</Text>
            <Text className='remark-value'>2025-12-16 失效</Text>
            <Text className='remark-value'>{currentCoupon.expireTime} 失效</Text>
          </View>
          <View className='remark-card'>
            <Text className='remark-label'>使用时段</Text>
            <Text className='remark-value'>运营时间内可用</Text>
            <Text className='remark-value'>{currentCoupon.validTimeRange}</Text>
          </View>
          <View className='remark-card'>
            <Text className='remark-label'>使用地区</Text>
            <Text className='remark-value'> 广州、武汉</Text>
            <Text className='remark-value'> {(currentCoupon.applicableCities || []).join('、')}</Text>
          </View>
          <View className='remark-card'>
            <Text className='remark-label'>使用说明</Text>
            <Text className='remark-value'>小鹏Robotaxi用户专享,领取后7天有效 适用于小鹏Robotaxi设定运营区内使用 如有疑问请联系:400-xxx-xxxxxx</Text>
            <Text className='remark-value'>{currentCoupon.usageInstructions}</Text>
          </View>
        </View>
src/robotaxi/pages/select-map-site/index.tsx
@@ -192,6 +192,14 @@
      return
    }
    if (pageType === 'start') {
      const endSiteCache = Taro.getStorageSync('endSite');
      if (endSiteCache?.electronicAreaId && selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== selectedSite.electronicAreaId) {
        Taro.showToast({
          title: '起终点需在同一运营区域',
          icon: 'none',
        })
        return
      }
      // 校验当前位置与选择的上车点距离
      if (longitude && latitude) {
        const distance = getDistance(latitude, longitude, selectedSite.latitude, selectedSite.longitude);
@@ -202,18 +210,21 @@
        }
      }
      Taro.setStorageSync('startSite', {
        latitude: selectedSite.latitude,
        longitude: selectedSite.longitude,
        name: selectedSite.name,
      })
      Taro.setStorageSync('startSite', selectedSite)
      Taro.navigateBack({
        delta: 2,
      })
    }
    if (pageType === 'end') {
      // 校验上车点与目的地的距离
      const startSiteCache = Taro.getStorageSync('startSite');
      if (startSiteCache?.electronicAreaId && selectedSite?.electronicAreaId && startSiteCache.electronicAreaId !== selectedSite.electronicAreaId) {
        Taro.showToast({
          title: '起终点需在同一运营区域',
          icon: 'none',
        })
        return
      }
      // 校验上车点与目的地的距离
      if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) {
        const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, selectedSite.latitude, selectedSite.longitude);
        if (distance < distanceConfig.minDistance) {
@@ -223,11 +234,7 @@
        }
      }
      Taro.setStorageSync('endSite', {
        latitude: selectedSite.latitude,
        longitude: selectedSite.longitude,
        name: selectedSite.name,
      })
      Taro.setStorageSync('endSite', selectedSite)
      Taro.reLaunch({
        url: '/robotaxi/pages/begin-route/index',
      })
src/robotaxi/pages/site-search/index.tsx
@@ -131,9 +131,25 @@
  const onChangeSelectSite = (item) => {
    if (pageType == 'start') {
      const endSiteCache = Taro.getStorageSync('endSite');
      if (endSiteCache?.electronicAreaId && item?.electronicAreaId && endSiteCache.electronicAreaId !== item.electronicAreaId) {
        Taro.showToast({
          title: '起终点需在同一运营区域',
          icon: 'none',
        })
        return
      }
      Taro.setStorageSync('startSite', item);
      Taro.navigateBack()
    } else if (pageType == 'end') {
      const startSiteCache = Taro.getStorageSync('startSite');
      if (startSiteCache?.electronicAreaId && item?.electronicAreaId && startSiteCache.electronicAreaId !== item.electronicAreaId) {
        Taro.showToast({
          title: '起终点需在同一运营区域',
          icon: 'none',
        })
        return
      }
      Taro.setStorageSync('endSite', item)
      Taro.reLaunch({
        url: '/robotaxi/pages/begin-route/index',
src/utils/websocket.ts
@@ -1,4 +1,6 @@
import Taro from '@tarojs/taro';
import { getStorage } from './tools';
import { _UID } from '../constants/api';
type MessageCallback = (data: any) => void;
@@ -13,8 +15,9 @@
  private heartbeatTimer: any = null;
  private heartbeatInterval: number = 30000; // 30秒心跳
  private listeners: Map<string, MessageCallback[]> = new Map();
  private globalEventsBound: boolean = false;
  private constructor() {}
  private constructor() { }
  public static getInstance(): WebSocketManager {
    if (!WebSocketManager.instance) {
@@ -27,7 +30,7 @@
   * 初始化连接
   * @param url WebSocket地址
   */
  public connect(url: string) {
  public async connect(url: string) {
    if (this.socketTask || this.isConnected) {
      console.log('WebSocket 已连接或正在连接中');
      return;
@@ -36,41 +39,111 @@
    this.url = url;
    console.log(`正在连接 WebSocket: ${url}`);
    Taro.connectSocket({
      url: this.url,
      success: (task) => {
        this.socketTask = task;
        this.initEventListeners();
    const uid = await getStorage(_UID) || '-1';
    const task = Taro.connectSocket({
      header: {
        'XP-Uid': uid.toString(),
      },
      url: this.url,
      success: () => { },
      fail: (err) => {
        console.error('WebSocket 连接失败:', err);
        this.handleReconnect();
      }
    });
    const maybeTask: any = task as any;
    if (maybeTask && typeof maybeTask.onOpen === 'function') {
      this.socketTask = maybeTask;
      this.initEventListeners();
      return;
    }
    if (maybeTask && typeof maybeTask.then === 'function') {
      maybeTask
        .then((resolvedTask) => {
          this.socketTask = resolvedTask;
          this.initEventListeners();
        })
        .catch((err) => {
          console.error('WebSocket 连接失败:', err);
          this.handleReconnect();
        });
      return;
    }
    this.socketTask = null;
    this.initEventListeners();
  }
  /**
   * 初始化事件监听
   */
  private initEventListeners() {
    if (!this.socketTask) return;
    if (this.socketTask && typeof this.socketTask.onOpen === 'function') {
      this.socketTask.onOpen(async () => {
        console.log('WebSocket 连接已打开');
        this.isConnected = true;
        this.reconnectAttempts = 0;
        this.startHeartbeat();
        const uid = await getStorage(_UID) || '-1';
        this.send({ type: 'auth', appUserId: uid.toString(), timestamp: Date.now() });
        this.notifyListeners('open', { status: 'connected' });
      });
    this.socketTask.onOpen(() => {
      this.socketTask.onMessage((res) => {
        try {
          const data = JSON.parse(res.data as string);
          const content = JSON.parse(data.content || '{}');
          this.handleIncomingMessage(data, content);
          if (data.type) {
            this.notifyListeners(data.type, data);
          }
          this.notifyListeners('message', data);
        } catch (e) {
          console.warn('解析 WebSocket 消息失败:', res.data);
          this.notifyListeners('message', res.data);
        }
      });
      this.socketTask.onClose((res) => {
        console.log('WebSocket 连接已关闭', res);
        this.isConnected = false;
        this.stopHeartbeat();
        this.socketTask = null;
        if (res.code !== 1000) {
          this.handleReconnect();
        }
        this.notifyListeners('close', res);
      });
      this.socketTask.onError((err) => {
        console.error('WebSocket 错误:', err);
        this.isConnected = false;
        this.notifyListeners('error', err);
      });
      return;
    }
    if (this.globalEventsBound) return;
    this.globalEventsBound = true;
    Taro.onSocketOpen(async () => {
      console.log('WebSocket 连接已打开');
      this.isConnected = true;
      this.reconnectAttempts = 0;
      this.startHeartbeat();
      const uid = await getStorage(_UID) || '-1';
      this.send({ type: 'auth', appUserId: uid.toString(), timestamp: Date.now() });
      this.notifyListeners('open', { status: 'connected' });
    });
    this.socketTask.onMessage((res) => {
    Taro.onSocketMessage((res) => {
      try {
        const data = JSON.parse(res.data as string);
        // 假设后端消息格式包含 type 字段来区分消息类型
        const content = JSON.parse(data.content || '{}');
        this.handleIncomingMessage(data, content);
        if (data.type) {
          this.notifyListeners(data.type, data);
        }
        // 同时触发通用的 message 事件
        this.notifyListeners('message', data);
      } catch (e) {
        console.warn('解析 WebSocket 消息失败:', res.data);
@@ -78,20 +151,19 @@
      }
    });
    this.socketTask.onClose((res) => {
    Taro.onSocketClose((res) => {
      console.log('WebSocket 连接已关闭', res);
      this.isConnected = false;
      this.stopHeartbeat();
      this.socketTask = null;
      // 如果不是手动关闭(code 1000),则尝试重连
      if (res.code !== 1000) {
        this.handleReconnect();
      }
      this.notifyListeners('close', res);
    });
    this.socketTask.onError((err) => {
    Taro.onSocketError((err) => {
      console.error('WebSocket 错误:', err);
      this.isConnected = false;
      this.notifyListeners('error', err);
@@ -103,18 +175,27 @@
   * @param data 消息内容
   */
  public send(data: any) {
    if (!this.isConnected || !this.socketTask) {
    if (!this.isConnected) {
      console.warn('WebSocket 未连接,消息未发送:', data);
      return;
    }
    const message = typeof data === 'string' ? data : JSON.stringify(data);
    this.socketTask.send({
      data: message,
      fail: (err) => {
        console.error('发送消息失败:', err);
      }
    });
    if (this.socketTask) {
      this.socketTask.send({
        data: message,
        fail: (err) => {
          console.error('发送消息失败:', err);
        }
      });
    } else {
      Taro.sendSocketMessage({
        data: message,
        fail: (err) => {
          console.error('发送消息失败:', err);
        }
      });
    }
  }
  /**
@@ -126,7 +207,7 @@
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    if (this.socketTask) {
      this.socketTask.close({
        code: 1000,
@@ -224,6 +305,100 @@
      });
    }
  }
  // 处理传入消息
  private handleIncomingMessage(data: any, content: any) {
    if (data?.type === 'trip_status' || data?.type === 'trip_status_update') {
      const status = content?.tripStatus ?? content?.trip_status ?? data?.tripStatus ?? data?.trip_status;
      if (typeof status === 'number') {
        Taro.setStorageSync('tripStatus', status);
        this.notifyListeners('trip_status_change', { status, data, content });
      }
    }
    if (data?.type === 'ORDER_STATUS') {
      const status = content?.type ?? content?.status ?? data?.status;
      if (typeof status === 'number') {
        const mapped = this.mapOrderStatusToTripStatus(status);
        if (typeof mapped.tripStatus === 'number') {
          Taro.setStorageSync('tripStatus', mapped.tripStatus);
        }
        this.notifyListeners('order_status', { status, ...mapped, data, content });
      }
    }
    if (data?.type === 'VEHICLE_LOCATION') {
      const longitude = content?.lon ?? content?.lng ?? content?.longitude;
      const latitude = content?.lat ?? content?.latitude;
      if (typeof longitude === 'number' && typeof latitude === 'number') {
        this.notifyListeners('vehicle_location', { longitude, latitude, data, content });
      }
    }
    if (data?.type === 'ORDER_ROUTE') {
      const routeType = content?.type ?? content?.routeType ?? content?.route_type;
      const duration = Number(content?.duration);
      const distance = Number(content?.distance);
      const points = this.normalizeRoutePoints(content);
      if (!Number.isNaN(duration)) {
        Taro.setStorageSync('orderRouteDuration', duration);
      }
      if (!Number.isNaN(distance)) {
        Taro.setStorageSync('orderRouteDistance', distance);
      }
      if (points.length > 0) {
        const cached = Taro.getStorageSync('orderRoutePoints') || {};
        if (typeof routeType === 'number') {
          cached[String(routeType)] = points;
          Taro.setStorageSync('orderRouteLatestType', routeType);
        } else {
          cached.latest = points;
        }
        Taro.setStorageSync('orderRoutePoints', cached);
        this.notifyListeners('order_route', { routeType, points, data, content });
      }
    }
  }
  // 归一化路线点
  private normalizeRoutePoints(content: any) {
    const route = JSON.parse(content?.route ?? '{}');
    const rawPoints = route?.polyline ?? [];
    if (!Array.isArray(rawPoints)) return [];
    return rawPoints.map((item: any) => {
      const latitude = item?.latitude;
      const longitude = item?.longitude;
      if (typeof latitude === 'number' && typeof longitude === 'number') {
        return { latitude, longitude };
      }
      return null;
    }).filter(Boolean);
  }
  // 映射订单状态到行程状态
  private mapOrderStatusToTripStatus(status: number) {
    switch (status) {
      case 0:
        return { tripStatus: 1, showCancelPopup: true };
      case 1:
        return { tripStatus: 3, showPassengerNotice: true };
      case 2:
        return { tripStatus: 3 };
      case 3:
        return { tripStatus: 4 };
      case 4:
        return { tripStatus: 5, showAnnouncementsPopup: true };
      case 5:
      case 6:
        return { tripStatus: 6 };
      case 7:
        return { tripStatus: 7 };
      case 8:
        return { tripStatus: 8 };
      case 9:
      case 10:
        return { tripStatus: 9 };
      default:
        return { tripStatus: undefined };
    }
  }
}
export default WebSocketManager.getInstance();