zjk
2026-06-02 b63c5c017bc859e7a92958bbf50dcf5277a78ba3
Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
1个文件已删除
17个文件已修改
834 ■■■■■ 已修改文件
src/pages/cashier/index.tsx 322 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/privacy-popup/index.less 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/privacy-popup/index.tsx 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/types/begin-route.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/trip-motion-utils.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-request.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-speed-scale.ts 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cashier/index.tsx
@@ -2,7 +2,7 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import { View, Text, Image, Button } from '@tarojs/components';
import { dispatchGetOrderInfo, dispatchMakePay, dispatchGetResult, dispatchCreditPaySign, dispatchCreditPaySignQuery, dispatchCreditOrderCreate, dispatchCreditOrderQuery } from '../../actions/cashier';
import { dispatchGetOrderInfo, dispatchMakePay, dispatchGetResult } from '../../actions/cashier';
import { getStorage } from '../../utils/tools';
import { jumpTo } from '../../utils/jump';
import { dispatchSubscribeCreateFree } from '../../actions/im';
@@ -17,10 +17,6 @@
  dispatchMakePay: any
  dispatchGetResult: any
  dispatchSubscribeCreateFree: any
  dispatchCreditPaySign: any
  dispatchCreditPaySignQuery: any
  dispatchCreditOrderCreate: any
  dispatchCreditOrderQuery: any
}
type State = {
@@ -32,7 +28,7 @@
const ENV = process.env.TARO_ENV;
// @ts-ignore
@connect(() => ({}), { dispatchGetOrderInfo, dispatchMakePay, dispatchGetResult, dispatchSubscribeCreateFree, dispatchCreditPaySign, dispatchCreditPaySignQuery, dispatchCreditOrderCreate, dispatchCreditOrderQuery })
@connect(() => ({}), { dispatchGetOrderInfo, dispatchMakePay, dispatchGetResult, dispatchSubscribeCreateFree })
/**
 * 收银台页面
 *
@@ -129,7 +125,7 @@
            return;
          }
          if (ENV === 'weapp') {
            if (item.payType === 1 || item.payType === 11 || item.payType === 17 || item.payType === 19 || item.payType === 13) {
            if (item.payType === 1 || item.payType === 11 || item.payType === 17 || item.payType === 19) {
              payType = item.payType;
            }
          } else if (ENV === 'alipay') {
@@ -165,12 +161,6 @@
    const { payType } = this.state;
    // 微信支付分走单独流程
    if (payType === 13) {
      this.creditPay();
      return;
    }
    const { openId } = await this.$app.getIdData() || {};
    getStorage(_UID)
      .then(res => {
@@ -199,297 +189,6 @@
        console.log(err)
      })
      .then(() => this.loading = false);
  }
  // 微信支付分支付流程
  creditPay = async () => {
    try {
      // 第一步:查询微信支付分开通状态
      const status = await this.queryCreditPaySignStatus();
      console.log('creditPay sign status:', status);
      if (status === 'VALID') {
        // 已开通,进入信用下单流程
        await this.creditOrderCreate();
      } else if (status === 'INVALID' || status === 'INIT') {
        // 未签约,走签约开通流程
        await this.creditPaySign();
      } else {
        Taro.showToast({ title: `当前状态: ${status || '未知'},暂不支持`, icon: 'none' });
        this.resetLoading();
      }
    } catch (err) {
      console.error('creditPay error:', err);
      Taro.showToast({ title: '查询支付分状态失败', icon: 'none' });
      this.resetLoading();
    }
  }
  // 微信支付分签约开通
  creditPaySign = async () => {
    try {
      // 调用签约接口获取 signStr(预授权token)
      const signRes = await this.props.dispatchCreditPaySign({
        token: this.token,
        payType: 13,
        returnUrl: null,
        cancelUrl: null,
      });
      console.log('creditPaySign result:', signRes);
      const signStr = signRes?.signStr;
      if (!signStr) {
        Taro.showToast({ title: '获取授权信息失败', icon: 'none' });
        this.resetLoading();
        return;
      }
      // 拉起微信支付分授权窗口
      // @ts-ignore
      if (wx.openBusinessView) {
        // 即将跳转到微信侧,解锁 loading(用户在外部小程序中无法操作当前页面)
        this.resetLoading();
        // @ts-ignore
        wx.openBusinessView({
          businessType: 'wxpayScoreEnable',
          extraData: {
            apply_permissions_token: signStr,
          },
          success: () => {
            console.log('openBusinessView success');
          },
          fail: (err) => {
            console.error('openBusinessView fail:', err);
            Taro.showToast({ title: '拉起授权失败', icon: 'none' });
          },
          complete: () => {
            console.log('openBusinessView complete');
          },
        });
        // 监听从微信支付分小程序返回
        const handleAppShow = async (res) => {
          if (res && res.scene === 1038) {
            const { appId } = res.referrerInfo || {};
            if (appId === 'wxd8f3793ea3b935b8') {
              Taro.offAppShow(handleAppShow);
              this.appShowListener = null;
              // 用户返回,重新加锁
              this.loading = true;
              try {
                const newStatus = await this.queryCreditPaySignStatus();
                console.log('sign callback status:', newStatus);
                if (newStatus === 'VALID') {
                  Taro.showToast({ title: '开通成功', icon: 'success' });
                  await this.creditOrderCreate();
                } else {
                  Taro.showToast({ title: '开通未完成', icon: 'none' });
                  this.resetLoading();
                }
              } catch (e) {
                console.error('query sign status after auth error:', e);
                Taro.showToast({ title: '查询签约结果失败', icon: 'none' });
                this.resetLoading();
              }
            }
          }
        };
        this.appShowListener = handleAppShow;
        Taro.onAppShow(handleAppShow);
      } else {
        Taro.showToast({ title: '请升级微信版本后重试', icon: 'none' });
        this.resetLoading();
      }
    } catch (err) {
      console.error('creditPaySign error:', err);
      Taro.showToast({ title: '签约请求失败', icon: 'none' });
      this.resetLoading();
    }
  }
  // 信用下单
  creditOrderCreate = async () => {
    try {
      Taro.showLoading({ title: '下单中...' });
      const { openId } = await this.$app.getIdData() || {};
      const orderRes = await this.props.dispatchCreditOrderCreate({
        token: this.token,
        payType: 13,
        openId,
      });
      Taro.hideLoading();
      console.log('creditOrderCreate result:', orderRes);
      if (!orderRes) {
        Taro.showToast({ title: '信用下单失败', icon: 'none' });
        this.resetLoading();
        return;
      }
      const { businessOrderId, authOrderId, channelOrderId } = orderRes;
      const packageStr = orderRes.package;
      // 拉起微信支付分订单确认页
      // @ts-ignore
      if (wx.openBusinessView) {
        // 即将跳转到微信侧,解锁 loading
        this.resetLoading();
        // @ts-ignore
        wx.openBusinessView({
          businessType: 'wxpayScoreUse',
          extraData: {
            package: packageStr,
          },
          success: () => {
            console.log('openBusinessView wxpayScoreUse success');
          },
          fail: (err) => {
            console.error('openBusinessView wxpayScoreUse fail:', err);
            Taro.showToast({ title: '拉起支付分订单失败', icon: 'none' });
          },
          complete: () => {
            console.log('openBusinessView wxpayScoreUse complete');
          },
        });
        // 监听从微信支付分小程序返回
        const handleAppShow = async (res) => {
          if (res && res.scene === 1038) {
            const { appId } = res.referrerInfo || {};
            if (appId === 'wxd8f3793ea3b935b8') {
              Taro.offAppShow(handleAppShow);
              this.appShowListener = null;
              this.loading = true;
              // 轮询查询信用订单状态
              await this.pollCreditOrderStatus({ businessOrderId, authOrderId, channelOrderId });
            }
          }
        };
        this.appShowListener = handleAppShow;
        Taro.onAppShow(handleAppShow);
      } else {/*  */
        Taro.showToast({ title: '请升级微信版本后重试', icon: 'none' });
        this.resetLoading();
      }
    } catch (err) {
      Taro.hideLoading();
      console.error('creditOrderCreate error:', err);
      Taro.showToast({ title: '信用下单失败', icon: 'none' });
      this.resetLoading();
    }
  }
  // 轮询查询信用订单状态
  pollCreditOrderStatus = ({ businessOrderId, authOrderId, channelOrderId }): Promise<void> => {
    const maxRetries = 10;
    const interval = 2000;
    Taro.showLoading({ title: '查询订单状态...' });
    return new Promise((resolve) => {
      let retries = 0;
      const poll = async () => {
        retries++;
        try {
          const queryRes = await this.props.dispatchCreditOrderQuery({
            token: this.token,
            businessOrderId,
            authOrderId,
            channelOrderId,
            payType: 13,
          });
          console.log('creditOrderQuery result:', queryRes);
          const status = queryRes?.creditOrderStatus;
          if (status === 'DOING' || status === 'DONE') {
            Taro.hideLoading();
            this.resetLoading();
            this.creditOrderSuccess(queryRes);
            resolve();
            return;
          }
          if (status === 'REVOKED' || status === 'EXPIRED') {
            Taro.hideLoading();
            this.resetLoading();
            Taro.showToast({ title: '订单已取消或失效', icon: 'none' });
            resolve();
            return;
          }
          if (retries >= maxRetries) {
            Taro.hideLoading();
            this.resetLoading();
            Taro.showToast({ title: '查询超时,请稍后再试', icon: 'none' });
            resolve();
            return;
          }
          // 继续轮询
          this.timer = setTimeout(poll, interval);
        } catch (err) {
          console.error('pollCreditOrderStatus error:', err);
          if (retries >= maxRetries) {
            Taro.hideLoading();
            this.resetLoading();
            Taro.showToast({ title: '查询订单状态失败', icon: 'none' });
            resolve();
            return;
          }
          // 出错也继续重试
          this.timer = setTimeout(poll, interval);
        }
      };
      poll();
    });
  }
  // 统一重置 loading 状态
  resetLoading = () => {
    this.loading = false;
  }
  // 信用订单成功回调
  creditOrderSuccess = (orderRes) => {
    console.log('creditOrderSuccess:', orderRes);
    if (this.from === 'app') {
      this.setState({ showModal: true });
      return;
    }
    if (this.from === 'miniapp') {
      Taro.eventCenter.trigger('cashier:paySuccess', {
        businessOrderId: orderRes.businessOrderId,
        channelOrderId: orderRes.channelOrderId,
        authOrderId: orderRes.authOrderId,
        creditOrderStatus: orderRes.creditOrderStatus,
      });
      jumpTo({ type: 'back' });
      return;
    }
  }
  // 查询微信支付分签约状态
  queryCreditPaySignStatus = async (): Promise<string> => {
    const signRes = await this.props.dispatchCreditPaySignQuery({
      token: this.token,
      payType: 13,
    });
    console.log('creditPaySignQuery result:', signRes);
    const signList = signRes?.signList || [];
    const signItem = signList.find((item: any) => String(item.payType) === '13');
    if (signItem) {
      return signItem.status || '';
    }
    // 未签约时 signList 仅一项且 payType 为空
    if (signList.length === 1 && (signList[0].payType === '' || signList[0].payType == null)) {
      return signList[0]?.status || '';
    }
    return '';
  }
  // 银联云闪付支付
@@ -720,7 +419,7 @@
        {ENV === 'weapp' && this.from !== 'app' && (
          <View className='pay-list'>
            {payTypeList.map(item => {
              //普通微信:1, 建行在微信:17,微信支付分13
              //普通微信:1, 建行在微信:17
              if (item.payType === 1 || item.payType === 11 || item.payType === 17) {
                return (
                  <View key={item.payType} className='pay-item' onClick={() => this.selectPayType(item.payType)}>
@@ -739,19 +438,6 @@
                  <View key={item.payType} className='pay-item' onClick={() => this.selectPayType(item.payType)}>
                    <Image className='icon' src={item.icon}></Image>
                    <View className='name'>{item.description}</View>
                    {payType === item.payType ? (
                      <Image className='ic-right' src={require('./assets/ic_select.png')}></Image>
                    ) : (
                      <Image className='ic-right' src={require('./assets/ic_se_default.png')}></Image>
                    )}
                  </View>
                );
              }
              if (item.payType === 13) {
                return (
                  <View key={item.payType} className='pay-item' onClick={() => this.selectPayType(item.payType)}>
                    <Image className='icon' src={require('./assets/ic_wxf.png')}></Image>
                    <View className='name'>微信支付分 | 先乘后付</View>
                    {payType === item.payType ? (
                      <Image className='ic-right' src={require('./assets/ic_select.png')}></Image>
                    ) : (
src/pages/robotaxi/home/components/privacy-popup/index.less
@@ -1,4 +1,4 @@
.privacy-popup {
.rt-privacy-popup {
  position: fixed;
  top: 0;
  left: 0;
@@ -9,7 +9,7 @@
  align-items: center;
  justify-content: center;
  .privacy-mask {
  .rt-privacy-mask {
    position: absolute;
    top: 0;
    left: 0;
@@ -18,14 +18,14 @@
    background: rgba(0, 0, 0, 0.5);
  }
  .privacy-content {
  .rt-privacy-content {
    position: relative;
    background: #ffffff;
    padding: 56px 40px 40px 40px;
    border-radius: 32px;
    margin: 0 75px;
    .title {
    .rt-title {
      margin-bottom: 34px;
      text-align: center;
      font-size: 36px;
@@ -35,7 +35,7 @@
      color: rgba(0, 0, 0, 0.9);
    }
    .privacy-text {
    .rt-privacy-text {
      font-size: 28px;
      font-weight: normal;
      line-height: 48px;
@@ -43,28 +43,28 @@
      color: rgba(0, 0, 0, 0.9);
      margin-bottom: 0;
      .txt-tip {
      .rt-txt-tip {
        margin-top: 24px;
      }
    }
    .privacy-link {
    .rt-privacy-link {
      margin-top: 16px;
      .link-item {
      .rt-link-item {
        display: flex;
        align-items: flex-start;
        margin-bottom: 8px;
      }
      .item-radio {
      .rt-item-radio {
        flex-shrink: 0;
        width: 36px;
        height: 36px;
        margin-right: 14px;
      }
      .item-name {
      .rt-item-name {
        flex: 1;
        font-size: 24px;
        font-weight: normal;
@@ -76,7 +76,7 @@
      }
    }
    .agree-btn {
    .rt-agree-btn {
      margin: 0 auto;
      margin-top: 30px;
      width: 472px;
@@ -90,11 +90,11 @@
      color: #FFFFFF;
    }
    .btn-no-agree {
    .rt-btn-no-agree {
      background: rgba(0, 0, 0, 0.1);
    }
    .close-txt {
    .rt-close-txt {
      text-align: center;
      margin-top: 40px;
      font-size: 28px;
src/pages/robotaxi/home/components/privacy-popup/index.tsx
@@ -135,40 +135,40 @@
  if (!visible) return null;
  return (
    <View className='privacy-popup' catchMove>
      <View className='privacy-mask'></View>
      <View className='privacy-content' animation={animationData}>
        <View className='title'>用户协议与隐私政策</View>
    <View className='rt-privacy-popup' catchMove>
      <View className='rt-privacy-mask'></View>
      <View className='rt-privacy-content' animation={animationData}>
        <View className='rt-title'>用户协议与隐私政策</View>
        <View className='privacy-text'>
          <Text className='text-content'>
        <View className='rt-privacy-text'>
          <Text className='rt-text-content'>
            欢迎使用小鹏Robotaxi小程序!为了更好地保障您的个人权益,我们将通过 {agreementNamesText || ''} 等文档来保护您的个人信息。
          </Text>
          <View className='txt-tip'>我已满十四周岁,阅读并同意:</View>
          <View className='rt-txt-tip'>我已满十四周岁,阅读并同意:</View>
        </View>
        <View className='privacy-link'>
        <View className='rt-privacy-link'>
          {
            agreementList.map((item) => (
              <View className='link-item' key={String(item.id)}>
                <Image onClick={checkItem(String(item.id))} className='item-radio' src={isAgree[String(item.id)] ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/check.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'}
              <View className='rt-link-item' key={String(item.id)}>
                <Image onClick={checkItem(String(item.id))} className='rt-item-radio' src={isAgree[String(item.id)] ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/check.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'}
                  mode='aspectFill' />
                <View className='item-name' onClick={() => handleLinkClick(item.id)}>《 {item.name} 》</View>
                <View className='rt-item-name' onClick={() => handleLinkClick(item.id)}>《 {item.name} 》</View>
              </View>
            ))
          }
          <View className='link-item'>
            <Image onClick={checkAll} className='item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/check.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'}
          <View className='rt-link-item'>
            <Image onClick={checkAll} className='rt-item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/check.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'}
              mode='aspectFill' />
            <View onClick={checkAll} className='item-name'>全部勾选</View>
            <View onClick={checkAll} className='rt-item-name'>全部勾选</View>
          </View>
        </View>
        <View className={'agree-btn ' + (!isAllAgree && 'btn-no-agree')} onClick={handleAgree}>
        <View className={`rt-agree-btn ${!isAllAgree ? 'rt-btn-no-agree' : ''}`} onClick={handleAgree}>
          同意并进入
        </View>
        <View className='close-txt' onClick={handleClose}>暂不同意,留在游客模式</View>
        <View className='rt-close-txt' onClick={handleClose}>暂不同意,留在游客模式</View>
      </View>
    </View>
  )
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
@@ -295,7 +295,6 @@
        },
        duration: ANIMATION_DURATION_MS,
        autoRotate: false,
        moveWithRotate: !isAndroidRef.current,
        rotate: resolveMarkerRotate(
          interpolateDirection(track.fromDirection, track.toDirection, 1),
          isAndroidRef.current
src/robotaxi/api/types/begin-route.ts
@@ -417,8 +417,6 @@
    duration?: NumericLike;
    /** 方向角。 */
    direction?: NumericLike;
    /** 行驶速度。 */
    speed?: NumericLike;
}
/** 订单轨迹响应 content。 */
@@ -447,8 +445,6 @@
    distance?: NumericLike;
    /** 路线时长。 */
    duration?: NumericLike;
    /** 行驶速度。 */
    speed?: NumericLike;
    /** 方向角。 */
    direction?: NumericLike;
    /** 行程状态。 */
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx
@@ -7,10 +7,14 @@
};
export default function BluetoothOperateCard({ description, actionText = '', onClick }: Props) {
    const iconSrc = actionText === '去开启'
        ? "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png"
        : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png";
    return (
        <View className="bluetooth-operate-card" onClick={onClick}>
            <View className="bluetooth-operate-card-left">
                <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png" />
                <Image src={iconSrc} />
                <View>{description}</View>
            </View>
            {!!actionText && (
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts
@@ -26,8 +26,6 @@
  duration: number | null;
  /** 车辆朝向(0-359) */
  direction: number | null;
  /** 当前速度(m/s)。 */
  speedMps: number | null;
  /** 消息时间戳(用于乱序保护) */
  timestamp: number | null;
  /** 已行驶轨迹点(servedPoints) */
@@ -38,6 +36,8 @@
  eraseVehiclePointMode: VehiclePointMode;
  /** 非擦线态车辆点策略 */
  normalVehiclePointMode: VehiclePointMode;
  /** 当前车速 (m/s) */
  speed: number | null;
};
/**
@@ -67,34 +67,22 @@
}
/**
 * 将后端下发的速度统一规整为 m/s。
 */
export function normalizeSpeedMps(rawSpeed: any) {
  const speedMetersPerSecond = Number(rawSpeed);
  if (!Number.isFinite(speedMetersPerSecond)) {
    return null;
  }
  return Math.max(0, speedMetersPerSecond);
}
/**
 * 归一化 socket 路线消息,统一路线点、概要和车辆移动点结构。
 */
export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot {
  const route = safeParse(payload?.content?.route, {});
  const timestamp = Number(payload?.data?.timestamp);
  const speedMps = normalizeSpeedMps(route?.speed);
  return {
    points: normalizeRoutePoints(payload?.points),
    distance: normalizeDistanceKm(Number(route?.distance)),
    duration: normalizeDurationSeconds(Number(route?.duration)),
    direction: normalizeDirection(payload?.content?.direction),
    speedMps,
    timestamp: Number.isFinite(timestamp) ? timestamp : null,
    vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints),
    forceFullRedraw: payload?.forceFullRedraw === true,
    eraseVehiclePointMode: 'first_then_served_last',
    normalVehiclePointMode: 'served_last_then_first',
    speed: Number.isFinite(Number(route?.speed)) ? Number(route?.speed) : null,
  };
}
src/robotaxi/pages/begin-route/hooks/trip-motion-utils.ts
@@ -269,3 +269,29 @@
  }
  return Math.max(searchStartDistance, bestProjectedDistance);
}
/**
 * 根据最大距离截取路线片段(用于车速分级自适应缩放视口)。
 * 从起点开始,累加线段距离,直到超过 maxDistanceMeters,截取这部分点返回。
 */
export function sliceRoutePointsByDistance(points: RoutePoint[], maxDistanceMeters: number): RoutePoint[] {
  if (!Array.isArray(points) || points.length <= 1) {
    return points;
  }
  const result: RoutePoint[] = [points[0]];
  let accumulatedDistance = 0;
  for (let index = 1; index < points.length; index += 1) {
    const prev = points[index - 1];
    const curr = points[index];
    const segmentDistance = getDistanceMeters(prev, curr);
    if (accumulatedDistance + segmentDistance > maxDistanceMeters) {
      const ratio = (maxDistanceMeters - accumulatedDistance) / segmentDistance;
      result.push(interpolatePoint(prev, curr, ratio));
      break;
    } else {
      accumulatedDistance += segmentDistance;
      result.push(curr);
    }
  }
  return result;
}
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts
@@ -1,7 +1,7 @@
import Taro from '@tarojs/taro';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useEffect, useRef } from 'react';
import { TripStatus } from '../constants/trip-status';
import useVehicleSpeedScale from './use-vehicle-speed-scale';
import { sliceRoutePointsByDistance } from './trip-motion-utils';
type RoutePoint = {
  latitude: number;
@@ -11,51 +11,91 @@
type Deps = {
  /** 当前行程状态。 */
  tripStatus: TripStatus;
  /** 当前速度,单位 m/s。 */
  speedMps: number | null;
  /** 当前地图比例尺。 */
  mapScale: number;
  /** 设置地图比例尺。 */
  setMapScale: (value: number) => void;
  /** 动态缩放生效后同步车辆视口中心。 */
  syncActiveTripViewportToVehicle: () => void;
  /** 车辆速度 (m/s) */
  vehicleSpeed: number | null;
  /** 当前路线点集 */
  routePoints: RoutePoint[];
  /** 获取速度缩放暂停截止时间 */
  getDynamicScaleHoldUntil: () => number;
  /** 计算 includePoints 所需底部留白。 */
  computeBottomPadding: (points: any[], viewportType: string) => number;
  /** 读取顶部安全区高度。 */
  resolveSafeTopInset: () => number;
  /** 暂停动态缩放。 */
  holdDynamicScale: (durationMs?: number) => void;
  /** 读取动态缩放暂停截止时间。 */
  getDynamicScaleHoldUntil: () => number;
};
const ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService]; // 启用速度缩放的行程状态
const USER_GESTURE_CAUSED_BY = new Set(['gesture', 'drag', 'scale']); // 需要暂停速度缩放的用户手势来源
const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停速度缩放时长
const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停速度缩放时长
const ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService]; // 启用视口编排的行程状态
const USER_GESTURE_CAUSED_BY = new Set(['gesture', 'drag', 'scale']); // 需要暂停动态缩放的用户手势来源
const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停动态缩放时长
const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停动态缩放时长
const ORDER_UPDATE_LINE_SIDE_PADDING = 100; // ORDER_UPDATE_LINE 路线入屏左右留白
const ORDER_UPDATE_LINE_TOP_PADDING = 24; // ORDER_UPDATE_LINE 路线入屏最小顶部留白
const ORDER_UPDATE_LINE_TOP_EXTRA_PADDING = 32; // ORDER_UPDATE_LINE 顶部安全区附加留白
const DYNAMIC_SCALE_INTERVAL_MS = 8000; // 动态缩放巡航周期
/**
 * 接驾中/行程中动态缩放入口:速度缩放单独交给专用 Hook,这里仅保留视口编排职责。
 * 接驾中/行程中视口编排入口:仅保留视口编排职责。
 */
export default function useActiveTripScale(deps: Deps) {
  const isActiveStatus = useMemo(() => {
    return ACTIVE_TRIP_STATUSES.includes(deps.tripStatus);
  }, [deps.tripStatus]);
  useVehicleSpeedScale({
    tripStatus: deps.tripStatus,
    speedMps: deps.speedMps,
    mapScale: deps.mapScale,
    setMapScale: deps.setMapScale,
    syncActiveTripViewportToVehicle: deps.syncActiveTripViewportToVehicle,
    getDynamicScaleHoldUntil: deps.getDynamicScaleHoldUntil
  });
  const depsRef = useRef(deps);
  useEffect(() => {
    depsRef.current = deps;
  }, [deps]);
  /**
   * 处理地图手势,手动缩放/拖拽后短暂暂停速度接管。
   * 基于车速区间进行路线截取,推导视口缩放。
   */
  const tryApplySpeedBasedScale = useCallback(() => {
    const currentDeps = depsRef.current;
    if (currentDeps.tripStatus !== TripStatus.InService) {
      return; // 仅在行程中开启速度级缩放
    }
    const now = Date.now();
    if (currentDeps.getDynamicScaleHoldUntil() > now) {
      return; // 处于手势或强行全览冷却期
    }
    const speed = currentDeps.vehicleSpeed || 0;
    const points = currentDeps.routePoints;
    if (!Array.isArray(points) || points.length < 2) {
      return;
    }
    let targetDistance = 5000; // 高速段 5公里
    if (speed < 8.3) { // 约 30km/h
      targetDistance = 300; // 低速段 300米
    } else if (speed < 16.6) { // 约 60km/h
      targetDistance = 1500; // 中速段 1.5公里
    }
    const slicedPoints = sliceRoutePointsByDistance(points, targetDistance);
    if (slicedPoints.length < 2) {
      return;
    }
    const mapCtx = Taro.createMapContext('map');
    const bottomPadding = currentDeps.computeBottomPadding(slicedPoints, 'route');
    const topPadding = Math.max(ORDER_UPDATE_LINE_TOP_PADDING, currentDeps.resolveSafeTopInset() + ORDER_UPDATE_LINE_TOP_EXTRA_PADDING);
    mapCtx.includePoints({
      points: slicedPoints,
      padding: [topPadding, ORDER_UPDATE_LINE_SIDE_PADDING, bottomPadding, ORDER_UPDATE_LINE_SIDE_PADDING]
    });
  }, []);
  useEffect(() => {
    if (!isActiveStatus) return;
    const timer = setInterval(() => {
      tryApplySpeedBasedScale();
    }, DYNAMIC_SCALE_INTERVAL_MS);
    return () => clearInterval(timer);
  }, [isActiveStatus, tryApplySpeedBasedScale]);
  /**
   * 处理地图手势,手动缩放/拖拽后短暂暂停视口同步。
   */
  const handleRegionChange = useCallback((event: any) => {
    const causedBy = String(event?.causedBy || event?.detail?.causedBy || '').toLowerCase();
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts
@@ -55,8 +55,6 @@
    distanceMeters: number | null;
    /** 预估行程时长,单位秒。 */
    durationSeconds: number | null;
    /** 预估速度,单位米/秒。 */
    speedMps: number | null;
  };
  /** 路线备选项。 */
  routeOptions?: any[];
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts
@@ -105,8 +105,6 @@
    distanceMeters: number | null;
    /** 时长,单位秒。 */
    durationSeconds: number | null;
    /** 速度,单位 m/s。 */
    speedMps: number | null;
  }) => void;
  /** 设置当前用户定位。 */
  setCurrentUserLocation?: (location: CurrentUserLocation) => void;
src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -121,7 +121,6 @@
  distance: normalizeDistanceKm(Number(response?.distance)),
  duration: normalizeDurationSeconds(Number(response?.duration)),
  direction,
  speedMps: normalizeSpeedMps(response?.speed),
  timestamp: Date.now(),
  vehicleMoveAlongPoints: [],
  forceFullRedraw: true,
@@ -480,8 +479,7 @@
      if (syncOrderRouteMetrics) {
        deps.setOrderRouteMetrics?.({
          distanceMeters: Number.isFinite(Number(route?.distance)) ? Number(route?.distance) : null,
          durationSeconds: Number.isFinite(Number(route?.duration)) ? Number(route?.duration) : null,
          speedMps: normalizeSpeedMps(route?.speed)
          durationSeconds: Number.isFinite(Number(route?.duration)) ? Number(route?.duration) : null
        });
      }
@@ -562,8 +560,7 @@
        deps.setOrderRouteMetrics?.({
          distanceMeters: Number.isFinite(Number(response?.distance)) ? Number(response?.distance) : null,
          durationSeconds: Number.isFinite(Number(response?.duration)) ? Number(response?.duration) : null,
          speedMps: normalizeSpeedMps(response?.speed)
          durationSeconds: Number.isFinite(Number(response?.duration)) ? Number(response?.duration) : null
        });
        const normalizedDirection = resolveDirectionValue(response);
src/robotaxi/pages/begin-route/hooks/use-vehicle-speed-scale.ts
File was deleted
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -58,7 +58,6 @@
          longitude: task.longitude,
        },
        autoRotate: false,
        moveWithRotate: !isAndroid,
        rotate: task.rotate,
        duration: TRANSLATE_DURATION_MS,
        fail: () => {
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -26,7 +26,6 @@
    orderRouteMetrics: {
        distanceMeters: number | null;
        durationSeconds: number | null;
        speedMps: number | null;
    };
    setOrderId: (orderId: string) => void;
    setTripStatus: (status: TripStatus) => void;
@@ -58,11 +57,11 @@
    setVehicleMoveAlongPoints: (points: any[]) => void;
    setRouteDistance: (value: any) => void;
    setRouteDuration: (value: number) => void;
    setVehicleSpeed?: (speed: number) => void;
    setModifyDestinationSocketStatus: (value: number | null) => void;
    setOrderRouteMetrics?: (metrics: {
        distanceMeters: number | null;
        durationSeconds: number | null;
        speedMps: number | null;
    }) => void;
    setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void;
    isModifyDestinationReviewVisible?: boolean;
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts
@@ -57,7 +57,6 @@
  const route = safeParse(payload?.content?.route, {});
  const distanceMeters = Number(route?.distance);
  const durationSecondsRaw = Number(route?.duration);
  const speedMps = normalizeSpeedMps(route?.speed);
  const distanceKm = normalizeDistanceKm(distanceMeters);
  if (distanceKm !== null) {
    deps.setRouteDistance(distanceKm);
@@ -68,8 +67,7 @@
  }
  deps.setOrderRouteMetrics?.({
    distanceMeters: Number.isFinite(distanceMeters) ? distanceMeters : null,
    durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null,
    speedMps
    durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null
  });
};
@@ -107,6 +105,9 @@
    return;
  }
  const routeSnapshot = normalizeSocketRouteSnapshot(payload);
  if (typeof routeSnapshot.speed === 'number') {
    currentDeps.setVehicleSpeed?.(routeSnapshot.speed);
  }
  const shouldOnlySyncModifyPreviewVehicle = isModifyDestinationPreviewActive && currentTripStatus === TripStatus.InService;
  const shouldFreezeModifyPreviewRoute = isModifyDestinationPreviewActive && currentTripStatus !== TripStatus.InService;
  if (isModifyDestinationPreviewActive) {
src/robotaxi/pages/begin-route/index.tsx
@@ -99,7 +99,6 @@
type OrderRouteMetrics = {
  distanceMeters: number | null;
  durationSeconds: number | null;
  speedMps: number | null;
};
/**
@@ -203,10 +202,10 @@
  const [showCouponSelectPopup, setShowCouponSelectPopup] = useState(false); // 优惠券选择弹窗展示状态
  const [showModifyDestinationFailPopup, setShowModifyDestinationFailPopup] = useState(false); // 修改目的地失败弹窗
  const [modifyDestinationFailMessage, setModifyDestinationFailMessage] = useState(''); // 修改目的地失败文案(默认空,避免闪文案)
  const [vehicleSpeed, setVehicleSpeed] = useState<number | null>(null); // 车辆当前速度 (m/s)
  const [orderRouteMetrics, setOrderRouteMetrics] = useState<OrderRouteMetrics>({
    distanceMeters: null,
    durationSeconds: null,
    speedMps: null
    durationSeconds: null
  });
  const [currentUserLocation, setCurrentUserLocation] = useState<CurrentUserLocation>(null); // 当前用户真实定位,避免被地图中心点覆盖
  const [modifyDestinationSocketStatus, setModifyDestinationSocketStatus] = useState<number | null>(null); // 后端推送的修改目的地展示状态
@@ -252,8 +251,7 @@
  useEffect(() => {
    setOrderRouteMetrics({
      distanceMeters: null,
      durationSeconds: null,
      speedMps: null
      durationSeconds: null
    });
    const cachedSocketStatus = Taro.getStorageSync(MODIFY_DESTINATION_SOCKET_STATUS_KEY);
    const cachedOrderId = String(cachedSocketStatus?.orderId || '');
@@ -624,15 +622,13 @@
  } = viewportState; // 视口状态解构
  const activeTripScale = useActiveTripScale({
    tripStatus,
    speedMps: orderRouteMetrics?.speedMps,
    mapScale,
    setMapScale: viewportState.setMapScale,
    syncActiveTripViewportToVehicle,
    vehicleSpeed,
    routePoints,
    getDynamicScaleHoldUntil,
    computeBottomPadding,
    resolveSafeTopInset,
    holdDynamicScale,
    getDynamicScaleHoldUntil
  }); // 行程中速度缩放与 ORDER_UPDATE_LINE 视口编排
    holdDynamicScale
  }); // 行程中 ORDER_UPDATE_LINE 视口编排
  const arrivedMarkerReady = tripStatus !== TripStatus.ArrivedDestination || typeof vehicleDirection === 'number';
  const arrivedTooltipReady = tripStatus !== TripStatus.ArrivedDestination || (arrivedMarkerReady && mapScale >= 20);
  const markerState = useMapMarkers({
@@ -929,6 +925,7 @@
    setVehicleMoveAlongPoints,
    setRouteDistance,
    setRouteDuration,
    setVehicleSpeed,
    setModifyDestinationSocketStatus,
    setOrderRouteMetrics,
    setBeforeUserEndSite,
src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts
@@ -62,7 +62,6 @@
                longitude: vehicleLocation.longitude
            },
            autoRotate: false,
            moveWithRotate: true,
            rotate:
                typeof vehicleDirection === "number"
                    ? ((vehicleDirection % 360) + 360) % 360