Merge branch 'feature_pr_fix' into 'develop'
fix: 修复行程结束态地图跳动与搜索页兜底逻辑
See merge request robotaxi1/xp-robotaxi-miniprogram!13
| | |
| | | } |
| | | |
| | | export default function AnnouncementsPopup({ show = false, onClose }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [doorImgError, setDoorImgError] = useState(false); |
| | | const [seatbeltImgError, setSeatbeltImgError] = useState(false); |
| | | const [visible, setVisible] = useState(false); // 弹窗显示状态 |
| | | const [animationData, setAnimationData] = useState<any>(); // 弹窗开合动画 |
| | | const [doorImgError, setDoorImgError] = useState(false); // 开门动画图加载失败状态 |
| | | const [seatbeltImgError, setSeatbeltImgError] = useState(false); // 系安全带动画图加载失败状态 |
| | | |
| | | const doorImage = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/door-anim.webp' |
| | | const seatbeltImage = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/seatbelt-anim.webp' |
| | | |
| | | // 控制弹窗开合动画并重置图片兜底状态 |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | |
| | | handleConfirmEvaluate |
| | | } = props; |
| | | |
| | | // 统一渲染地图与业务弹窗容器 |
| | | return ( |
| | | <View className='home-container'> |
| | | <Map |
| | |
| | | handleOpenOverRoutePopup, |
| | | handleOpenAnnouncementsPopup |
| | | }: Props) { |
| | | const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); |
| | | const [lockStatus, setLockStatus] = useState(false); |
| | | const trunkPollTimerRef = useRef<any>(null); |
| | | const hasShownAnnouncementsRef = useRef(false); |
| | | const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态 |
| | | const [lockStatus, setLockStatus] = useState(false); // 车门解锁状态 |
| | | const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器 |
| | | const hasShownAnnouncementsRef = useRef(false); // 安全须知是否已展示 |
| | | |
| | | // 外部后备箱状态变化时同步本地状态 |
| | | useEffect(() => { |
| | | setTrunkStatus(orderTrunkStatus); |
| | | }, [orderTrunkStatus]); |
| | | |
| | | // 组件卸载时清理轮询定时器 |
| | | useEffect(() => { |
| | | return () => { |
| | | if (trunkPollTimerRef.current) { |
| | |
| | | }; |
| | | }, []); |
| | | |
| | | // 获取解锁请求所需定位参数,失败时回退到缓存上车点 |
| | | const getUnlockLocationPayload = async () => { |
| | | try { |
| | | Taro.showLoading({ title: '正在获取位置...', mask: true }); |
| | |
| | | return {}; |
| | | }; |
| | | |
| | | // 解锁车门/后备箱,后备箱场景会轮询确认结果 |
| | | const handleUnlock = async (doorType: 'door' | 'trunk', onSuccess?: () => void) => { |
| | | const targetOrderId = orderId || Taro.getStorageSync('orderId'); |
| | | if (!targetOrderId) return; |
| | | |
| | | const locationPayload = await getUnlockLocationPayload(); |
| | | const isCloseAction = doorType === 'trunk' ? !!trunkStatus : false; |
| | | const actionLoadingText = doorType === 'trunk' |
| | | ? (isCloseAction ? '正在关闭后备箱' : '正在开启后备箱') |
| | | : '正在解锁车门'; |
| | | Taro.showLoading({ title: actionLoadingText, mask: true }); |
| | | unlockVehicle({ |
| | | orderId: targetOrderId, |
| | | doorType, |
| | |
| | | }).then((res: any) => { |
| | | console.log('解锁结果:', res); |
| | | |
| | | if (!res) { |
| | | Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' }); |
| | | if (res?.code !== 200 || !res?.data) { |
| | | Taro.hideLoading(); |
| | | Taro.showToast({ title: res?.msg || '解锁失败,请重试或联系客服', icon: 'none' }); |
| | | return; |
| | | } |
| | | |
| | | if (doorType === 'trunk') { |
| | | const isCloseAction = !!trunkStatus; |
| | | const loadingText = isCloseAction ? '正在关闭后备箱' : '正在开启后备箱'; |
| | | Taro.showLoading({ title: loadingText, mask: true }); |
| | | |
| | | let pollCount = 0; |
| | | const maxPolls = 5; |
| | | const pollInterval = 2000; |
| | |
| | | } |
| | | |
| | | if (doorType === 'door') { |
| | | Taro.hideLoading(); |
| | | Taro.showToast({ title: '右侧车门已解锁', icon: 'none' }); |
| | | if (!hasShownAnnouncementsRef.current) { |
| | | handleOpenAnnouncementsPopup?.(); |
| | |
| | | } |
| | | } |
| | | }).catch(() => { |
| | | Taro.hideLoading(); |
| | | Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' }); |
| | | }); |
| | | }; |
| | |
| | | evaluateSubmitVersion, |
| | | evaluateCloseVersion |
| | | }: Props) { |
| | | const [activeIndex, setActiveIndex] = useState(0); |
| | | const [isEvaluated, setIsEvaluated] = useState(false); |
| | | const skipEvaluateResetRef = useRef(false); |
| | | const [activeIndex, setActiveIndex] = useState(0); // 当前选中的满意度 |
| | | const [isEvaluated, setIsEvaluated] = useState(false); // 是否已完成评价 |
| | | const skipEvaluateResetRef = useRef(false); // 跳过关闭重置的一次性标记 |
| | | |
| | | // 外部提交成功后同步评价状态 |
| | | useEffect(() => { |
| | | if (typeof evaluateSubmitVersion === 'number' && evaluateSubmitVersion > 0) { |
| | | skipEvaluateResetRef.current = true; |
| | |
| | | } |
| | | }, [evaluateSubmitVersion]); |
| | | |
| | | // 关闭评价弹窗时重置高亮状态 |
| | | useEffect(() => { |
| | | if (typeof evaluateCloseVersion === 'number' && evaluateCloseVersion > 0) { |
| | | if (skipEvaluateResetRef.current) { |
| | |
| | | } |
| | | }, [evaluateCloseVersion]); |
| | | |
| | | // 点击评价项:不满意走弹窗,其余直接提交 |
| | | const changeEvaluate = (index: number) => { |
| | | if (isEvaluated) return; |
| | | setActiveIndex(index); |
| | |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |
| | | } |
| | |
| | | onToggleDetailFade, |
| | | handlePay |
| | | }: Props) { |
| | | const [showDetail, setShowDetail] = useState(false); |
| | | const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态 |
| | | |
| | | // 外部重置版本变化时收起明细 |
| | | useEffect(() => { |
| | | if (typeof resetDetailVersion === 'number') { |
| | | setShowDetail(false); |
| | | } |
| | | }, [resetDetailVersion]); |
| | | |
| | | // 跳转费用说明页 |
| | | const toMoneyDetail = () => { |
| | | Taro.navigateTo({ |
| | | url: `/robotaxi/pages/expense-description/index?orderId=${orderId}` |
| | | }); |
| | | }; |
| | | |
| | | // 切换明细面板并同步地图淡化状态 |
| | | const handleToggleDetail = () => { |
| | | const next = !showDetail; |
| | | setShowDetail(next); |
| | |
| | | } |
| | | </View> |
| | | ); |
| | | } |
| | | } |
| | |
| | | }; |
| | | |
| | | export default function TemperatureControl({ tripStatus, orderId }: Props) { |
| | | const [tempStatus, setTempStatus] = useState<number | null>(); |
| | | const tempTimerRef = useRef<any>(null); |
| | | const lastConfirmedTempRef = useRef<number>(); |
| | | const [tempStatus, setTempStatus] = useState<number | null>(); // 当前显示温度 |
| | | const tempTimerRef = useRef<any>(null); // 温度设置防抖定时器 |
| | | const lastConfirmedTempRef = useRef<number>(); // 最近一次设置成功温度 |
| | | |
| | | const TEMP_MIN = 22; |
| | | const TEMP_MAX = 28; |
| | | |
| | | // 状态 4 时拉取车辆当前温度 |
| | | useEffect(() => { |
| | | if (tripStatus === 4) { |
| | | const targetOrderId = orderId || Taro.getStorageSync('orderId'); |
| | |
| | | } |
| | | }, [tripStatus, orderId]); |
| | | |
| | | // 初始化确认温度并在卸载时清理定时器 |
| | | useEffect(() => { |
| | | lastConfirmedTempRef.current = tempStatus as number; |
| | | return () => { |
| | |
| | | }; |
| | | }, []); |
| | | |
| | | // 防抖提交目标温度到后端 |
| | | const scheduleTemperatureUpdate = (targetTemperature: number) => { |
| | | if (tempTimerRef.current) { |
| | | clearTimeout(tempTimerRef.current); |
| | |
| | | }, 5000); |
| | | }; |
| | | |
| | | // 处理温度增减并做边界校验 |
| | | const handleTempChange = (delta: number) => { |
| | | setTempStatus((prev) => { |
| | | if (typeof prev !== 'number') { |
| | |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |
| | | } |
| | |
| | | limitMinutes, |
| | | handleOpenCancelPopup |
| | | }: Props) { |
| | | // 标题行文案映射 |
| | | const titleMap = { |
| | | 3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>, |
| | | 4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | |
| | | 9: '支付成功' |
| | | } |
| | | |
| | | // 说明文案映射 |
| | | const descMap = { |
| | | 3: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' }, |
| | | 4: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: isArriveSoon ? 'calling-car-content-desc-red' : 'calling-car-content-warning' } : null, |
| | |
| | | } |
| | | </View> |
| | | ); |
| | | } |
| | | } |
| | |
| | | const pickupTimerRef = useRef<any>(null); |
| | | const finishTimerRef = useRef<any>(null); |
| | | const countdownFinishedRef = useRef(false); |
| | | const pickupTripStatusRef = useRef(tripStatus); |
| | | const pickupEndTimeRef = useRef<number | null>(null); |
| | | const finishEndTimeRef = useRef<number | null>(null); |
| | | const lastFareOrderIdRef = useRef<string | null>(null); |
| | |
| | | }, [tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | pickupTripStatusRef.current = tripStatus; |
| | | }, [tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== 5) { |
| | | clearPickupTimer(); |
| | | return; |
| | | } |
| | | |
| | | const runPickupTick = () => { |
| | | // 防止状态已切换后,旧定时回调仍触发取消逻辑 |
| | | if (pickupTripStatusRef.current !== 5) { |
| | | clearPickupTimer(); |
| | | return; |
| | | } |
| | | const remaining = getRemainingSeconds(pickupEndTimeRef.current); |
| | | if (remaining !== null && remaining <= 0) { |
| | | if (!countdownFinishedRef.current) { |
| | |
| | | }; |
| | | |
| | | export default function VehicleInfoCard({ vehicleInfo }: Props) { |
| | | // 展示车辆基础信息与车辆图片 |
| | | return ( |
| | | <View className="car-card-detail"> |
| | | <View> |
| | |
| | | } |
| | | </View> |
| | | ); |
| | | } |
| | | } |
| | |
| | | orderTrunkStatus, |
| | | }: Props) { |
| | | |
| | | // 汇总行程中的倒计时与费用数据 |
| | | const { |
| | | waitMinutes, |
| | | limitMinutes, |
| | |
| | | /** 上车倒计时即将结束(<60秒) */ |
| | | const isPickupCountdownSoon = tripStatus === 5 && (typeof pickupCountdownSeconds === 'number' && pickupCountdownSeconds < 60); |
| | | |
| | | // 按状态组合渲染行程卡片模块 |
| | | return ( |
| | | <View className='calling-car'> |
| | | <Image className="calling-car-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' /> |
| | |
| | | } |
| | | |
| | | export default function CancelPopup({ show = false, cancelType = 1, startSite, endSite, waitMinutes, tripStatus, routeDurationSeconds, discountAmount, onClose, onConfirm }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [visible, setVisible] = useState(false); // 弹窗显示状态 |
| | | const [animationData, setAnimationData] = useState<any>(); // 弹窗开合动画 |
| | | const cachedStartSite = startSite || Taro.getStorageSync('startSite') || {}; |
| | | const cachedEndSite = endSite || Taro.getStorageSync('endSite') || {}; |
| | | const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-'; |
| | |
| | | const durationDisplay = formatDurationDisplay(routeDurationSeconds); |
| | | const discountAmountValue = typeof discountAmount === 'number' && !Number.isNaN(discountAmount) ? discountAmount : 0; |
| | | |
| | | // 不同取消阶段对应的文案配置 |
| | | const STATUS_MAP = { |
| | | 1: { |
| | | title: `预计${waitMinutesValue}分钟内有车,确定取消吗?`, |
| | |
| | | }, |
| | | } |
| | | |
| | | // 控制弹窗开合动画 |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | |
| | | } |
| | | }, [show]); |
| | | |
| | | // 关闭弹窗时按类型执行对应流程 |
| | | const closePopup = () => { |
| | | if (cancelType == 5) { |
| | | onConfirm?.(); |
| | |
| | | } |
| | | |
| | | export default function EvaluatePopup({ show = false, onClose, onConfirm, orderId, orderRating, submitMode = 'comment' }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [selectedTypeId, setSelectedTypeId] = useState<number | null>(null); |
| | | const [feedbackContent, setFeedbackContent] = useState(''); |
| | | const [typeOptions, setTypeOptions] = useState<any[]>([]); |
| | | const [isSubmitting, setIsSubmitting] = useState(false); |
| | | const [visible, setVisible] = useState(false); // 弹窗显示状态 |
| | | const [animationData, setAnimationData] = useState<any>(); // 弹窗进出场动画 |
| | | const [selectedTypeId, setSelectedTypeId] = useState<number | null>(null); // 当前选择的问题类型 |
| | | const [feedbackContent, setFeedbackContent] = useState(''); // 反馈文本内容 |
| | | const [typeOptions, setTypeOptions] = useState<any[]>([]); // 问题类型选项 |
| | | const [isSubmitting, setIsSubmitting] = useState(false); // 提交中状态 |
| | | const [pictures, setPictures] = useState<Array<{ |
| | | localUrl: string; |
| | | server?: { id: string; name: string; url: string } | null; |
| | | status: 'uploading' | 'success' | 'failed'; |
| | | }>>([]); |
| | | }>>([]); // 图片上传队列 |
| | | |
| | | // 控制弹窗开合动画 |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | |
| | | } |
| | | }, [show]); |
| | | |
| | | // 提交反馈(评价/投诉) |
| | | const submit = () => { |
| | | if (isSubmitting) { |
| | | return; |
| | |
| | | }); |
| | | } |
| | | |
| | | // 压缩图片到可上传大小 |
| | | const ensureSizeUnderLimit = async (path: string): Promise<string> => { |
| | | try { |
| | | const fs = Taro.getFileSystemManager(); |
| | |
| | | } |
| | | }; |
| | | |
| | | // 上传单张图片并写回状态 |
| | | const uploadSingle = async (index: number, filePath: string) => { |
| | | setPictures(prev => { |
| | | const next = [...prev]; |
| | |
| | | } |
| | | }; |
| | | |
| | | // 选择图片并触发上传 |
| | | const handleChooseImages = async () => { |
| | | const remain = Math.max(0, 3 - pictures.length); |
| | | if (remain <= 0) { |
| | |
| | | } |
| | | }; |
| | | |
| | | // 重试上传失败图片 |
| | | const handleRetry = async (index: number) => { |
| | | const item = pictures[index]; |
| | | if (!item) return; |
| | | uploadSingle(index, item.localUrl); |
| | | }; |
| | | |
| | | // 删除已选图片 |
| | | const handleDelete = (index: number) => { |
| | | setPictures(prev => { |
| | | const next = [...prev]; |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 拉取问题类型并重置本次填写内容 |
| | | useEffect(() => { |
| | | if (!show) { |
| | | return; |
| | |
| | | } |
| | | |
| | | export default function OverRoute({ show = false, onClose, onConfirm }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [finishMinutes, setFinishMinutes] = useState<number | null>(null); |
| | | const [isSubmitting, setIsSubmitting] = useState(false); |
| | | const [visible, setVisible] = useState(false); // 弹窗显示状态 |
| | | const [animationData, setAnimationData] = useState<any>(); // 弹窗动画数据 |
| | | const [finishMinutes, setFinishMinutes] = useState<number | null>(null); // 自动驶离倒计时(分钟) |
| | | const [isSubmitting, setIsSubmitting] = useState(false); // 结束行程提交状态 |
| | | |
| | | // 确认结束行程并调用结束接口 |
| | | const handleConfirm = () => { |
| | | if (isSubmitting) { |
| | | return; |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 控制弹窗关闭动画 |
| | | useEffect(() => { |
| | | if (!show && visible) { |
| | | const animation = Taro.createAnimation({ |
| | |
| | | } |
| | | }, [show]); |
| | | |
| | | // 打开弹窗时先拉取结束后离车时间 |
| | | useEffect(() => { |
| | | if (!show) { |
| | | return; |
| | |
| | | } |
| | | |
| | | export default function PassengerNotice({ show = false, onClose, onCheckedChange }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [checked, setChecked] = useState(true); |
| | | const [noticeContent, setNoticeContent] = useState(''); |
| | | const [thumbHeight, setThumbHeight] = useState(0); |
| | | const [thumbTop, setThumbTop] = useState(0); |
| | | const [containerHeight, setContainerHeight] = useState(0); |
| | | const [showScrollbar, setShowScrollbar] = useState(false); |
| | | const [visible, setVisible] = useState(false); // 弹窗显示状态 |
| | | const [animationData, setAnimationData] = useState<any>(); // 弹窗动画数据 |
| | | const [checked, setChecked] = useState(true); // 近三个月不再显示勾选状态 |
| | | const [noticeContent, setNoticeContent] = useState(''); // 须知富文本内容 |
| | | const [thumbHeight, setThumbHeight] = useState(0); // 自定义滚动条高度 |
| | | const [thumbTop, setThumbTop] = useState(0); // 自定义滚动条位置 |
| | | const [containerHeight, setContainerHeight] = useState(0); // 文本容器高度 |
| | | const [showScrollbar, setShowScrollbar] = useState(false); // 是否展示自定义滚动条 |
| | | |
| | | // 内容滚动时同步自定义滚动条位置 |
| | | const onScroll = (e) => { |
| | | const { scrollHeight, scrollTop } = e.detail; |
| | | if (containerHeight > 0 && scrollHeight > containerHeight) { |
| | |
| | | } |
| | | }; |
| | | |
| | | // 文案渲染完成后计算滚动条尺寸 |
| | | useEffect(() => { |
| | | if (noticeContent && visible) { |
| | | // Give it a little time to render |
| | |
| | | } |
| | | }, [noticeContent, visible]); |
| | | |
| | | // 控制弹窗开合动画 |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | |
| | | } |
| | | }, [show]); |
| | | |
| | | // 每次打开时拉取最新须知内容 |
| | | useEffect(() => { |
| | | if (!show) { |
| | | return; |
| | |
| | | } |
| | | |
| | | export default function ReleaseBeBeing({ handleOpenCancelPopup, onFinish }: Props) { |
| | | const [progress, setProgress] = useState(0); |
| | | const [completedCount, setCompletedCount] = useState(0); |
| | | const [progress, setProgress] = useState(0); // 发布进度条 |
| | | const [completedCount, setCompletedCount] = useState(0); // 已完成步骤数 |
| | | const startSite = Taro.getStorageSync('startSite') || {}; |
| | | const endSite = Taro.getStorageSync('endSite') || {}; |
| | | const startSiteName = startSite?.name || startSite?.address || '-'; |
| | | const endSiteName = endSite?.name || endSite?.address || '-'; |
| | | |
| | | // 模拟发布流程进度和状态推进 |
| | | useEffect(() => { |
| | | // 使用 setTimeout 触发 CSS transition 动画 |
| | | const timer = setTimeout(() => { |
| | |
| | | |
| | | export default function ReleaseRoute({ startSite, endSite, callImmediately, fareEstimate = {}, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false }: Props) { |
| | | |
| | | // 跳转选点页(起点/终点) |
| | | const toSeleteSitePage = (type: string) => { |
| | | Taro.navigateTo({ |
| | | url: `/robotaxi/pages/select-map-site/index?pageType=${type}&skipSource=1`, |
| | | }) |
| | | } |
| | | // 跳转费用说明页 |
| | | const toPriceDescription = () => { |
| | | Taro.navigateTo({ |
| | | url: `/robotaxi/pages/expense-detail/index`, |
| | | }) |
| | | } |
| | | // 格式化步行时长文案 |
| | | const formatWalkingTime = (minutes: number) => { |
| | | const safeMinutes = Math.max(0, Math.floor(Number(minutes) || 0)); |
| | | if (safeMinutes >= 60) { |
| | |
| | | shouldShowWaitingRadar: boolean; |
| | | isAndroid: boolean; |
| | | tripStatus: number; |
| | | mapScale: number; |
| | | startSite: any; |
| | | endSite: any; |
| | | vehicleLocation: { latitude: number; longitude: number } | null; |
| | |
| | | updateArriveCalloutPosition(); |
| | | }, [deps.isAndroid, deps.tripStatus, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | |
| | | useEffect(() => { |
| | | if (!deps.isAndroid || deps.tripStatus !== 7) return; |
| | | if (deps.mapScale < 20) return; |
| | | const timer = setTimeout(() => { |
| | | updateArriveCalloutPosition(); |
| | | }, 80); |
| | | return () => clearTimeout(timer); |
| | | }, [deps.isAndroid, deps.tripStatus, deps.mapScale, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | |
| | | return { |
| | | waitingRadarStyle, |
| | | waitingRadarVisible, |
| | |
| | | vehicleLocation: { latitude: number; longitude: number } | null; |
| | | vehicleDirection: number | null; |
| | | vehicleMarkerId: number; |
| | | arrivedMarkerReady: boolean; |
| | | arrivedTooltipReady: boolean; |
| | | }; |
| | | |
| | | const defaultDeps: Deps = { |
| | |
| | | endSite: null, |
| | | vehicleLocation: null, |
| | | vehicleDirection: null, |
| | | vehicleMarkerId: 3 |
| | | vehicleMarkerId: 3, |
| | | arrivedMarkerReady: false, |
| | | arrivedTooltipReady: false |
| | | }; |
| | | |
| | | export default function useMapMarkers(rawDeps?: Partial<Deps>) { |
| | |
| | | const useAndroidNativeTrip1Callout = deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus); |
| | | const pickupMarker = createPickupMarker(pickupSite, useAndroidNativeTrip1Callout); |
| | | const dropoffMarker = createDropoffMarker(dropoffSite, useAndroidNativeTrip1Callout); |
| | | const shouldWaitArrivedDirection = deps.tripStatus === 7 && !deps.arrivedMarkerReady; |
| | | const effectiveShowCar = deps.statusRule.showCar && !shouldWaitArrivedDirection; |
| | | const carMarker = createCarMarker(); |
| | | const nextMarkers: any[] = []; |
| | | if (deps.statusRule.showPickup && pickupMarker) nextMarkers.push(pickupMarker); |
| | | if (deps.statusRule.showCar && carMarker) nextMarkers.push(carMarker); |
| | | if (effectiveShowCar && carMarker) nextMarkers.push(carMarker); |
| | | if (deps.statusRule.showDropoff && dropoffMarker) nextMarkers.push(dropoffMarker); |
| | | const nextMarkersSignature = nextMarkers |
| | | .map((m: any) => [ |
| | |
| | | .join(';'); |
| | | const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2); |
| | | const shouldHoldMarkerByTranslate = [4, 6].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldHoldMarkerWhenArrived = deps.tripStatus === 7 && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldSkipUpdate = (shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current)) || shouldHoldMarkerWhenArrived; |
| | | const shouldSkipUpdate = shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current); |
| | | applyMarkerState(nextMarkers, nextMarkersSignature, shouldSkipUpdate, carMarker); |
| | | }, [deps.tripStatus, deps.startSite?.name, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.name, deps.endSite?.latitude, deps.endSite?.longitude, deps.vehicleLocation, deps.vehicleDirection, deps.vehicleMarkerId, deps.isAndroid, deps.statusRule.showPickup, deps.statusRule.showCar, deps.statusRule.showDropoff]); |
| | | |
| | |
| | | |
| | | const mapRectRef = useRef<any>(null); |
| | | const viewportKeyRef = useRef<string>(''); |
| | | const orderDetailViewportLockedRef = useRef(false); |
| | | const tripStatus6ViewportLockedRef = useRef(false); |
| | | const orderDetailEnterSignatureRef = useRef(''); |
| | | const orderDetailRouteUpdatedAfterEnterRef = useRef(false); |
| | | |
| | | const isAndroid = useMemo(() => { |
| | | try { |
| | |
| | | return { points, keyParts }; |
| | | }; |
| | | |
| | | const buildOrderDetailRouteSignature = (pickupSite: any, dropoffSite: any) => { |
| | | const firstPoint = deps.routePoints[0]; |
| | | const lastPoint = deps.routePoints[deps.routePoints.length - 1]; |
| | | return [ |
| | | deps.routePoints.length, |
| | | firstPoint?.latitude ?? '', |
| | | firstPoint?.longitude ?? '', |
| | | lastPoint?.latitude ?? '', |
| | | lastPoint?.longitude ?? '', |
| | | pickupSite?.latitude ?? '', |
| | | pickupSite?.longitude ?? '', |
| | | dropoffSite?.latitude ?? '', |
| | | dropoffSite?.longitude ?? '', |
| | | ].join('|'); |
| | | }; |
| | | |
| | | const updateViewportByPayload = (viewportType: string, points: any[]) => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const bottomPadding = computeBottomPadding(points, viewportType); |
| | |
| | | const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite); |
| | | if (points.length === 0) return; |
| | | const nextKey = keyParts.join('|'); |
| | | if (nextKey === viewportKeyRef.current) return; |
| | | if (nextKey === viewportKeyRef.current) { |
| | | if (mapScale < DROPOFF_MAX_SCALE) { |
| | | setMapScale(DROPOFF_MAX_SCALE); |
| | | } |
| | | return; |
| | | } |
| | | viewportKeyRef.current = nextKey; |
| | | updateViewportByPayload(viewportType, points); |
| | | }, [deps.tripStatus, deps.statusRule?.viewport, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | }, [deps.tripStatus, deps.statusRule?.viewport, deps.endSite?.latitude, deps.endSite?.longitude, mapScale]); |
| | | |
| | | useEffect(() => { |
| | | if (![8, 9].includes(deps.tripStatus)) { |
| | | orderDetailViewportLockedRef.current = false; |
| | | orderDetailRouteUpdatedAfterEnterRef.current = false; |
| | | orderDetailEnterSignatureRef.current = ''; |
| | | return; |
| | | } |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | orderDetailEnterSignatureRef.current = buildOrderDetailRouteSignature(pickupSite, dropoffSite); |
| | | orderDetailRouteUpdatedAfterEnterRef.current = false; |
| | | orderDetailViewportLockedRef.current = false; |
| | | viewportKeyRef.current = ''; |
| | | setMapScale(14); |
| | | }, [deps.tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | if (![8, 9].includes(deps.tripStatus)) return; |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | const currentSignature = buildOrderDetailRouteSignature(pickupSite, dropoffSite); |
| | | if (!orderDetailEnterSignatureRef.current) { |
| | | orderDetailEnterSignatureRef.current = currentSignature; |
| | | return; |
| | | } |
| | | if (currentSignature === orderDetailEnterSignatureRef.current) return; |
| | | orderDetailRouteUpdatedAfterEnterRef.current = true; |
| | | orderDetailViewportLockedRef.current = false; |
| | | viewportKeyRef.current = ''; |
| | | }, [deps.tripStatus, deps.routePoints, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | |
| | | useEffect(() => { |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | |
| | | if (viewportType === 'dropoff') return; |
| | | const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite); |
| | | if (points.length === 0) return; |
| | | if ( |
| | | [8, 9].includes(deps.tripStatus) && |
| | | viewportType === 'order_detail' && |
| | | orderDetailViewportLockedRef.current && |
| | | orderDetailRouteUpdatedAfterEnterRef.current |
| | | ) { |
| | | return; |
| | | } |
| | | if (deps.tripStatus === 6 && viewportType === 'route' && tripStatus6ViewportLockedRef.current) return; |
| | | const nextKey = keyParts.join('|'); |
| | | if (nextKey === viewportKeyRef.current) return; |
| | |
| | | updateViewportByPayload(viewportType, points); |
| | | if (deps.tripStatus === 6 && viewportType === 'route') { |
| | | tripStatus6ViewportLockedRef.current = true; |
| | | } |
| | | if ([8, 9].includes(deps.tripStatus) && viewportType === 'order_detail') { |
| | | const hasOrderDetailRouteAfterEnter = orderDetailRouteUpdatedAfterEnterRef.current && deps.routePoints.length > 0; |
| | | orderDetailViewportLockedRef.current = hasOrderDetailRouteAfterEnter; |
| | | } |
| | | }, [deps.tripStatus, deps.statusRule, deps.routePoints, deps.walkingPoints, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.latitude, deps.endSite?.longitude, deps.vehicleLocation, deps.longitude, deps.latitude, mapBottomPadding]); |
| | | |
| | |
| | | setShowLocationFlag, |
| | | mapRectRef, |
| | | viewportKeyRef, |
| | | orderDetailViewportLockedRef, |
| | | tripStatus6ViewportLockedRef, |
| | | isAndroid, |
| | | resolveSafeBottomInset, |
| | |
| | | useEffect(() => { |
| | | const wasHold = [4, 6].includes(deps.lastTripStatusRef.current); |
| | | const isHold = [4, 6].includes(deps.tripStatus); |
| | | if (deps.tripStatus != 4) Taro.removeStorageSync('pickupEndTime'); |
| | | if (deps.tripStatus != 5) Taro.removeStorageSync('pickupEndTime'); |
| | | if (deps.tripStatus != 7) Taro.removeStorageSync('finishEndTime'); |
| | | if (!wasHold && isHold) { |
| | | deps.orderRouteInitializedRef.current = false; |
| | |
| | | arriveCalloutVisible: boolean; |
| | | /** 到达气泡样式 */ |
| | | arriveCalloutStyle: any; |
| | | /** 到达态气泡渲染就绪(角度与缩放均就绪) */ |
| | | arrivedTooltipReady: boolean; |
| | | /** 路线距离 */ |
| | | routeDistance: { value: number; unit: 'km' | 'm' } | null; |
| | | /** 路线时长 */ |
| | |
| | | </CoverView> |
| | | </CoverView> |
| | | )} |
| | | {(item.id === 2 && [7].includes(deps.tripStatus) && !deps.isAndroid && deps.mapScale >= 20) && ( |
| | | {(item.id === 2 && [7].includes(deps.tripStatus) && !deps.isAndroid && deps.arrivedTooltipReady) && ( |
| | | <CoverView className='custom-callout-content'> |
| | | 已到达 |
| | | </CoverView> |
| | |
| | | </View> |
| | | </View> |
| | | )} |
| | | {deps.isAndroid && deps.arriveCalloutVisible && ( |
| | | {deps.isAndroid && deps.arriveCalloutVisible && deps.arrivedTooltipReady && ( |
| | | <View className="car-callout-android" style={deps.arriveCalloutStyle}> |
| | | <View className="custom-callout-content"> |
| | | 已到达 |
| | |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | if (currentTripStatus === 7) { |
| | | const lastPoint = deps.lastVehiclePointRef.current; |
| | | const lastUpdateAt = Number(deps.lastVehicleUpdateAtRef.current || 0); |
| | | const now = Date.now(); |
| | | const latDiff = Math.abs(Number(lastPoint?.latitude || 0) - targetPoint.latitude); |
| | | const lngDiff = Math.abs(Number(lastPoint?.longitude || 0) - targetPoint.longitude); |
| | | const almostSamePoint = latDiff < 0.000003 && lngDiff < 0.000003; |
| | | if (almostSamePoint || (now - lastUpdateAt < 1200)) { |
| | | if (almostSamePoint) { |
| | | return; |
| | | } |
| | | } |
| | |
| | | const forceFullRedraw = payload?.forceFullRedraw === true; |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | updateRouteSummary(payload, deps); |
| | | // 行程结束态仅更新概要信息,不再消费路线点,避免视口反复重算造成跳动 |
| | | if ([8, 9].includes(currentTripStatus)) { |
| | | return; |
| | | } |
| | | if ([4, 6].includes(currentTripStatus)) { |
| | | deps.setVehicleMoveAlongPoints(servedPoints); |
| | | } |
| | |
| | | 4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | 5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | 6: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | 7: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'dropoff', translateCar: false }, |
| | | 7: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | 8: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | 9: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | 10: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | |
| | | lastVehiclePointRef, |
| | | lastVehicleUpdateAtRef |
| | | } = vehicleState; // 车辆状态解构 |
| | | const markerState = useMapMarkers({ |
| | | tripStatus, |
| | | statusRule, |
| | | isAndroid, |
| | | startSite, |
| | | endSite, |
| | | vehicleLocation, |
| | | vehicleDirection, |
| | | vehicleMarkerId |
| | | }); // 地图标记状态容器 |
| | | const { |
| | | markers, |
| | | tripStatus6MarkerRefreshedRef |
| | | } = markerState; // 标记状态解构 |
| | | const { |
| | | waitingRadarStyle, |
| | | waitingRadarVisible, |
| | | carCalloutStyle, |
| | | carCalloutVisible, |
| | | arriveCalloutStyle, |
| | | arriveCalloutVisible, |
| | | handleRegionChange: handleAndroidOverlayRegionChange |
| | | } = useAndroidOverlays({ |
| | | shouldShowWaitingRadar, |
| | | isAndroid, |
| | | tripStatus, |
| | | startSite, |
| | | endSite, |
| | | vehicleLocation, |
| | | longitude, |
| | | latitude |
| | | }); // Android 覆盖物状态编排 |
| | | const routeState = useRouteManager({ |
| | | isAndroid, |
| | | tripStatus, |
| | |
| | | toOrigin, |
| | | getLocationInfo |
| | | } = viewportState; // 视口状态解构 |
| | | const arrivedMarkerReady = tripStatus !== 7 || typeof vehicleDirection === 'number'; |
| | | const arrivedTooltipReady = tripStatus !== 7 || (arrivedMarkerReady && mapScale >= 20); |
| | | const markerState = useMapMarkers({ |
| | | tripStatus, |
| | | statusRule, |
| | | isAndroid, |
| | | startSite, |
| | | endSite, |
| | | vehicleLocation, |
| | | vehicleDirection, |
| | | vehicleMarkerId, |
| | | arrivedMarkerReady, |
| | | arrivedTooltipReady |
| | | }); // 地图标记状态容器 |
| | | const { |
| | | markers, |
| | | tripStatus6MarkerRefreshedRef |
| | | } = markerState; // 标记状态解构 |
| | | const { |
| | | waitingRadarStyle, |
| | | waitingRadarVisible, |
| | | carCalloutStyle, |
| | | carCalloutVisible, |
| | | arriveCalloutStyle, |
| | | arriveCalloutVisible, |
| | | handleRegionChange: handleAndroidOverlayRegionChange |
| | | } = useAndroidOverlays({ |
| | | shouldShowWaitingRadar, |
| | | isAndroid, |
| | | tripStatus, |
| | | mapScale, |
| | | startSite, |
| | | endSite, |
| | | vehicleLocation, |
| | | longitude, |
| | | latitude |
| | | }); // Android 覆盖物状态编排 |
| | | const isCheckedEnter = useRef(false); // 是否已检查页面进入状态 |
| | | const { checkDistanceAndCityReady } = useOrderDistanceConfig({ |
| | | endSite, |
| | |
| | | carCalloutStyle, |
| | | arriveCalloutVisible, |
| | | arriveCalloutStyle, |
| | | arrivedTooltipReady, |
| | | routeDistance, |
| | | routeDuration, |
| | | customCalloutIconSrc, |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useEffect, useMemo, useRef, useState } from 'react'; |
| | | import { getStartPoints, getEndPoints, queryPointByName } from '../../../api/site-search'; |
| | | import { debounce } from '../../../../utils/Robotaxi-public-method'; |
| | | import { checkOtp } from '../../../../utils/tools'; |
| | | import { getNearestSite } from '../../../api/home'; |
| | | import { _AMAP_KEY } from '../../../../constants/api'; |
| | |
| | | const [nearbySiteList, setNearbySiteList] = useState<any[]>([]); |
| | | // 页面类型(start/end) |
| | | const [pageType, setPageType] = useState(''); |
| | | // 当前关键词是否仍在搜索中(含高德补充结果) |
| | | const [isSearching, setIsSearching] = useState(false); |
| | | // 最近一次完成搜索的关键词 |
| | | const [lastCompletedKeyword, setLastCompletedKeyword] = useState(''); |
| | | const searchSeqRef = useRef(0); |
| | | const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| | | |
| | | // 拉取附近站点 |
| | | const fetchNearbySites = async (lat: number, lon: number, electronicAreaId?: string) => { |
| | |
| | | |
| | | // 站点搜索请求与结果处理 |
| | | const searchSite = async (val?: string) => { |
| | | setNonOperationalSite([]); |
| | | const queryVal = val !== undefined ? val : deps.searchValue; |
| | | if (!queryVal) return; |
| | | const normalizedQuery = String(queryVal || '').trim(); |
| | | searchSeqRef.current += 1; |
| | | const requestSeq = searchSeqRef.current; |
| | | setNonOperationalSite([]); |
| | | if (!normalizedQuery) { |
| | | setIsSearching(false); |
| | | setLastCompletedKeyword(''); |
| | | return; |
| | | } |
| | | setIsSearching(true); |
| | | const { latitude, longitude, electronicAreaId } = deps.router.params; |
| | | const selectedCity = Taro.getStorageSync('selectedCity'); |
| | | const payload = { |
| | |
| | | electronicAreaId: electronicAreaId ? Number(electronicAreaId) : 0, |
| | | lat: Number(latitude), |
| | | lon: Number(longitude), |
| | | pointPoi: queryVal |
| | | pointPoi: normalizedQuery |
| | | }; |
| | | try { |
| | | const res = await queryPointByName(payload); |
| | | if (requestSeq !== searchSeqRef.current) return; |
| | | const { availablePoints } = res; |
| | | if (Array.isArray(availablePoints)) { |
| | | const list = availablePoints.map(normalizeSiteItem); |
| | | const sorted = sortSitesByNameMatchAndDistance(list, queryVal); |
| | | const sorted = sortSitesByNameMatchAndDistance(list, normalizedQuery); |
| | | setSiteList(sorted.slice(0, deps.maxExpandedCount)); |
| | | } else { |
| | | setSiteList([]); |
| | | } |
| | | const myAmapFun = new amapFile.AMapWX({ key: _AMAP_KEY }); |
| | | myAmapFun.getPoiAround({ |
| | | querykeywords: queryVal, |
| | | location: `${longitude},${latitude}`, |
| | | success: (data: any) => { |
| | | if (data && data.poisData) { |
| | | const list = data.poisData.map((item: any) => { |
| | | const locationArr = item.location.split(','); |
| | | return { |
| | | ...item, |
| | | longitude: Number(locationArr[0]), |
| | | latitude: Number(locationArr[1]), |
| | | ...normalizeSiteItem(item) |
| | | }; |
| | | }); |
| | | const sorted = sortSitesByNameMatchAndDistance(list, queryVal); |
| | | setNonOperationalSite(sorted.slice(0, deps.maxShowCount)); |
| | | await new Promise<void>((resolve) => { |
| | | myAmapFun.getPoiAround({ |
| | | querykeywords: normalizedQuery, |
| | | location: `${longitude},${latitude}`, |
| | | success: (data: any) => { |
| | | if (requestSeq !== searchSeqRef.current) { |
| | | resolve(); |
| | | return; |
| | | } |
| | | if (data && data.poisData) { |
| | | const list = data.poisData.map((item: any) => { |
| | | const locationArr = item.location.split(','); |
| | | return { |
| | | ...item, |
| | | longitude: Number(locationArr[0]), |
| | | latitude: Number(locationArr[1]), |
| | | ...normalizeSiteItem(item) |
| | | }; |
| | | }); |
| | | const sorted = sortSitesByNameMatchAndDistance(list, normalizedQuery); |
| | | setNonOperationalSite(sorted.slice(0, deps.maxShowCount)); |
| | | } else { |
| | | setNonOperationalSite([]); |
| | | } |
| | | resolve(); |
| | | }, |
| | | fail: () => { |
| | | if (requestSeq === searchSeqRef.current) { |
| | | setNonOperationalSite([]); |
| | | } |
| | | resolve(); |
| | | } |
| | | }, |
| | | fail: () => { } |
| | | }); |
| | | }); |
| | | } catch { |
| | | if (requestSeq !== searchSeqRef.current) return; |
| | | setSiteList([]); |
| | | setNonOperationalSite([]); |
| | | } finally { |
| | | if (requestSeq === searchSeqRef.current) { |
| | | setIsSearching(false); |
| | | setLastCompletedKeyword(normalizedQuery); |
| | | } |
| | | } |
| | | }; |
| | | |
| | |
| | | searchSiteRef.current = searchSite; |
| | | |
| | | // 搜索输入防抖 |
| | | const debouncedSearch = useMemo(() => debounce((val) => { |
| | | searchSiteRef.current(val); |
| | | }, 300), []); |
| | | const debouncedSearch = useMemo(() => { |
| | | return (val: string) => { |
| | | if (debounceTimerRef.current) { |
| | | clearTimeout(debounceTimerRef.current); |
| | | } |
| | | debounceTimerRef.current = setTimeout(() => { |
| | | searchSiteRef.current(val); |
| | | }, 300); |
| | | }; |
| | | }, []); |
| | | |
| | | // 清空搜索结果列表 |
| | | const clearSearchResults = () => { |
| | | if (debounceTimerRef.current) { |
| | | clearTimeout(debounceTimerRef.current); |
| | | debounceTimerRef.current = null; |
| | | } |
| | | // 推进请求序号,使清空前已发出的请求结果失效 |
| | | searchSeqRef.current += 1; |
| | | setSiteList([]); |
| | | setNonOperationalSite([]); |
| | | setIsSearching(false); |
| | | setLastCompletedKeyword(''); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | return () => { |
| | | if (debounceTimerRef.current) { |
| | | clearTimeout(debounceTimerRef.current); |
| | | } |
| | | }; |
| | | }, []); |
| | | |
| | | // 初始化:拉取附近站点、历史记录、页面类型 |
| | | useEffect(() => { |
| | |
| | | historySite, |
| | | nonOperationalSite, |
| | | nearbySiteList, |
| | | isSearching, |
| | | lastCompletedKeyword, |
| | | searchSite, |
| | | debouncedSearch, |
| | | clearSearchResults |
| | |
| | | historySite, |
| | | nonOperationalSite, |
| | | nearbySiteList, |
| | | isSearching, |
| | | lastCompletedKeyword, |
| | | searchSite, |
| | | debouncedSearch, |
| | | clearSearchResults |
| | |
| | | const recommendList = searchValue ? sortSitesByNameMatchAndDistance(siteList, searchValue) : filteredNearbyList; |
| | | const visibleHistory = showAllHistory ? historySite.slice(0, MAX_EXPANDED_COUNT) : historySite.slice(0, MAX_SHOW_COUNT); |
| | | const visibleRecommend = showAllRecommend ? recommendList.slice(0, MAX_EXPANDED_COUNT) : recommendList.slice(0, MAX_SHOW_COUNT); |
| | | const canShowSearchFallback = !searchValue || (!isSearching && lastCompletedKeyword === searchValue.trim()); |
| | | |
| | | // 使用数据截断展示,不再通过高度裁剪内容 |
| | | |
| | |
| | | </View> |
| | | } |
| | | { |
| | | (!siteList.length && (!searchValue ? !nearbySiteList.length : true) && !nonOperationalSite.length && (!searchValue ? !historySite.length : true)) && |
| | | canShowSearchFallback && (!siteList.length && (!searchValue ? !nearbySiteList.length : true) && !nonOperationalSite.length && (!searchValue ? !historySite.length : true)) && |
| | | <View className='history-card'> |
| | | <View className='card-header'>暂无搜索结果,为您推荐附近站点</View> |
| | | <View className='card-content list-content'> |