feat(行程页): 集成优惠券选择与协议列表动态化
- 在发布路线卡片中增加优惠券选择入口,支持实时切换并仅刷新价格
- 新增修改目的地确认按钮防重复提交与加载状态蒙层
- 重构协议相关逻辑,统一从后端动态拉取登录协议列表
- 优化修改目的地会话管理,避免跨订单状态残留
- 新增登录协议工具函数,统一解析与标准化协议数据
- 更新乘客须知页面,支持动态协议类型的内容拉取
- 调整优惠券页面默认选中状态为“未使用”
| | |
| | | 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 = { |
| | |
| | | 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' |
| | |
| | | }, [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 = () => { |
| | |
| | | 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 |
| | | }) |
| | |
| | | }); |
| | | }; |
| | | |
| | | 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; |
| | |
| | | |
| | | <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> |
| | | )) |
| | | } |
| | |
| | | }, 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({ |
| | |
| | | type Props = { |
| | | show: boolean; |
| | | orderMoney?: number; |
| | | initialSelectedCouponId?: string; |
| | | onClose: () => void; |
| | | onSelect: (item: any) => void; |
| | | }; |
| | |
| | | export default function FavoritePopup({ |
| | | show, |
| | | orderMoney = 0, |
| | | initialSelectedCouponId = "", |
| | | onClose, |
| | | onSelect |
| | | }: Props) { |
| | |
| | | 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); |
| | |
| | | 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 |
| | | }) |
| | |
| | | |
| | | 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> |
| | | ) |
| | | } |
| | | } |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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 { |
| | |
| | | background: rgba(255, 255, 255, 0.4); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | handleSelectRoute?: (index: number) => void; |
| | | onEditDestination?: () => void; |
| | | onConfirm: () => void; |
| | | isConfirmingModifyDestination?: boolean; |
| | | }; |
| | | |
| | | export default function ModifyDestinationReview({ |
| | |
| | | onToggleRouteSelect, |
| | | handleSelectRoute, |
| | | onEditDestination, |
| | | onConfirm |
| | | onConfirm, |
| | | isConfirmingModifyDestination = false |
| | | }: Props) { |
| | | // 高度统一使用 rpx,'auto' 表示由内容撑开 |
| | | const [boxHeightRpx, setBoxHeightRpx] = useState<number | 'auto'>('auto'); |
| | |
| | | </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> |
| | |
| | | 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'; |
| | | |
| | |
| | | endSite: any; |
| | | callImmediately: () => void; |
| | | fareEstimate?: any; |
| | | onOpenCouponSelect?: () => void; |
| | | handleOpenPassengerNotice?: () => void; |
| | | walkingTime?: number; |
| | | isCallDisabled?: boolean; |
| | |
| | | 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); |
| | |
| | | <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 && |
| | |
| | | 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) { |
| | |
| | | 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; |
| | |
| | | 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); |
| | |
| | | |
| | | 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, |
| | |
| | | |
| | | 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(); |
| | | |
| | | // 清除本地修改状态,不再作为“本地覆盖”存在,因为订单已真正变更 |
| | | // 同步更新缓存中新的终点信息 |
| | |
| | | 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' |
| | |
| | | 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, |
| | |
| | | previousEndSite, |
| | | previousFareEstimate, |
| | | previousRoutePoints, |
| | | isConfirmingModifyDestination, |
| | | isModifyDestinationReviewVisible, |
| | | hasLocalDestinationOverride, |
| | | clearLocalDestinationOverride, |
| | |
| | | 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; |
| | |
| | | * 页面动作编排:统一处理服务入口、页面跳转、地图气泡点击行为。 |
| | | */ |
| | | 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); |
| | | }; |
| | | |
| | | /** |
| | | * 拨打客服电话。 |
| | | */ |
| | |
| | | * 返回首页并清理必要缓存。 |
| | | */ |
| | | const toBack = () => { |
| | | clearModifyDestinationStorage(); |
| | | if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) { |
| | | clearOrderTripStorage(); |
| | | Taro.removeStorageSync('endSite'); |
| | |
| | | setVehicleDirection: (value: number) => void; |
| | | }; |
| | | |
| | | type RequestRouteAndFareOptions = { |
| | | force?: boolean; |
| | | refreshMode?: 'full' | 'price_only'; |
| | | couponId?: string; |
| | | }; |
| | | |
| | | const defaultDeps: Deps = { |
| | | isAndroid: false, |
| | | tripStatus: TripStatus.PublishRoute, |
| | |
| | | }; |
| | | |
| | | // 请求预估路线与费用 |
| | | 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; |
| | |
| | | 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, |
| | |
| | | longitude: originLng |
| | | }, |
| | | }; |
| | | if (cachedCouponId) { |
| | | params.userCouponId = cachedCouponId; |
| | | if (couponId) { |
| | | params.userCouponId = couponId; |
| | | } |
| | | |
| | | queryRouteAndFare(params).then((res: any) => { |
| | |
| | | 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) { |
| | |
| | | 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); |
| | |
| | | callImmediately: () => void; |
| | | /** 费用信息 */ |
| | | fareEstimate: any; |
| | | /** 打开优惠券选择弹窗 */ |
| | | handleOpenCouponSelect: () => void; |
| | | /** 打开乘客须知 */ |
| | | handleOpenPassengerNotice: () => void; |
| | | /** 步行时长 */ |
| | |
| | | handleMapTap?: (e: any) => void; |
| | | /** 修改目的地确认态 */ |
| | | isModifyDestinationReviewVisible?: boolean; |
| | | /** 修改目的地确认请求中 */ |
| | | isConfirmingModifyDestination?: boolean; |
| | | /** 修改前目的地 */ |
| | | previousEndSite?: any; |
| | | /** 修改前费用 */ |
| | |
| | | endSite={deps.endSite} |
| | | callImmediately={deps.callImmediately} |
| | | fareEstimate={deps.fareEstimate} |
| | | onOpenCouponSelect={deps.handleOpenCouponSelect} |
| | | handleOpenPassengerNotice={deps.handleOpenPassengerNotice} |
| | | walkingTime={deps.walkingTime} |
| | | isCallDisabled={deps.isCallDisabled} |
| | |
| | | nextEndSite={deps.previewEndSite} |
| | | previousFareEstimate={deps.previousFareEstimate} |
| | | fareEstimate={deps.fareEstimate} |
| | | isConfirmingModifyDestination={deps.isConfirmingModifyDestination} |
| | | routeOptions={deps.routeOptions} |
| | | selectedRouteIndex={deps.selectedRouteIndex} |
| | | isRouteSelectExpanded={deps.isRouteSelectExpanded} |
| | |
| | | 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 事件编排 |
| | |
| | | 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); // 上一次行程状态,用于检测状态变化 |
| | |
| | | previousEndSite, |
| | | previousFareEstimate, |
| | | previousRoutePoints, |
| | | isConfirmingModifyDestination, |
| | | isModifyDestinationReviewVisible, |
| | | hasLocalDestinationOverride, |
| | | clearLocalDestinationOverride, |
| | |
| | | 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, |
| | |
| | | endSite: effectiveEndSite, |
| | | callImmediately, |
| | | fareEstimate, |
| | | handleOpenCouponSelect, |
| | | handleOpenPassengerNotice, |
| | | walkingTime, |
| | | isCallDisabled: routeAndFareLoading || routeAndFareError || callSubmitting, |
| | |
| | | handleSelectRoute, |
| | | handleMapTap, |
| | | isModifyDestinationReviewVisible, |
| | | isConfirmingModifyDestination, |
| | | previousEndSite, |
| | | previousFareEstimate, |
| | | previewEndSite, |
| | |
| | | }; |
| | | |
| | | 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} |
| | | /> |
| | | </> |
| | | ) |
| | | } |
| | |
| | | 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); |
| | |
| | | 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 ( |
| New file |
| | |
| | | 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]; |
| | | }; |