feat(begin-route): 优化蓝牙、温控与车辆跟踪功能及UI
- 更新蓝牙卡片提示文案与操作按钮的显示逻辑
- 新增setTemperatureIgnoreBusinessError API用于初始化越界温度纠正
- 优化车辆跟踪的旋转计算逻辑与地图动画参数
- 为结束行程弹窗和温度调整添加防抖处理防止重复触发
- 处理初始温度越界的自动修正逻辑
- 调整UI样式、注释无用代码并修复CSS缩进问题
- 简化运营区域页面的描述文本
- 清理无用的控制台日志输出
| | |
| | | }, true); |
| | | } |
| | | |
| | | // 设置温度并跳过业务异常拦截,供初始化越界温度纠正场景使用 |
| | | export const setTemperatureIgnoreBusinessError = async (payload: SetTemperaturePayload): Promise<SetTemperatureResponse> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/ac/set-temperature', |
| | | method: 'POST', |
| | | payload, |
| | | skipException: true, |
| | | showToast: false |
| | | }, true); |
| | | } |
| | | |
| | | // 解锁车辆 |
| | | export const unlockVehicle = async (payload: UnlockVehiclePayload): Promise<UnlockVehicleResponse> => { |
| | | return request({ |
| | |
| | | <Image src={iconSrc} /> |
| | | <View>{description}</View> |
| | | </View> |
| | | {!!actionText && ( |
| | | {(!!actionText && actionText !== '去开启') && ( |
| | | <View className="bluetooth-operate-card-right"> |
| | | <View>{actionText}</View> |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/blue-right-icon.png" /> |
| | |
| | | const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态 |
| | | const [lockStatus, setLockStatus] = useState(false); // 车门解锁状态 |
| | | const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器 |
| | | const overRoutePopupDebounceTimerRef = useRef<any>(null); // 结束行程弹窗防抖定时器 |
| | | const OVER_ROUTE_POPUP_DEBOUNCE_MS = 1000; // 结束行程弹窗点击防抖时长 |
| | | |
| | | // 外部后备箱状态变化时同步本地状态 |
| | | useEffect(() => { |
| | |
| | | if (trunkPollTimerRef.current) { |
| | | clearTimeout(trunkPollTimerRef.current); |
| | | } |
| | | if (overRoutePopupDebounceTimerRef.current) { |
| | | clearTimeout(overRoutePopupDebounceTimerRef.current); |
| | | } |
| | | }; |
| | | }, []); |
| | | |
| | | // 打开结束行程弹窗并在短时间内忽略重复点击 |
| | | const handleOpenOverRoutePopupWithDebounce = () => { |
| | | if (!handleOpenOverRoutePopup || overRoutePopupDebounceTimerRef.current) { |
| | | return; |
| | | } |
| | | handleOpenOverRoutePopup(); |
| | | overRoutePopupDebounceTimerRef.current = setTimeout(() => { |
| | | overRoutePopupDebounceTimerRef.current = null; |
| | | }, OVER_ROUTE_POPUP_DEBOUNCE_MS); |
| | | }; |
| | | |
| | | // 获取解锁请求所需定位参数,优先高精定位,失败后降级普通定位 |
| | | const getUnlockLocationPayload = async () => { |
| | |
| | | const actionLoadingText = doorType === 'trunk' |
| | | ? (isCloseAction ? '正在关闭后备箱' : '正在开启后备箱') |
| | | : '正在解锁车门'; |
| | | Taro.showLoading({ title: actionLoadingText, mask: true }); |
| | | // Taro.showLoading({ title: actionLoadingText, mask: true }); |
| | | unlockVehicle({ |
| | | orderId: targetOrderId, |
| | | doorType, |
| | |
| | | console.log('解锁结果:', res); |
| | | |
| | | if (res?.code !== 200 || !res?.data) { |
| | | Taro.hideLoading(); |
| | | // Taro.hideLoading(); |
| | | Taro.showToast({ title: res?.msg || '解锁失败,请重试或联系客服', icon: 'none' }); |
| | | return; |
| | | } |
| | |
| | | trunkPollTimerRef.current = setTimeout(doPoll, pollInterval); |
| | | } else { |
| | | stopPolling(); |
| | | Taro.hideLoading(); |
| | | // Taro.hideLoading(); |
| | | Taro.showToast({ |
| | | title: failToastText, |
| | | icon: 'none' |
| | |
| | | stopPolling(); |
| | | const isOpen = !isCloseAction; |
| | | setTrunkStatus(isOpen); |
| | | Taro.hideLoading(); |
| | | // Taro.hideLoading(); |
| | | Taro.showToast({ |
| | | title: successToastText, |
| | | icon: 'none' |
| | |
| | | } |
| | | |
| | | if (doorType === 'door') { |
| | | Taro.hideLoading(); |
| | | // Taro.hideLoading(); |
| | | Taro.showToast({ title: '右侧车门已解锁', icon: 'none' }); |
| | | onDoorUnlockSuccess?.(); |
| | | onSuccess?.(); |
| | | } |
| | | }).catch(() => { |
| | | Taro.hideLoading(); |
| | | // Taro.hideLoading(); |
| | | Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' }); |
| | | }); |
| | | }; |
| | |
| | | <Image className="car-card-reach-btn-item-img" src={trunkStatus ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'} /> |
| | | <View className="car-card-reach-btn-item-text">{trunkStatus ? '关闭后备箱' : '打开后备箱'}</View> |
| | | </View> |
| | | <View className="car-card-reach-btn-item" onClick={handleOpenOverRoutePopup}> |
| | | <View className="car-card-reach-btn-item" onClick={handleOpenOverRoutePopupWithDebounce}> |
| | | <Image className="car-card-reach-btn-item-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/over-gps-icon.png" /> |
| | | <View className="car-card-reach-btn-item-text">结束行程</View> |
| | | </View> |
| | |
| | | airConditionerUnlock, |
| | | queryTemperatureByOrderId, |
| | | queryVehicleAirConditionerSwitch, |
| | | setTemperature |
| | | setTemperature, |
| | | setTemperatureIgnoreBusinessError |
| | | } from '../../../../api/begin-route'; |
| | | import { getOrderId } from '../../../../utils/order'; |
| | | import { TripStatus } from '../../../../constants/trip-status'; |
| | |
| | | const lastConfirmedTempRef = useRef<number | null>(null); // 最近一次设置成功温度 |
| | | const acQuerySeqRef = useRef(0); // 空调状态查询请求序号,避免旧请求覆盖新状态 |
| | | const skipQueryOnThisCycleRef = useRef(false); // 修改目的地返回本次页面恢复是否跳过温度/空调查询 |
| | | const INITIAL_OUT_OF_RANGE_TARGET_TEMP = 22; // 初始化查询温度越界时回写的目标温度 |
| | | const TEMP_UPDATE_DEBOUNCE_MS = 1000; // 温度调整后提交接口的防抖时长 |
| | | |
| | | const TEMP_MIN = 22; // 空调最低温度 |
| | | const TEMP_MAX = 28; // 空调最高温度 |
| | |
| | | const res: any = await queryTemperatureByOrderId({ orderId: targetOrderId }); |
| | | const currentTemperature = Number(res); |
| | | if (!Number.isNaN(currentTemperature)) { |
| | | setTempStatus(currentTemperature); |
| | | lastConfirmedTempRef.current = currentTemperature; |
| | | const shouldResetTemperature = currentTemperature < TEMP_MIN || currentTemperature > TEMP_MAX; |
| | | if (!shouldResetTemperature) { |
| | | setTempStatus(currentTemperature); |
| | | lastConfirmedTempRef.current = currentTemperature; |
| | | return; |
| | | } |
| | | try { |
| | | await setTemperatureIgnoreBusinessError({ |
| | | orderId: targetOrderId, |
| | | targetTemperature: INITIAL_OUT_OF_RANGE_TARGET_TEMP |
| | | }); |
| | | setTempStatus(INITIAL_OUT_OF_RANGE_TARGET_TEMP); |
| | | lastConfirmedTempRef.current = INITIAL_OUT_OF_RANGE_TARGET_TEMP; |
| | | } catch (innerError) { |
| | | setTempStatus(currentTemperature); |
| | | lastConfirmedTempRef.current = currentTemperature; |
| | | } |
| | | } |
| | | } catch (error) { |
| | | // ignore |
| | |
| | | icon: 'none' |
| | | }); |
| | | }); |
| | | }, 2000); |
| | | }, TEMP_UPDATE_DEBOUNCE_MS); |
| | | }; |
| | | |
| | | // 处理温度增减并做边界校验 |
| | |
| | | } |
| | | const targetOpenState = !acSwitchOn; |
| | | setIsTogglingAc(true); |
| | | Taro.showLoading({ |
| | | title: targetOpenState ? AC_OPENING_TEXT : AC_CLOSING_TEXT, |
| | | mask: true |
| | | }); |
| | | // Taro.showLoading({ |
| | | // title: targetOpenState ? AC_OPENING_TEXT : AC_CLOSING_TEXT, |
| | | // mask: true |
| | | // }); |
| | | try { |
| | | const res = await airConditionerUnlock({ |
| | | orderId: targetOrderId, |
| | |
| | | queryTemperature(targetOrderId); |
| | | } |
| | | } catch (error) { |
| | | Taro.hideLoading(); |
| | | // Taro.hideLoading(); |
| | | Taro.showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' }); |
| | | } finally { |
| | | Taro.hideLoading(); |
| | | // Taro.hideLoading(); |
| | | setIsTogglingAc(false); |
| | | } |
| | | }; |
| | |
| | | } |
| | | </View> |
| | | <View className="car-card-temp-num-text"> |
| | | <View className={`car-card-temp-num-text-temp ${tempTextDisabledClassName}`.trim()}>{typeof tempStatus === 'number' ? `${tempStatus.toFixed(1)}℃` : '--'}</View> |
| | | <View className={`car-card-temp-num-text-temp ${tempTextDisabledClassName}`.trim()}>{typeof tempStatus === 'number' ? `${tempStatus.toFixed(1)}℃` : `${TEMP_MIN}℃`}</View> |
| | | <View className={`car-card-temp-num-text-desc ${tempTextDisabledClassName}`.trim()}>设定温度</View> |
| | | </View> |
| | | <View className="car-card-temp-num-btn"> |
| | |
| | | orderId, |
| | | orderTrunkStatus, |
| | | bluetoothStatusReady = false, |
| | | bluetoothCardDescription = '系统蓝牙未开启,无法自动解锁车辆', |
| | | bluetoothCardDescription = '授权并打开蓝牙链接,享受无感出行', |
| | | bluetoothCardActionText = '去开启', |
| | | handleBluetoothCardClick, |
| | | showAuthBluetoothPopup = false, |
| | |
| | | font-weight: 500; |
| | | line-height: 40px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | .modify-destination-review-tip { |
| | | position: relative; |
| | | z-index: 1; |
| | | margin-top: 4px; |
| | | font-size: 24px; |
| | | line-height: 36px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | margin-bottom: 32px; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | .start-txt{ |
| | | .start-txt { |
| | | color: rgba(0, 0, 0, 0.4); |
| | | } |
| | | |
| | |
| | | |
| | | &-fare { |
| | | min-width: 0; |
| | | .modify-destination-review-operate-free{ |
| | | |
| | | .modify-destination-review-operate-free { |
| | | margin-top: 28px; |
| | | } |
| | | } |
| | |
| | | background: rgba(255, 255, 255, 0.45); |
| | | z-index: 2; |
| | | } |
| | | } |
| | | } |
| | |
| | | <View className='modify-destination-review-title'> |
| | | 由于目的地变更,本次行程费用已更新 |
| | | </View> |
| | | {/* <View className='modify-destination-review-tip'>仅可修改一次</View> */} |
| | | <View className='modify-destination-review-select-site'> |
| | | <View className='modify-destination-review-sites-card'> |
| | | <View className='modify-destination-review-site-item'> |
| | |
| | | // 蓝牙卡片主文案 |
| | | const cardDescription = useMemo(() => { |
| | | if (status === 'system_off') { |
| | | return '系统蓝牙未开启,无法自动解锁车辆'; |
| | | return '授权并打开蓝牙链接,享受无感出行'; |
| | | } |
| | | if (status === 'unauthorized') { |
| | | return '授权蓝牙,车辆自动解锁'; |
| | |
| | | const nextMarkersSignature = buildMarkersSignature(nextMarkers, deps.vehicleMarkerId, shouldUsePureTranslateCarMarker); |
| | | const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2); |
| | | const shouldHoldMarkerByTranslate = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldRefreshInServiceRotate = deps.tripStatus === TripStatus.InService |
| | | && !!carMarker |
| | | && !!currentCarMarker |
| | | && currentCarMarker.rotate !== carMarker.rotate; |
| | | const shouldSkipUpdate = !shouldRefreshInServiceRotate |
| | | && shouldHoldMarkerByTranslate |
| | | const shouldSkipUpdate = shouldHoldMarkerByTranslate |
| | | && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current); |
| | | if (deps.tripStatus === TripStatus.InService && carMarker) { |
| | | console.log('[开始行程][送驾方向][Marker]', { |
| | | 车辆方向: deps.vehicleDirection, |
| | | 当前Marker角度: currentCarMarker?.rotate, |
| | | 下一次Marker角度: carMarker.rotate, |
| | | 是否触发角度刷新: shouldRefreshInServiceRotate, |
| | | 是否跳过Marker更新: shouldSkipUpdate, |
| | | 车辆位置: deps.vehicleLocation, |
| | | 车辆标记ID: deps.vehicleMarkerId, |
| | |
| | | longitude: number; |
| | | rotate: number; |
| | | } | null>(null); |
| | | const lastResolvedRotateRef = useRef<number | null>(null); |
| | | |
| | | // 执行一次平移动画,完成后自动消费最新待执行目标 |
| | | const flushTranslateTask = () => { |
| | |
| | | longitude: task.longitude, |
| | | }, |
| | | autoRotate: false, |
| | | moveWithRotate: false, |
| | | moveWithRotate: true, |
| | | rotate: task.rotate, |
| | | duration: task.duration, |
| | | animationEnd: () => { |
| | |
| | | ? ((vehicleDirection % 360) + 360) % 360 |
| | | : 0; |
| | | const lastAppliedTask = lastAppliedTranslateTaskRef.current; |
| | | if (lastAppliedTask) { |
| | | targetRotate = resolveShortestRotatePath(targetRotate, lastAppliedTask.rotate); |
| | | const previousResolvedRotate = typeof lastResolvedRotateRef.current === 'number' |
| | | ? lastResolvedRotateRef.current |
| | | : lastAppliedTask?.rotate; |
| | | if (typeof previousResolvedRotate === 'number') { |
| | | targetRotate = resolveShortestRotatePath(targetRotate, previousResolvedRotate); |
| | | } |
| | | lastResolvedRotateRef.current = targetRotate; |
| | | const hasLocationDelta = hasMeaningfulLocationDelta(vehicleLocation, lastAppliedTask); |
| | | const rotateDelta = lastAppliedTask |
| | | ? getRotateDelta(targetRotate, lastAppliedTask.rotate) |
| | |
| | | isTranslatingRef.current = false; |
| | | pendingTranslateRef.current = null; |
| | | lastAppliedTranslateTaskRef.current = null; |
| | | lastResolvedRotateRef.current = null; |
| | | }, [statusRule.translateCar]); |
| | | |
| | | return { |
| | |
| | | padding-left: 15px; |
| | | padding-right: 8px; |
| | | box-sizing: border-box; |
| | | overflow: visible; |
| | | } |
| | | |
| | | .route-summary-callout-bg { |
| | |
| | | color: #FFFFFF; |
| | | white-space: nowrap; |
| | | text-align: center; |
| | | padding-right: 8px; |
| | | overflow: visible; |
| | | } |
| | | |
| | | .txt-center { |
| | |
| | | </View> |
| | | <View className="content-desc"> |
| | | <View className="desc-item"> |
| | | <Text className="index">1.</Text> |
| | | <Text className="text">在运营时段可正常使用打车服务,非运营时段服务暂停</Text> |
| | | </View> |
| | | <View className="desc-item"> |
| | | <Text className="index">2.</Text> |
| | | <Text className="text">各站点运营时间可能存在调整,实际运营情况请以页面实时展示为准</Text> |
| | | </View> |
| | | <View className="desc-item"> |
| | | <Text className="index">3.</Text> |
| | | <Text className="text">如有问题,请拨打客服热线xxxxxx</Text> |
| | | </View> |
| | | {/* <View className="desc-item"> |
| | | <Text className="index">2.</Text> |
| | | <Text className="text">各站点运营时间可能存在调整,实际运营情况请以页面实时展示为准</Text> |
| | | </View> */} |
| | | </View> |
| | | </View> |
| | | </View> |