hejianhao
2026-04-27 091e7af951cdb5c0291f83825e93d4f7790f188b
feat(行程页): 集成优惠券选择与协议列表动态化

- 在发布路线卡片中增加优惠券选择入口,支持实时切换并仅刷新价格
- 新增修改目的地确认按钮防重复提交与加载状态蒙层
- 重构协议相关逻辑,统一从后端动态拉取登录协议列表
- 优化修改目的地会话管理,避免跨订单状态残留
- 新增登录协议工具函数,统一解析与标准化协议数据
- 更新乘客须知页面,支持动态协议类型的内容拉取
- 调整优惠券页面默认选中状态为“未使用”
2个文件已添加
15个文件已修改
766 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/privacy-popup/index.tsx 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/home.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/change-coupon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/coupon-select-popup/index.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/agreement-page/index.tsx 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/modify-destination-review.less 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-route.less 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/coupon/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/passenger-notice/index.tsx 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/login-agreement.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/privacy-popup/index.tsx
@@ -1,11 +1,12 @@
import Taro from '@tarojs/taro';
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import './index.less';
import check from '../../../../../robotaxi/assets/check.png'
import uncheck from '../../../../../robotaxi/assets/uncheck.png'
import { privicyShowManager } from './utils'
import { submitAgreement } from '../../../../../robotaxi/api/home'
import { getNormalizedLoginAgreementList, type LoginAgreementItem } from '../../../../../robotaxi/utils/login-agreement'
import { reportEvent } from '../../../../../utils/Robotaxi-public-method'
type Props = {
@@ -14,29 +15,45 @@
  onAgree?: () => void;
}
/**
 * 统一格式化协议名展示文案,避免协议名写死在页面中。
 */
const formatAgreementNamesText = (list: LoginAgreementItem[]) => {
  const names = list
    .map((item: LoginAgreementItem) => String(item?.name || '').trim())
    .filter((name: string) => !!name);
  if (names.length === 0) return '';
  return names.map((name: string) => `《${name}》`).join('、');
};
export default function PrivacyPopup({ show = false, onClose, onAgree }: Props) {
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const [isAgree, setIsAgree] = useState({
    1: false,
    2: false,
    3: false,
    4: false,
    7: false,
  });
  const [agreementList, setAgreementList] = useState<LoginAgreementItem[]>([]);
  const [isAgree, setIsAgree] = useState<Record<string, boolean>>({});
  const [isAllAgree, setIsAllAgree] = useState(false);
  const agreementNamesText = useMemo(() => formatAgreementNamesText(agreementList), [agreementList]); // 协议说明文案由协议列表动态生成
  const AGREE_NAME = {
    1: '小鹏Robotaxi隐私政策',
    2: '个人信息收集清单',
    3: '第三方共享信息清单',
    4: '小鹏Robotaxi录音录像及个人信息采集单独告知书',
    7: '权限说明',
  }
  // 拉取登录协议列表并初始化勾选状态
  const loadLoginAgreement = () => {
    getNormalizedLoginAgreementList({ includeRideNotice: false }).then((list: LoginAgreementItem[]) => {
      setAgreementList(list);
      const initState = list.reduce((acc: Record<string, boolean>, item: LoginAgreementItem) => {
        acc[String(item.id)] = false;
        return acc;
      }, {});
      setIsAgree(initState);
    }).catch(() => {
      Taro.showToast({ title: '协议获取失败,请重试', icon: 'none' });
    });
  };
  useEffect(() => {
    if (show && !visible) {
      setVisible(true);
      if (agreementList.length === 0) {
        loadLoginAgreement();
      }
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
@@ -58,7 +75,8 @@
  }, [show]);
  useEffect(() => {
    setIsAllAgree(Object.values(isAgree).every((item) => item));
    const keys = Object.keys(isAgree);
    setIsAllAgree(keys.length > 0 && keys.every((key) => isAgree[key]));
  }, [isAgree]);
  const handleClose = () => {
@@ -74,7 +92,13 @@
      Taro.showToast({ title: '请先同意全部协议', icon: 'none' });
      return
    };
    submitAgreement({ type: Object.keys(isAgree).map(Number) }).then((res: any) => {
    const selectedType = Object.keys(isAgree)
      .filter((key) => isAgree[key])
      .map((key) => {
        const idNumber = Number(key);
        return Number.isNaN(idNumber) ? key : idNumber;
      });
    submitAgreement({ type: selectedType }).then((res: any) => {
      reportEvent('RT_xcx_protocol_click', {
        status_code: 1
      })
@@ -85,28 +109,28 @@
    });
  };
  const handleLinkClick = (type: number) => {
  const handleLinkClick = (type: number | string) => {
    Taro.navigateTo({
      url: `/robotaxi/pages/passenger-notice/index?type=${type}`
    });
  };
  const checkItem = (key: keyof typeof isAgree) => () => {
    setIsAgree({
      ...isAgree,
      [key]: !isAgree[key],
    })
  const checkItem = (key: string) => () => {
    setIsAgree((prev) => ({
      ...prev,
      [key]: !prev[key],
    }));
  }
  const checkAll = () => {
    setIsAllAgree(!isAllAgree);
    setIsAgree({
      1: !isAllAgree,
      2: !isAllAgree,
      3: !isAllAgree,
      4: !isAllAgree,
      7: !isAllAgree,
    })
    const nextChecked = !isAllAgree;
    setIsAgree((prev) => {
      const nextState: Record<string, boolean> = {};
      Object.keys(prev).forEach((key) => {
        nextState[key] = nextChecked;
      });
      return nextState;
    });
  }
  if (!visible) return null;
@@ -119,18 +143,18 @@
        <View className='privacy-text'>
          <Text className='text-content'>
            欢迎使用小鹏Robotaxi App!为了更好地保障您的个人权益,我们将通过 《小鹏Robotaxi隐私政策》、《个人信息收集清单》、《第三方共享信息清单》、《小鹏Robotaxi录音录像及个人信息采集单独告知书》、《权限说明》等文档来保护您的个人信息。
            欢迎使用小鹏Robotaxi小程序!为了更好地保障您的个人权益,我们将通过 {agreementNamesText || ''} 等文档来保护您的个人信息。
          </Text>
          <View className='txt-tip'>我已满十四周岁,阅读并同意:</View>
        </View>
        <View className='privacy-link'>
          {
            Object.keys(isAgree).map((key) => (
              <View className='link-item' key={key}>
                <Image onClick={checkItem(key)} className='item-radio' src={isAgree[key] ? check : uncheck}
            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)] ? check : uncheck}
                  mode='aspectFill' />
                <View className='item-name' onClick={() => handleLinkClick(Number(key))}>《 {AGREE_NAME[key]} 》</View>
                <View className='item-name' onClick={() => handleLinkClick(item.id)}>《 {item.name} 》</View>
              </View>
            ))
          }
src/robotaxi/api/home.ts
@@ -69,6 +69,22 @@
    }, true);
}
// 获取登录协议列表
export const getLoginAgreement = async () => {
    return request({
        url: _ROBOTAXI_HOST + '/operation/base/v1/mini-program/loginAgreement/getLoginAgreement',
        method: 'GET'
    }, true);
}
// 获取登录协议详情
export const getLoginAgreementContentInfo = async (id: string | number) => {
    return request({
        url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/loginAgreement/getAppContentInfo/${id}`,
        method: 'GET'
    }, true);
}
// 获取订单状态
export const getOrderActive = async () => {
    return request({
src/robotaxi/assets/change-coupon.png
src/robotaxi/components/coupon-select-popup/index.tsx
@@ -12,6 +12,7 @@
type Props = {
  show: boolean;
  orderMoney?: number;
  initialSelectedCouponId?: string;
  onClose: () => void;
  onSelect: (item: any) => void;
};
@@ -38,6 +39,7 @@
export default function FavoritePopup({
  show,
  orderMoney = 0,
  initialSelectedCouponId = "",
  onClose,
  onSelect
}: Props) {
@@ -54,6 +56,13 @@
  const [expandUnusable, setExpandUnusable] = useState(false);
  const [expandedCouponIds, setExpandedCouponIds] = useState<string[]>([]);
  useEffect(() => {
    if (!show) {
      return;
    }
    setActiveCouponId(initialSelectedCouponId || "");
  }, [show, initialSelectedCouponId]);
  const getList = async () => {
    try {
      const res = await getUserCouponList(orderMoney);
src/robotaxi/pages/agreement-page/index.tsx
@@ -1,11 +1,21 @@
import Taro from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { View, Text, Image } from '@tarojs/components';
import { View, Image } from '@tarojs/components';
import './index.less';
import { getNormalizedLoginAgreementList, type LoginAgreementItem } from '../../utils/login-agreement';
export default function AgreementPage() {
  const [agreementList, setAgreementList] = useState<LoginAgreementItem[]>([]);
  const skipToNotice = (type: number) => {
  useEffect(() => {
    getNormalizedLoginAgreementList({ includeRideNotice: true, rideNoticeInsertBeforeId: 7 }).then((list: LoginAgreementItem[]) => {
      setAgreementList(list);
    }).catch(() => {
      setAgreementList([]);
    });
  }, []);
  const skipToNotice = (type: number | string) => {
    Taro.navigateTo({
      url: '/robotaxi/pages/passenger-notice/index?type=' + type
    })
@@ -13,30 +23,17 @@
  return (
    <View className='agreement-page'>
      <View onClick={() => skipToNotice(1)} className='agreement-item' hoverClass='common-hover-class'>
        <View className='agreement-item-title'>小鹏Robotaxi隐私政策</View>
        <Image className='agreement-item-icon' src={require('../../assets/skip-arrows.png')} />
      </View>
      <View onClick={() => skipToNotice(2)} className='agreement-item' hoverClass='common-hover-class'>
        <View className='agreement-item-title'>个人信息收集清单</View>
        <Image className='agreement-item-icon' src={require('../../assets/skip-arrows.png')} />
      </View>
      <View onClick={() => skipToNotice(3)} className='agreement-item' hoverClass='common-hover-class'>
        <View className='agreement-item-title'>第三方共享信息清单</View>
        <Image className='agreement-item-icon' src={require('../../assets/skip-arrows.png')} />
      </View>
      <View onClick={() => skipToNotice(4)} className='agreement-item' hoverClass='common-hover-class'>
        <View className='agreement-item-title'>小鹏Robotaxi录音录像及个人信息采集单独告知书</View>
        <Image className='agreement-item-icon' src={require('../../assets/skip-arrows.png')} />
      </View>
      <View onClick={() => skipToNotice(5)} className='agreement-item' hoverClass='common-hover-class'>
        <View className='agreement-item-title'>乘车须知</View>
        <Image className='agreement-item-icon' src={require('../../assets/skip-arrows.png')} />
      </View>
      <View onClick={() => skipToNotice(7)} className='agreement-item' hoverClass='common-hover-class'>
        <View className='agreement-item-title'>权限说明</View>
        <Image className='agreement-item-icon' src={require('../../assets/skip-arrows.png')} />
      </View>
      {agreementList.map((item: LoginAgreementItem) => (
        <View
          key={String(item.id)}
          onClick={() => skipToNotice(item.id)}
          className='agreement-item'
          hoverClass='common-hover-class'
        >
          <View className='agreement-item-title'>{item.name}</View>
          <Image className='agreement-item-icon' src={require('../../assets/skip-arrows.png')} />
        </View>
      ))}
    </View>
  )
}
}
src/robotaxi/pages/begin-route/components/css-folder/modify-destination-review.less
@@ -162,4 +162,22 @@
    line-height: 40px;
    color: #ffffff;
  }
  &-confirm-wrapper {
    position: relative;
    flex-shrink: 0;
    width: 296px;
    height: 94px;
  }
  &-confirm-mask {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 20px;
    background: rgba(255, 255, 255, 0.45);
    z-index: 2;
  }
}
src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -345,15 +345,35 @@
                    gap: 8px;
                    .discount-amount {
                        border-radius: 8px;
                        box-sizing: border-box;
                        border: 1.4px solid #F58900;
                        line-height: 37px;
                        text-align: center;
                        padding: 0px 12px;
                        font-size: 20px;
                        font-weight: 500;
                        color: #F58900;
                        display: flex;
                        align-items: center;
                        .discount-amount-text {
                            border-radius: 6px;
                            background: rgba(245, 137, 0, 0.06);
                            box-sizing: border-box;
                            border: 1px solid rgba(245, 137, 0, 0.2);
                            line-height: 30px;
                            text-align: center;
                            padding: 0px 6px;
                            font-size: 16px;
                            font-weight: 500;
                            color: #F58900;
                        }
                        .discount-amount-num {
                            margin-left: 8px;
                            font-size: 20px;
                            font-weight: 500;
                            line-height: 20px;
                            letter-spacing: normal;
                            color: #F58900;
                        }
                        .change-coupon-icon {
                            width: 24px;
                            height: 24px;
                        }
                    }
                    .original-fare {
@@ -398,4 +418,4 @@
            background: rgba(255, 255, 255, 0.4);
        }
    }
}
}
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx
@@ -16,6 +16,7 @@
  handleSelectRoute?: (index: number) => void;
  onEditDestination?: () => void;
  onConfirm: () => void;
  isConfirmingModifyDestination?: boolean;
};
export default function ModifyDestinationReview({
@@ -29,7 +30,8 @@
  onToggleRouteSelect,
  handleSelectRoute,
  onEditDestination,
  onConfirm
  onConfirm,
  isConfirmingModifyDestination = false
}: Props) {
  // 高度统一使用 rpx,'auto' 表示由内容撑开
  const [boxHeightRpx, setBoxHeightRpx] = useState<number | 'auto'>('auto');
@@ -167,8 +169,17 @@
                </View>
              )}
            </View>
            <View className='modify-destination-review-confirm' onClick={onConfirm}>
              确认修改
            <View className='modify-destination-review-confirm-wrapper'>
              <View
                className='modify-destination-review-confirm'
                onClick={() => {
                  if (isConfirmingModifyDestination) return;
                  onConfirm();
                }}
              >
                {isConfirmingModifyDestination ? '确认中...' : '确认修改'}
              </View>
              {isConfirmingModifyDestination && <View className='modify-destination-review-confirm-mask' />}
            </View>
          </View>
        </View>
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -4,6 +4,7 @@
import rightIcon from '../../../assets/right-icon.png'
import './css-folder/release-route.less'
import tipBlueIcon from '../../../assets/tip-blue-icon.png'
import changeCouponIcon from '../../../assets/change-coupon.png'
import tipDefaultIcon from '../../../assets/tip-default-icon.png'
import RouteSelectionPanel from './route-selection-panel';
@@ -12,6 +13,7 @@
    endSite: any;
    callImmediately: () => void;
    fareEstimate?: any;
    onOpenCouponSelect?: () => void;
    handleOpenPassengerNotice?: () => void;
    walkingTime?: number;
    isCallDisabled?: boolean;
@@ -22,7 +24,7 @@
    handleSelectRoute?: (index: number) => void;
}
export default function ReleaseRoute({ startSite, endSite, callImmediately, fareEstimate = {}, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false, isRouteSelectExpanded = false, onToggleRouteSelect, routeOptions = [], selectedRouteIndex = 0, handleSelectRoute }: Props) {
export default function ReleaseRoute({ startSite, endSite, callImmediately, fareEstimate = {}, onOpenCouponSelect, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false, isRouteSelectExpanded = false, onToggleRouteSelect, routeOptions = [], selectedRouteIndex = 0, handleSelectRoute }: Props) {
    const [boxHeight, setBoxHeight] = useState<number | null>(null);
    const startY = useRef(0);
    const startHeight = useRef(0);
@@ -238,7 +240,11 @@
                        <View className="route-content-bottom-right">
                            {
                                fareEstimate.discountAmount > 0 &&
                                <View className="discount-amount">已优惠{fareEstimate.discountAmount}元</View>
                                <View className="discount-amount" onClick={onOpenCouponSelect}>
                                    <View className="discount-amount-text">最佳优惠</View>
                                    <View className="discount-amount-num">{fareEstimate.discountAmount}元</View>
                                    <Image className="change-coupon-icon" src={changeCouponIcon} />
                                </View>
                            }
                            {
                                fareEstimate.originalFareStr > 0 &&
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
@@ -40,6 +40,7 @@
  Taro.removeStorageSync(MODIFY_DESTINATION_SESSION_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
};
export default function useModifyDestinationFlow(deps: Deps) {
@@ -47,6 +48,7 @@
  const [previousEndSite, setPreviousEndSite] = useState<any>(null);
  const [previousFareEstimate, setPreviousFareEstimate] = useState<any>(null);
  const [previousRoutePoints, setPreviousRoutePoints] = useState<any[]>([]);
  const [isConfirmingModifyDestination, setIsConfirmingModifyDestination] = useState(false); // 确认修改请求中状态,控制按钮蒙层与防重复提交
  const [overrideEndSite, setOverrideEndSite] = useState<any>(() => {
    const cached = Taro.getStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    return isValidSite(cached) ? cached : null;
@@ -79,15 +81,28 @@
    const candidate = Taro.getStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    const cachedOverride = Taro.getStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    const cachedRoutePoints = Taro.getStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
    const isSessionOrderMatched = !session?.orderId || String(session.orderId) === String(deps.orderId); // 会话订单校验,避免旧订单会话串入当前页面
    if (isValidSite(cachedOverride)) {
      setOverrideEndSite(cachedOverride);
    }
    if (session?.active && !isSessionOrderMatched) {
      clearModifyDestinationSession();
      setPreviewEndSite(null);
      setPreviousEndSite(null);
      setPreviousFareEstimate(null);
      setPreviousRoutePoints([]);
      return;
    }
    if (session?.active && isValidSite(candidate)) {
      if (isSameSite(session.previousEndSite, candidate)) {
        clearModifyDestinationSession();
        setPreviewEndSite(null);
        setPreviousEndSite(null);
        setPreviousFareEstimate(null);
        setPreviousRoutePoints([]);
        return;
      }
      setPreviousEndSite(session.previousEndSite || deps.endSite || null);
@@ -99,7 +114,18 @@
    if (session?.active && !isValidSite(candidate)) {
      clearModifyDestinationSession();
      setPreviewEndSite(null);
      setPreviousEndSite(null);
      setPreviousFareEstimate(null);
      setPreviousRoutePoints([]);
      return;
    }
    // 会话已结束时,强制退出“修改目的地预览态”。
    setPreviewEndSite(null);
    setPreviousEndSite(null);
    setPreviousFareEstimate(null);
    setPreviousRoutePoints([]);
  }, [
    deps.tripStatus,
    deps.orderId,
@@ -142,23 +168,19 @@
  const handleConfirmModifyDestination = async (selectedRoute?: any) => {
    if (!isValidSite(previewEndSite)) return;
    if (isConfirmingModifyDestination) return;
    try {
      Taro.showLoading({ title: '正在修改...', mask: true });
      let targetOrderId: string | number = deps.orderId || '';
      const activeOrderRes: any = await getOrderActive();
      targetOrderId = activeOrderRes?.id ? activeOrderRes.id : targetOrderId;
      setIsConfirmingModifyDestination(true);
      // 如果后端需要字符串格式可以显式转String,如果后端支持number直接传值,保留原始格式防止隐式转换截断
      // 如果原本因为 Number() 导致大整数丢失精度,去掉 Number() 即可
      await orderChange({
        endPointId: previewEndSite.id || 0,
        orderId: targetOrderId,
        orderId: Taro.getStorageSync('orderId') || '',
        selectedRouteId: selectedRoute?.route?.routeID || selectedRoute?.routeId || '',
        selectedRoutePlanRule: selectedRoute?.amapStrategy || ''
      });
      Taro.hideLoading();
      // 清除本地修改状态,不再作为“本地覆盖”存在,因为订单已真正变更
      // 同步更新缓存中新的终点信息
@@ -170,17 +192,17 @@
        longitude: previewEndSite.longitude,
        id: previewEndSite.id
      };
      Taro.setStorageSync('endSite', newEndSite);
      deps.setEndSite(newEndSite);
      setPreviewEndSite(null);
      setPreviousEndSite(null);
      setPreviousFareEstimate(null);
      setPreviousRoutePoints([]);
      clearModifyDestinationSession();
      clearLocalDestinationOverride();
      Taro.showToast({
        title: '目的地已更新',
        icon: 'none'
@@ -190,19 +212,24 @@
        deps.onModifySuccess();
      }
    } catch (e: any) {
      Taro.hideLoading();
      Taro.showToast({
        title: e?.message || '修改失败,请重试',
        title: e?.msg || e?.message || '修改失败,请重试',
        icon: 'none'
      });
    } finally {
      setIsConfirmingModifyDestination(false);
    }
  };
  const isModifyDestinationReviewVisible = isValidSite(previewEndSite);
  const isModifyDestinationSessionActive = !!Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY)?.active; // 以缓存会话为准控制预览态展示,规避状态残留
  const isModifyDestinationReviewVisible = isValidSite(previewEndSite) && isModifyDestinationSessionActive;
  const hasLocalDestinationOverride = isValidSite(overrideEndSite);
  const effectiveEndSite = useMemo(() => (
    previewEndSite || overrideEndSite || deps.endSite
  ), [previewEndSite, overrideEndSite, deps.endSite]);
  const effectiveEndSite = useMemo(() => {
    if (isModifyDestinationReviewVisible) {
      return previewEndSite;
    }
    return overrideEndSite || deps.endSite;
  }, [isModifyDestinationReviewVisible, previewEndSite, overrideEndSite, deps.endSite]);
  return {
    effectiveEndSite,
@@ -210,6 +237,7 @@
    previousEndSite,
    previousFareEstimate,
    previousRoutePoints,
    isConfirmingModifyDestination,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    clearLocalDestinationOverride,
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
@@ -1,6 +1,12 @@
import Taro from '@tarojs/taro';
import { clearOrderTripStorage } from '../../../../utils/Robotaxi-public-method';
import { TripStatus } from '../constants/trip-status';
import {
  MODIFY_DESTINATION_CANDIDATE_KEY,
  MODIFY_DESTINATION_OVERRIDE_KEY,
  MODIFY_DESTINATION_ROUTE_POINTS_KEY,
  MODIFY_DESTINATION_SESSION_KEY
} from '../constants/modify-destination';
type Deps = {
  tripStatus: TripStatus;
@@ -13,6 +19,16 @@
 * 页面动作编排:统一处理服务入口、页面跳转、地图气泡点击行为。
 */
export default function usePageActions(deps: Deps) {
  /**
   * 清理修改目的地流程相关缓存,避免返回后残留会话影响后续流程。
   */
  const clearModifyDestinationStorage = () => {
    Taro.removeStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    Taro.removeStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
  };
  /**
   * 拨打客服电话。
   */
@@ -53,6 +69,7 @@
   * 返回首页并清理必要缓存。
   */
  const toBack = () => {
    clearModifyDestinationStorage();
    if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) {
      clearOrderTripStorage();
      Taro.removeStorageSync('endSite');
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
@@ -24,6 +24,12 @@
  setVehicleDirection: (value: number) => void;
};
type RequestRouteAndFareOptions = {
  force?: boolean;
  refreshMode?: 'full' | 'price_only';
  couponId?: string;
};
const defaultDeps: Deps = {
  isAndroid: false,
  tripStatus: TripStatus.PublishRoute,
@@ -362,7 +368,18 @@
  };
  // 请求预估路线与费用
  const requestRouteAndFare = async (originLat: number, originLng: number, destLat: number, destLng: number, force = false) => {
  const requestRouteAndFare = async (
    originLat: number,
    originLng: number,
    destLat: number,
    destLng: number,
    optionsOrForce: boolean | RequestRouteAndFareOptions = false
  ) => {
    const resolvedOptions: RequestRouteAndFareOptions = typeof optionsOrForce === 'boolean'
      ? { force: optionsOrForce, refreshMode: 'full' }
      : { force: false, refreshMode: 'full', ...(optionsOrForce || {}) };
    const force = !!resolvedOptions.force;
    const refreshMode = resolvedOptions.refreshMode || 'full';
    const now = Date.now();
    if (now - lastRouteAndFareRequestAtRef.current < 1000) {
      return;
@@ -376,7 +393,7 @@
    setRouteAndFareLoading(true);
    const selectedCity = await ensureSelectedCityForRoute(originLng, originLat);
    const openCityId = selectedCity ? selectedCity.id : 0;
    const cachedCouponId = Taro.getStorageSync('selectedCouponId');
    const couponId = resolvedOptions.couponId || Taro.getStorageSync('selectedCouponId');
    const params: any = {
      destination: {
        latitude: destLat,
@@ -388,8 +405,8 @@
        longitude: originLng
      },
    };
    if (cachedCouponId) {
      params.userCouponId = cachedCouponId;
    if (couponId) {
      params.userCouponId = couponId;
    }
    queryRouteAndFare(params).then((res: any) => {
@@ -412,30 +429,54 @@
      let allPointsArr: any[][] = [];
      if (responseRoutes.length > 0) {
        options = responseRoutes.map(normalizeRouteOption);
        setRouteOptions(options);
        routeData = options[0];
        setSelectedRouteIndex(0);
        allPointsArr = options.map(item => Array.isArray(item?.route?.polyline) ? item.route.polyline : []);
        setAllRoutePoints(allPointsArr);
      } else if (Array.isArray(res) && res.length > 0) {
        options = res.map(normalizeRouteOption);
        setRouteOptions(options);
        routeData = options[0];
        setSelectedRouteIndex(0);
        // 解析所有路线的点集
        allPointsArr = options.map(item => Array.isArray(item?.route?.polyline) ? item.route.polyline : []);
        setAllRoutePoints(allPointsArr);
      } else if (res) {
        options = [normalizeRouteOption(res)];
        setRouteOptions(options);
        routeData = options[0];
        setSelectedRouteIndex(0);
        const pts = Array.isArray(routeData?.route?.polyline) ? routeData.route.polyline : [];
        allPointsArr = [pts];
        setAllRoutePoints(allPointsArr);
      } else {
        setRouteOptions([]);
        setAllRoutePoints([]);
        options = [];
      }
      if (refreshMode === 'price_only') {
        const currentRouteId = routeOptions[selectedRouteIndex]?.route?.routeID;
        routeData = options.find((item: any) => item?.route?.routeID === currentRouteId) || options[0] || null;
        if (options.length > 0) {
          setRouteOptions(prev => {
            if (!Array.isArray(prev) || prev.length === 0) {
              return options;
            }
            const optionMap = new Map(
              options.map((item: any) => [item?.route?.routeID, item])
            );
            return prev.map((oldItem: any, index: number) => {
              const matchedByRouteId = optionMap.get(oldItem?.route?.routeID);
              const matchedOption = matchedByRouteId || options[index];
              if (!matchedOption) {
                return oldItem;
              }
              return {
                ...oldItem,
                ...matchedOption,
                route: {
                  ...(oldItem?.route || {}),
                  ...(matchedOption?.route || {}),
                  polyline: oldItem?.route?.polyline || matchedOption?.route?.polyline || []
                }
              };
            });
          });
        }
      } else {
        routeData = options[0] || null;
        if (options.length > 0) {
          setRouteOptions(options);
          setSelectedRouteIndex(0);
          allPointsArr = options.map(item => Array.isArray(item?.route?.polyline) ? item.route.polyline : []);
          setAllRoutePoints(allPointsArr);
        } else {
          setRouteOptions([]);
          setAllRoutePoints([]);
        }
      }
      if (routeData?.fareEstimate) {
@@ -453,28 +494,30 @@
      if (durationSeconds !== null) {
        setRouteDuration(durationSeconds);
      }
      const points = Array.isArray(route?.polyline) ? route.polyline : [];
      if (Array.isArray(points) && points.length > 0) {
        applyRoutePoints(points);
        if (deps.allowSelectableRoutes) {
          const allRoutesPoints: any[] = [];
          allPointsArr.forEach(pts => {
            if (Array.isArray(pts)) {
              allRoutesPoints.push(...pts);
            }
          });
          const allPoints = [...allRoutesPoints, ...walkingPoints];
          if (allPoints.length > 0) {
            const topPadding = Math.max(24, deps.resolveSafeTopInset() + 32);
            const bottomPadding = deps.computeBottomPadding(allPoints, 'route_walking');
            Taro.createMapContext('map').includePoints({
              points: allPoints,
              padding: [topPadding, 100, bottomPadding, 100]
      if (refreshMode === 'full') {
        const points = Array.isArray(route?.polyline) ? route.polyline : [];
        if (Array.isArray(points) && points.length > 0) {
          applyRoutePoints(points);
          if (deps.allowSelectableRoutes) {
            const allRoutesPoints: any[] = [];
            allPointsArr.forEach(pts => {
              if (Array.isArray(pts)) {
                allRoutesPoints.push(...pts);
              }
            });
            const allPoints = [...allRoutesPoints, ...walkingPoints];
            if (allPoints.length > 0) {
              const topPadding = Math.max(24, deps.resolveSafeTopInset() + 32);
              const bottomPadding = deps.computeBottomPadding(allPoints, 'route_walking');
              Taro.createMapContext('map').includePoints({
                points: allPoints,
                padding: [topPadding, 100, bottomPadding, 100]
              });
            }
          }
        } else {
          applyRoutePoints([]);
        }
      } else {
        applyRoutePoints([]);
      }
    }).catch(() => {
      setRouteAndFareError(true);
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -52,6 +52,8 @@
  callImmediately: () => void;
  /** 费用信息 */
  fareEstimate: any;
  /** 打开优惠券选择弹窗 */
  handleOpenCouponSelect: () => void;
  /** 打开乘客须知 */
  handleOpenPassengerNotice: () => void;
  /** 步行时长 */
@@ -108,6 +110,8 @@
  handleMapTap?: (e: any) => void;
  /** 修改目的地确认态 */
  isModifyDestinationReviewVisible?: boolean;
  /** 修改目的地确认请求中 */
  isConfirmingModifyDestination?: boolean;
  /** 修改前目的地 */
  previousEndSite?: any;
  /** 修改前费用 */
@@ -272,6 +276,7 @@
          endSite={deps.endSite}
          callImmediately={deps.callImmediately}
          fareEstimate={deps.fareEstimate}
          onOpenCouponSelect={deps.handleOpenCouponSelect}
          handleOpenPassengerNotice={deps.handleOpenPassengerNotice}
          walkingTime={deps.walkingTime}
          isCallDisabled={deps.isCallDisabled}
@@ -290,6 +295,7 @@
          nextEndSite={deps.previewEndSite}
          previousFareEstimate={deps.previousFareEstimate}
          fareEstimate={deps.fareEstimate}
          isConfirmingModifyDestination={deps.isConfirmingModifyDestination}
          routeOptions={deps.routeOptions}
          selectedRouteIndex={deps.selectedRouteIndex}
          isRouteSelectExpanded={deps.isRouteSelectExpanded}
src/robotaxi/pages/begin-route/index.tsx
@@ -4,6 +4,7 @@
import './index.less';
import BeginRouteView from './components/begin-route-view';
import CouponSelectPopup from '../../components/coupon-select-popup';
// 弹窗状态管理
import usePopupManager from './hooks/use-popup-manager';
// WebSocket 事件编排
@@ -127,6 +128,7 @@
  const [priceUpdateVersion, setPriceUpdateVersion] = useState(0); // 价格更新版本号
  const [evaluateSubmitVersion, setEvaluateSubmitVersion] = useState(0); // 评价提交版本号
  const [evaluateCloseVersion, setEvaluateCloseVersion] = useState(0); // 评价关闭版本号
  const [showCouponSelectPopup, setShowCouponSelectPopup] = useState(false); // 优惠券选择弹窗展示状态
  const tripStatusRef = useRef(tripStatus); // 行程状态引用
  const lastTripStatusRef = useRef(tripStatus); // 上一次行程状态,用于检测状态变化
@@ -183,6 +185,7 @@
    previousEndSite,
    previousFareEstimate,
    previousRoutePoints,
    isConfirmingModifyDestination,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    clearLocalDestinationOverride,
@@ -703,6 +706,44 @@
  const handleShareTrip = () => {
    isCheckedEnter.current = true;
  };
  // 打开优惠券选择弹窗
  const handleOpenCouponSelect = () => {
    if (tripStatus !== TripStatus.PublishRoute) {
      return;
    }
    setShowCouponSelectPopup(true);
  };
  // 关闭优惠券选择弹窗
  const handleCloseCouponSelect = () => {
    setShowCouponSelectPopup(false);
  };
  // 选择优惠券后仅刷新价格,不刷新地图路线
  const handleSelectCoupon = (item: any) => {
    const selectedCouponId = String(item?.operationCouponId || item?.couponId || '');
    if (!selectedCouponId) {
      setShowCouponSelectPopup(false);
      return;
    }
    Taro.setStorageSync('selectedCouponId', selectedCouponId);
    setShowCouponSelectPopup(false);
    const cachedStartSite = Taro.getStorageSync('startSite') || {};
    const originLat = Number(startSite?.latitude ?? cachedStartSite?.latitude);
    const originLng = Number(startSite?.longitude ?? cachedStartSite?.longitude);
    const destLat = Number(effectiveEndSite?.latitude);
    const destLng = Number(effectiveEndSite?.longitude);
    if (!Number.isFinite(originLat) || !Number.isFinite(originLng) || !Number.isFinite(destLat) || !Number.isFinite(destLng)) {
      return;
    }
    routeRequestRouteAndFare(originLat, originLng, destLat, destLng, {
      force: true,
      refreshMode: 'price_only',
      couponId: selectedCouponId
    });
  };
  const activeCouponId = String( // 优惠券弹窗默认选中券ID
    fareEstimate?.couponId || fareEstimate?.userCouponId || Taro.getStorageSync('selectedCouponId') || ''
  );
  const couponOrderMoney = Number(fareEstimate?.originalFare || fareEstimate?.finalFare || 0); // 优惠券可用性计算订单金额
  const { renderMapOverlays, renderTripCard } = useViewDelegates({
    isAndroid,
    tripStatus,
@@ -728,6 +769,7 @@
    endSite: effectiveEndSite,
    callImmediately,
    fareEstimate,
    handleOpenCouponSelect,
    handleOpenPassengerNotice,
    walkingTime,
    isCallDisabled: routeAndFareLoading || routeAndFareError || callSubmitting,
@@ -754,6 +796,7 @@
    handleSelectRoute,
    handleMapTap,
    isModifyDestinationReviewVisible,
    isConfirmingModifyDestination,
    previousEndSite,
    previousFareEstimate,
    previewEndSite,
@@ -801,68 +844,77 @@
  };
  return (
    <BeginRouteView
      mapLongitude={mapLongitude}
      mapLatitude={mapLatitude}
      mapScale={mapScale}
      markers={markersWithRouteSummary}
      polyline={displayPolyline}
      showLocation={!!longitude && showLocationFlag}
      handleCalloutTap={handleCalloutTap}
      handleRegionChange={handleRegionChange}
      onPolylineTap={handlePolylineTap}
      onMapTap={handleMapTap}
      renderMapOverlays={renderMapOverlays}
      toBack={toBack}
      backIcon={backIcon}
      onServiceEntry={handleServiceEntry}
      onCouponEntry={handleCouponEntry}
      onPersonalCenterEntry={handlePersonalCenterEntry}
      personalCenter={personalCenter}
      coupon={coupon}
      contactService={contactService}
      shouldShowOrigin={shouldShowOrigin}
      originBoxHidden={originBoxHidden}
      originOpacity={originOpacity}
      toOrigin={toOrigin}
      handleWalkNavigation={handleWalkNavigation}
      selectRouteIcon={selectRouteIcon}
      packUpIcon={packUpIcon}
      onSelectRouteClick={() => setIsRouteSelectExpanded(!isRouteSelectExpanded)}
      isRouteSelectExpanded={isRouteSelectExpanded}
      canToggleRouteSelect={canToggleRouteSelect}
      walkIcon={walkIcon}
      originIcon={originIcon}
      renderTripCard={renderTripCard}
      showCustomerService={showCustomerService}
      handleCloseCustomerService={handleCloseCustomerService}
      showCancelPopup={showCancelPopup}
      cancelType={cancelType}
      cancelPopupWaitMinutes={cancelPopupWaitMinutes}
      startSite={startSite}
      endSite={effectiveEndSite}
      fareEstimate={fareEstimate}
      tripStatus={tripStatus}
      routeDuration={routeDuration}
      handleCloseCancelPopup={handleCloseCancelPopup}
      handleConfirmCancel={handleConfirmCancel}
      showAnnouncementsPopup={showAnnouncementsPopup}
      handleCloseAnnouncementsPopup={handleCloseAnnouncementsPopup}
      showOverRoutePopup={showOverRoutePopup}
      handleCloseOverRoutePopup={handleCloseOverRoutePopup}
      handleConfirmOverRoute={handleConfirmOverRoute}
      showTipPopup={showTipPopup}
      handleCloseTipPopup={handleCloseTipPopup}
      tipType={tipType}
      showPrivacyPopup={showPrivacyPopup}
      handleClosePrivacyPopup={handleClosePrivacyPopup}
      showPassengerNotice={showPassengerNotice}
      handleClosePassengerNotice={handleClosePassengerNotice}
      changeCheckedEnter={changeCheckedEnter}
      showEvaluatePopup={showEvaluatePopup}
      orderId={orderId}
      handleCloseEvaluatePopup={handleCloseEvaluatePopup}
      handleConfirmEvaluate={handleConfirmEvaluate}
    />
    <>
      <BeginRouteView
        mapLongitude={mapLongitude}
        mapLatitude={mapLatitude}
        mapScale={mapScale}
        markers={markersWithRouteSummary}
        polyline={displayPolyline}
        showLocation={!!longitude && showLocationFlag}
        handleCalloutTap={handleCalloutTap}
        handleRegionChange={handleRegionChange}
        onPolylineTap={handlePolylineTap}
        onMapTap={handleMapTap}
        renderMapOverlays={renderMapOverlays}
        toBack={toBack}
        backIcon={backIcon}
        onServiceEntry={handleServiceEntry}
        onCouponEntry={handleCouponEntry}
        onPersonalCenterEntry={handlePersonalCenterEntry}
        personalCenter={personalCenter}
        coupon={coupon}
        contactService={contactService}
        shouldShowOrigin={shouldShowOrigin}
        originBoxHidden={originBoxHidden}
        originOpacity={originOpacity}
        toOrigin={toOrigin}
        handleWalkNavigation={handleWalkNavigation}
        selectRouteIcon={selectRouteIcon}
        packUpIcon={packUpIcon}
        onSelectRouteClick={() => setIsRouteSelectExpanded(!isRouteSelectExpanded)}
        isRouteSelectExpanded={isRouteSelectExpanded}
        canToggleRouteSelect={canToggleRouteSelect}
        walkIcon={walkIcon}
        originIcon={originIcon}
        renderTripCard={renderTripCard}
        showCustomerService={showCustomerService}
        handleCloseCustomerService={handleCloseCustomerService}
        showCancelPopup={showCancelPopup}
        cancelType={cancelType}
        cancelPopupWaitMinutes={cancelPopupWaitMinutes}
        startSite={startSite}
        endSite={effectiveEndSite}
        fareEstimate={fareEstimate}
        tripStatus={tripStatus}
        routeDuration={routeDuration}
        handleCloseCancelPopup={handleCloseCancelPopup}
        handleConfirmCancel={handleConfirmCancel}
        showAnnouncementsPopup={showAnnouncementsPopup}
        handleCloseAnnouncementsPopup={handleCloseAnnouncementsPopup}
        showOverRoutePopup={showOverRoutePopup}
        handleCloseOverRoutePopup={handleCloseOverRoutePopup}
        handleConfirmOverRoute={handleConfirmOverRoute}
        showTipPopup={showTipPopup}
        handleCloseTipPopup={handleCloseTipPopup}
        tipType={tipType}
        showPrivacyPopup={showPrivacyPopup}
        handleClosePrivacyPopup={handleClosePrivacyPopup}
        showPassengerNotice={showPassengerNotice}
        handleClosePassengerNotice={handleClosePassengerNotice}
        changeCheckedEnter={changeCheckedEnter}
        showEvaluatePopup={showEvaluatePopup}
        orderId={orderId}
        handleCloseEvaluatePopup={handleCloseEvaluatePopup}
        handleConfirmEvaluate={handleConfirmEvaluate}
      />
      <CouponSelectPopup
        show={showCouponSelectPopup}
        orderMoney={couponOrderMoney}
        initialSelectedCouponId={activeCouponId}
        onClose={handleCloseCouponSelect}
        onSelect={handleSelectCoupon}
      />
    </>
  )
}
src/robotaxi/pages/coupon/index.tsx
@@ -15,12 +15,12 @@
export default function CouponPage() {
  const [couponStatus] = useState([
    // { label: '全部', value: '' },
    { label: "可领取", value: 0 },
    // { label: "可领取", value: 0 },
    { label: "未使用", value: 1 },
    { label: "已使用", value: 2 },
    { label: "已失效", value: 3 }
  ]);
  const [activeStatus, setActiveStatus] = useState<string | number>(0);
  const [activeStatus, setActiveStatus] = useState<string | number>(1);
  const [couponList, setCouponList] = useState<CouponItem[]>([]);
  const [animationData, setAnimationData] = useState<any>(null);
  const [visible, setVisible] = useState(false);
src/robotaxi/pages/passenger-notice/index.tsx
@@ -2,31 +2,55 @@
import { View, RichText } from "@tarojs/components";
import { useEffect, useState } from 'react';
import './index.less'
import { getPrivacyContent } from '../../api/home';
import { getLoginAgreementContentInfo, getPrivacyContent } from '../../api/home';
type AgreementItem = {
  id: number | string;
  name: string;
  content?: string;
  version?: string | number;
};
export default function PassengerNotice() {
  const [content, setContent] = useState('');
  const [title, setTitle] = useState('乘车须知');
  const RIDE_NOTICE_TYPE = 5; // 乘车须知协议类型
  const normalizeContent = (rawContent: string) => {
    return String(rawContent || '').replace(
      /<img/gi,
      '<img style="max-width:100%;height:auto;margin:0 auto;display:block"'
    );
  };
  const resolveAgreementItem = (payload: any): AgreementItem | null => {
    if (payload && typeof payload === 'object' && payload.id !== undefined) return payload;
    if (payload?.data && typeof payload.data === 'object') return payload.data;
    if (payload?.result && typeof payload.result === 'object') return payload.result;
    return null;
  };
  useEffect(() => {
    const typeParam = Taro.getCurrentInstance()?.router?.params?.type;
    const type = Number(typeParam);
    const titleMap: Record<number, string> = {
      1: '小鹏Robotaxi隐私政策',
      2: '个人信息收集清单',
      3: '第三方共享信息清单',
      4: '小鹏Robotaxi录音录像及个人信息采集单独告知书',
      5: '乘车须知',
      7: '权限说明',
    };
    setTitle(titleMap[type] || '乘车须知');
    getPrivacyContent(type).then((res: any) => {
      if (res.content) {
        res.content = res.content.replace(/<img/gi,
          '<img style="max-width:100%;height:auto;margin:0 auto;display:block"')
    const type = Number(typeParam || 0);
    if (type === RIDE_NOTICE_TYPE) {
      setTitle('乘车须知');
      getPrivacyContent(type).then((res: any) => {
        setContent(normalizeContent(res?.content || ''));
      }).catch(() => { });
      return;
    }
    getLoginAgreementContentInfo(typeParam || '').then((res: any) => {
      const target = resolveAgreementItem(res);
      if (target) {
        setTitle(String(target.name || '乘车须知'));
        setContent(normalizeContent(target.content || ''));
        return;
      }
      setContent(res?.content);
    }).catch(() => { });
      Taro.showToast({ title: '协议内容获取失败', icon: 'none' });
    }).catch(() => {
      Taro.showToast({ title: '协议内容获取失败', icon: 'none' });
    });
  }, []);
  return (
src/robotaxi/utils/login-agreement.ts
New file
@@ -0,0 +1,57 @@
import { getLoginAgreement } from '../api/home';
export type LoginAgreementItem = {
  id: number | string;
  name: string;
  content?: string;
  version?: string | number;
};
export type LoginAgreementListOptions = {
  includeRideNotice?: boolean;
  rideNoticeInsertBeforeId?: number;
};
export const RIDE_NOTICE_ID = 5; // 乘车须知协议类型
export const RIDE_NOTICE_ITEM: LoginAgreementItem = { id: RIDE_NOTICE_ID, name: '乘车须知' }; // 乘车须知兜底协议
// 统一解析登录协议列表接口返回结构
export const resolveLoginAgreementList = (payload: any): LoginAgreementItem[] => {
  if (Array.isArray(payload)) return payload;
  if (Array.isArray(payload?.data)) return payload.data;
  if (Array.isArray(payload?.result)) return payload.result;
  return [];
};
// 统一标准化协议列表项结构
export const normalizeLoginAgreementList = (list: LoginAgreementItem[]): LoginAgreementItem[] => {
  return list
    .map((item: LoginAgreementItem) => ({
      id: item?.id,
      name: String(item?.name || ''),
      content: item?.content,
      version: item?.version,
    }))
    .filter((item: LoginAgreementItem) => item.id !== undefined && item.id !== null && item.name);
};
// 统一拉取协议列表,并按配置补充“乘车须知”
export const getNormalizedLoginAgreementList = async (options: LoginAgreementListOptions = {}) => {
  const includeRideNotice = options.includeRideNotice ?? false;
  const rideNoticeInsertBeforeId = options.rideNoticeInsertBeforeId ?? 7;
  const res = await getLoginAgreement();
  const normalized = normalizeLoginAgreementList(resolveLoginAgreementList(res))
    .filter((item: LoginAgreementItem) => Number(item.id) !== RIDE_NOTICE_ID);
  if (!includeRideNotice) {
    return normalized;
  }
  const insertIndex = normalized.findIndex((item: LoginAgreementItem) => Number(item.id) === rideNoticeInsertBeforeId);
  if (insertIndex >= 0) {
    return [
      ...normalized.slice(0, insertIndex),
      RIDE_NOTICE_ITEM,
      ...normalized.slice(insertIndex),
    ];
  }
  return [...normalized, RIDE_NOTICE_ITEM];
};