feat(robotaxi): 优化上车点持久化逻辑并修复修改目的地返回提示
- 新增use-home-start-site-display钩子管理首页上车点展示状态,持久化用户手动选择的上车点
- 修复从修改目的地页面返回时错误弹出「已有进行中订单」提示的问题
- 优化首页导航、站点搜索与地图选点页面的上车点状态同步逻辑
- 调整站点选择后的返回路由适配修改目的地业务流程
| | |
| | | isAdsorbed: boolean; |
| | | /** 当前站点。 */ |
| | | currentSite: any; |
| | | /** 首页确认上车点。 */ |
| | | preferredStartSite: any; |
| | | /** 是否展示快捷胶囊。 */ |
| | | showQuickCapsule: boolean; |
| | | /** 快捷胶囊文案。 */ |
| | |
| | | * 首页底部内容面板。 |
| | | */ |
| | | export default function HomeContentPanel(props: Props) { |
| | | const startSiteText = props.preferredStartSite?.name |
| | | || ((!props.isOperatingArea || props.isAdsorbed) ? (props.currentSite?.name || '请选择上车点') : '推荐上车点'); |
| | | |
| | | return ( |
| | | <View |
| | | className="content-box" |
| | |
| | | <View className="now-site-address-content"> |
| | | 从 |
| | | <Text className="now-site-address-text"> |
| | | {(!props.isOperatingArea || props.isAdsorbed) ? (props.currentSite?.name || '请选择上车点') : '推荐上车点'} |
| | | {startSiteText} |
| | | </Text> |
| | | 出发 |
| | | </View> |
| | |
| | | import wsManager from '../../../../utils/websocket'; |
| | | import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status'; |
| | | import { |
| | | clearHomeStartSiteDisplayState, |
| | | HOME_START_SITE_SOURCE_ACTIVE_ORDER, |
| | | isManualSelectedHomeStartSiteDisplayState, |
| | | readHomeStartSiteDisplayState, |
| | | resolveHomeStartSiteDisplaySite, |
| | | writeHomeStartSiteDisplayState |
| | | } from './use-home-start-site-display'; |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | |
| | | hasLogin: boolean; |
| | | /** 当前是否存在活跃订单。 */ |
| | | hasActiveOrder: boolean; |
| | | /** 当前首页确认上车点。 */ |
| | | preferredStartSite: any; |
| | | }; |
| | | |
| | | type Deps = { |
| | |
| | | ensureSelectedCityReady: (lng: number, lat: number, skipCityEffect?: boolean, forceRefresh?: boolean) => Promise<any>; |
| | | /** 设置当前站点 */ |
| | | setCurrentSiteFromStart: (value: any) => void; |
| | | /** 设置首页确认上车点。 */ |
| | | setPreferredStartSite: (value: any) => void; |
| | | /** 首次进入 key */ |
| | | firstEnterKey: string; |
| | | /** 默认纬度 */ |
| | |
| | | const otp = await deps.checkOtp(false); |
| | | deps.setNeedLogin(!otp); |
| | | if (!otp) { |
| | | return { hasLogin: false, hasActiveOrder: false } as AuthOrderSyncResult; |
| | | deps.setPreferredStartSite(null); |
| | | return { hasLogin: false, hasActiveOrder: false, preferredStartSite: null } as AuthOrderSyncResult; |
| | | } |
| | | wsManager.connect(deps.wsHost); |
| | | const agreement = await deps.getAgreement(); |
| | |
| | | } |
| | | const activeOrder = await deps.getOrderActive(); |
| | | if (!activeOrder) { |
| | | const preferredState = readHomeStartSiteDisplayState(); |
| | | const preferredStartSite = isManualSelectedHomeStartSiteDisplayState(preferredState) |
| | | ? resolveHomeStartSiteDisplaySite() |
| | | : null; |
| | | Taro.removeStorageSync('orderId'); |
| | | Taro.removeStorageSync('tripStatus'); |
| | | Taro.removeStorageSync('startSite'); |
| | | Taro.removeStorageSync('endSite'); |
| | | if (!preferredStartSite) { |
| | | Taro.removeStorageSync('startSite'); |
| | | } else { |
| | | Taro.setStorageSync('startSite', preferredStartSite); |
| | | } |
| | | if (preferredState?.source === HOME_START_SITE_SOURCE_ACTIVE_ORDER) { |
| | | clearHomeStartSiteDisplayState(); |
| | | } |
| | | deps.setTripStatus(0); |
| | | deps.setOrderData({}); |
| | | return { hasLogin: true, hasActiveOrder: false } as AuthOrderSyncResult; |
| | | deps.setPreferredStartSite(preferredStartSite); |
| | | return { hasLogin: true, hasActiveOrder: false, preferredStartSite } as AuthOrderSyncResult; |
| | | } |
| | | deps.setOrderData(activeOrder || {}); |
| | | const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0)); |
| | |
| | | Taro.setStorageSync('orderId', activeOrder.id || ''); |
| | | Taro.setStorageSync('startSite', startSite); |
| | | Taro.setStorageSync('endSite', endSite); |
| | | return { hasLogin: true, hasActiveOrder: true } as AuthOrderSyncResult; |
| | | writeHomeStartSiteDisplayState(startSite, HOME_START_SITE_SOURCE_ACTIVE_ORDER); |
| | | deps.setPreferredStartSite(startSite); |
| | | return { hasLogin: true, hasActiveOrder: true, preferredStartSite: startSite } as AuthOrderSyncResult; |
| | | }; |
| | | |
| | | /** |
| | |
| | | const auth = res.authSetting['scope.userLocation']; |
| | | Promise.resolve(authSyncTask) |
| | | .then((syncResult) => { |
| | | const startSite = syncResult.hasActiveOrder ? Taro.getStorageSync('startSite') : null; |
| | | const startSite = syncResult.hasActiveOrder |
| | | ? Taro.getStorageSync('startSite') |
| | | : syncResult.preferredStartSite; |
| | | processLocationSetting(auth, startSite); |
| | | }) |
| | | .catch(() => { |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | |
| | | export const HOME_START_SITE_SOURCE_MANUAL_SELECTED = 'manual_selected'; |
| | | export const HOME_START_SITE_SOURCE_ACTIVE_ORDER = 'active_order'; |
| | | export const HOME_START_SITE_DISPLAY_SOURCES = [ |
| | | HOME_START_SITE_SOURCE_MANUAL_SELECTED, |
| | | HOME_START_SITE_SOURCE_ACTIVE_ORDER |
| | | ] as const; |
| | | |
| | | export type HomeStartSiteDisplaySource = typeof HOME_START_SITE_DISPLAY_SOURCES[number]; |
| | | |
| | | export type HomeStartSiteDisplayState = { |
| | | site: any; |
| | | source: HomeStartSiteDisplaySource; |
| | | cityCode: string; |
| | | updatedAt: number; |
| | | }; |
| | | |
| | | export const HOME_START_SITE_DISPLAY_STATE_KEY = 'homeStartSiteDisplayState'; |
| | | |
| | | /** |
| | | * 判断站点是否具备有效经纬度。 |
| | | */ |
| | | export const isValidHomeStartSite = (site: any) => ( |
| | | Number.isFinite(Number(site?.latitude)) && Number.isFinite(Number(site?.longitude)) |
| | | ); |
| | | |
| | | /** |
| | | * 读取当前已选城市编码。 |
| | | */ |
| | | const getSelectedCityCode = () => String(Taro.getStorageSync('selectedCity')?.code || ''); |
| | | |
| | | /** |
| | | * 读取首页确认上车点显示态。 |
| | | */ |
| | | export const readHomeStartSiteDisplayState = () => { |
| | | const cachedState = Taro.getStorageSync(HOME_START_SITE_DISPLAY_STATE_KEY); |
| | | if (!cachedState || !isValidHomeStartSite(cachedState?.site)) { |
| | | return null; |
| | | } |
| | | const source = String(cachedState?.source || '') as HomeStartSiteDisplaySource; |
| | | if (!HOME_START_SITE_DISPLAY_SOURCES.includes(source)) { |
| | | return null; |
| | | } |
| | | return { |
| | | site: cachedState.site, |
| | | source, |
| | | cityCode: String(cachedState?.cityCode || ''), |
| | | updatedAt: Number(cachedState?.updatedAt || Date.now()) |
| | | } as HomeStartSiteDisplayState; |
| | | }; |
| | | |
| | | /** |
| | | * 清理首页确认上车点显示态。 |
| | | */ |
| | | export const clearHomeStartSiteDisplayState = () => { |
| | | Taro.removeStorageSync(HOME_START_SITE_DISPLAY_STATE_KEY); |
| | | }; |
| | | |
| | | /** |
| | | * 判断缓存显示态是否仍可用于当前城市。 |
| | | */ |
| | | export const isHomeStartSiteDisplayStateUsable = (state: HomeStartSiteDisplayState | null, cityCodeArg?: string) => { |
| | | if (!state || !isValidHomeStartSite(state.site)) { |
| | | return false; |
| | | } |
| | | const currentCityCode = String(cityCodeArg || getSelectedCityCode() || ''); |
| | | if (!state.cityCode || !currentCityCode) { |
| | | return true; |
| | | } |
| | | return state.cityCode === currentCityCode; |
| | | }; |
| | | |
| | | /** |
| | | * 判断当前显示态是否为用户手动确认的上车点。 |
| | | */ |
| | | export const isManualSelectedHomeStartSiteDisplayState = (state: HomeStartSiteDisplayState | null) => ( |
| | | state?.source === HOME_START_SITE_SOURCE_MANUAL_SELECTED |
| | | ); |
| | | |
| | | /** |
| | | * 写入首页确认上车点显示态。 |
| | | */ |
| | | export const writeHomeStartSiteDisplayState = (site: any, source: HomeStartSiteDisplaySource, cityCodeArg?: string) => { |
| | | if (!isValidHomeStartSite(site)) { |
| | | clearHomeStartSiteDisplayState(); |
| | | return; |
| | | } |
| | | const nextState: HomeStartSiteDisplayState = { |
| | | site, |
| | | source, |
| | | cityCode: String(cityCodeArg || getSelectedCityCode() || ''), |
| | | updatedAt: Date.now() |
| | | }; |
| | | Taro.setStorageSync(HOME_START_SITE_DISPLAY_STATE_KEY, nextState); |
| | | }; |
| | | |
| | | /** |
| | | * 解析当前可用的首页确认上车点。 |
| | | */ |
| | | export const resolveHomeStartSiteDisplaySite = (cityCodeArg?: string) => { |
| | | const cachedState = readHomeStartSiteDisplayState(); |
| | | if (!isHomeStartSiteDisplayStateUsable(cachedState, cityCodeArg)) { |
| | | clearHomeStartSiteDisplayState(); |
| | | return null; |
| | | } |
| | | return cachedState?.site || null; |
| | | }; |
| | |
| | | getLocationByIP |
| | | } from '../../../../utils/Robotaxi-public-method'; |
| | | import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE } from '../../../../robotaxi/constants/map'; |
| | | import { |
| | | isHomeStartSiteDisplayStateUsable, |
| | | isManualSelectedHomeStartSiteDisplayState, |
| | | readHomeStartSiteDisplayState |
| | | } from './use-home-start-site-display'; |
| | | |
| | | type Deps = { |
| | | mapState: any; |
| | |
| | | * 定位流程编排:不持有状态,仅组合 map/site/city 状态 hook 完成定位主链路。 |
| | | */ |
| | | export default function useLocationFlow({ mapState, siteState, cityState, isLoggedIn, onLocationSettled }: Deps) { |
| | | /** |
| | | * 解析当前城市下仍然有效的手动确认上车点。 |
| | | */ |
| | | const resolveLockedManualStartSite = (cityCodeArg?: string) => { |
| | | const preferredState = readHomeStartSiteDisplayState(); |
| | | if (!isManualSelectedHomeStartSiteDisplayState(preferredState)) { |
| | | return null; |
| | | } |
| | | if (!isHomeStartSiteDisplayStateUsable(preferredState, cityCodeArg)) { |
| | | return null; |
| | | } |
| | | return preferredState.site; |
| | | }; |
| | | |
| | | const updateByStartSite = async () => { |
| | | const startSite = Taro.getStorageSync('startSite'); |
| | | if (!(startSite && startSite.latitude && startSite.longitude)) return false; |
| | |
| | | ); |
| | | if (cityInfo?.code) { |
| | | const { isInside, newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code); |
| | | const lockedManualStartSite = resolveLockedManualStartSite(cityInfo.code); |
| | | const nearestCandidates = sites.length > 0 ? sites : newMarkers; |
| | | const { adsorbed, marker } = siteState.tryAdsorb(res.latitude, res.longitude, nearestCandidates); |
| | | const { adsorbed, marker } = lockedManualStartSite |
| | | ? { adsorbed: false, marker: null } |
| | | : siteState.tryAdsorb(res.latitude, res.longitude, nearestCandidates); |
| | | const finalLat = adsorbed && marker ? marker.latitude : res.latitude; // 最终纬度 |
| | | const finalLng = adsorbed && marker ? marker.longitude : res.longitude; // 最终经度 |
| | | onLocationSettled(finalLat, finalLng, !!isInside); |
| | |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | | import { getGeofence } from '../../../../robotaxi/api/home'; |
| | | import { GeofenceType } from '../../../../robotaxi/constants/geofence'; |
| | | import { resolveHomeStartSiteDisplaySite } from './use-home-start-site-display'; |
| | | |
| | | type Deps = { |
| | | /** 是否未登录 */ |
| | |
| | | return; |
| | | } |
| | | if (type == 'end') { |
| | | if (!deps.currentSite || !deps.currentSite.id) { |
| | | const preferredStartSite = resolveHomeStartSiteDisplaySite(); |
| | | const effectiveStartSite = preferredStartSite || deps.currentSite; |
| | | if (!effectiveStartSite || !effectiveStartSite.id) { |
| | | Taro.showToast({ title: '请先选择起点站点', icon: 'none' }); |
| | | return; |
| | | } |
| | | Taro.setStorageSync('startSite', deps.currentSite); |
| | | Taro.setStorageSync('startSite', effectiveStartSite); |
| | | Taro.navigateTo({ |
| | | url: `/robotaxi/pages/site-search/index?pageType=${type}&electronicAreaId=${effectiveStartSite.electronicAreaId}&longitude=${effectiveStartSite.longitude}&latitude=${effectiveStartSite.latitude}`, |
| | | success: () => { |
| | | reportEvent('RT_xcx_search_click', { element_type: type == 'start' ? 'start' : 'dest' }); |
| | | } |
| | | }); |
| | | return; |
| | | } |
| | | Taro.navigateTo({ |
| | | url: `/robotaxi/pages/site-search/index?pageType=${type}&electronicAreaId=${deps.electronicAreaId}&longitude=${deps.currentSite.longitude}&latitude=${deps.currentSite.latitude}`, |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE } from '../../../../robotaxi/constants/map'; |
| | | import { |
| | | HOME_START_SITE_SOURCE_MANUAL_SELECTED, |
| | | writeHomeStartSiteDisplayState |
| | | } from './use-home-start-site-display'; |
| | | |
| | | type Deps = { |
| | | mapState: any; |
| | |
| | | * 地图 region-change 编排:处理地图拖拽/缩放后的吸附、回填和自动归位。 |
| | | */ |
| | | export default function useRegionChange({ mapState, siteState, toOrigin, onRegionBegin, onRegionSettled }: Deps) { |
| | | /** |
| | | * 将用户显式换点后的吸附结果同步为首页确认上车点。 |
| | | */ |
| | | const syncManualSelectedStartSite = (site: any) => { |
| | | if (!site?.latitude || !site?.longitude) { |
| | | return; |
| | | } |
| | | writeHomeStartSiteDisplayState(site, HOME_START_SITE_SOURCE_MANUAL_SELECTED); |
| | | }; |
| | | |
| | | /** |
| | | * 按最终落点回传地图结算结果,保证场站围栏与吸附后的中心点一致。 |
| | | */ |
| | |
| | | const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates); |
| | | const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度 |
| | | const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度 |
| | | if (adsorbed && marker && (causedBy === 'gesture' || causedBy === 'drag' || wasReturningToOrigin)) { |
| | | syncManualSelectedStartSite(marker); |
| | | } |
| | | settleRegionResult({ lat: finalLat, lon: finalLng, isInside: true }); |
| | | if ((causedBy === 'gesture' || causedBy === 'drag' || wasReturningToOrigin) && marker) { |
| | | mapCtx.moveToLocation({ |
| | |
| | | OVERLAY_ANCHOR_OFFSET, |
| | | WAITING_RADAR_SRC |
| | | } from './constants'; |
| | | import { |
| | | HOME_START_SITE_SOURCE_MANUAL_SELECTED, |
| | | writeHomeStartSiteDisplayState |
| | | } from './hooks/use-home-start-site-display'; |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | |
| | | const [needLogin, setNeedLogin] = useState(false); // 当前是否需要登录 |
| | | const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 地图浮层锚点 |
| | | const [temperatureUnit, setTemperatureUnit] = useState(DEFAULT_TEMPERATURE_UNIT); // 温度单位配置 |
| | | const [preferredStartSite, setPreferredStartSite] = useState<any>(null); // 首页确认上车点 |
| | | |
| | | const mapState = useMapState(); // 地图领域状态 |
| | | const siteState = useSiteState(); // 站点吸附状态 |
| | |
| | | syncStationFenceVisibility, |
| | | ensureSelectedCityReady, |
| | | setCurrentSiteFromStart: setCurrentSite, |
| | | setPreferredStartSite, |
| | | firstEnterKey: FIRST_ENTER_KEY, |
| | | defaultLatitude: DEFAULT_LATITUDE, |
| | | defaultLongitude: DEFAULT_LONGITUDE, |
| | |
| | | setBounce(false); |
| | | setIsAdsorbed(true); |
| | | setCurrentSite(targetMarker); |
| | | setPreferredStartSite(targetMarker); |
| | | Taro.setStorageSync('startSite', targetMarker); |
| | | writeHomeStartSiteDisplayState(targetMarker, HOME_START_SITE_SOURCE_MANUAL_SELECTED); |
| | | setShowToolTip(true); |
| | | }, MARKER_TAP_SETTLE_DELAY); |
| | | }; |
| | |
| | | isOperatingArea={isOperatingArea} |
| | | isAdsorbed={isAdsorbed} |
| | | currentSite={currentSite} |
| | | preferredStartSite={preferredStartSite} |
| | | showQuickCapsule={showQuickCapsule} |
| | | quickCapsuleText={quickCapsuleText} |
| | | onSelectSite={toSelectSitePage} |
| | |
| | | /** |
| | | * DidShow 阶段入口。 |
| | | */ |
| | | const handleDidShow = (skipPageFlag?: string) => { |
| | | const handleDidShow = (skipPageFlag?: string, fromModifyDestination?: string) => { |
| | | ensurePageSocketReady(); |
| | | const isModifyDestinationSessionActive = !!Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY)?.active; // 修改目的地返回链路不提示“已有进行中订单” |
| | | if (skipPageFlag && !isModifyDestinationSessionActive) { |
| | | const isModifyDestinationReturn = String(fromModifyDestination || '') === '1'; |
| | | if (skipPageFlag && !isModifyDestinationReturn && !isModifyDestinationSessionActive) { |
| | | Taro.showToast({ |
| | | title: '您已有进行中的订单', |
| | | icon: 'none', |
| | |
| | | |
| | | useDidShow(() => { |
| | | setPageDidShowVersion((v) => v + 1); |
| | | handleDidShow(router.params?.skipPageFlag); |
| | | handleDidShow(router.params?.skipPageFlag, router.params?.fromModifyDestination); |
| | | updateMapViewportPadding(); |
| | | }); |
| | | |
| | |
| | | MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY, |
| | | MODIFY_DESTINATION_SESSION_KEY |
| | | } from '../../begin-route/constants/modify-destination'; |
| | | import { |
| | | HOME_START_SITE_SOURCE_MANUAL_SELECTED, |
| | | writeHomeStartSiteDisplayState |
| | | } from '../../../../pages/robotaxi/home/hooks/use-home-start-site-display'; |
| | | |
| | | const AUTO_SCALE_MAX = 17; // 自动缩放最大等级 |
| | | const AUTO_SCALE_MIN = 11; // 自动缩放最小等级 |
| | |
| | | } |
| | | } |
| | | Taro.setStorageSync('startSite', state.selectedSite); |
| | | writeHomeStartSiteDisplayState(state.selectedSite, HOME_START_SITE_SOURCE_MANUAL_SELECTED); |
| | | if (state.router.params.skipSource) { |
| | | Taro.navigateBack(); |
| | | return; |
| | |
| | | } else { |
| | | Taro.setStorageSync('endSite', state.selectedSite); |
| | | } |
| | | const returnToBeginRouteUrl = modifySession?.active |
| | | ? '/robotaxi/pages/begin-route/index?skipPageFlag=1&fromModifyDestination=1' |
| | | : (Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index'); |
| | | Taro.reLaunch({ |
| | | url: Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index', |
| | | url: returnToBeginRouteUrl, |
| | | }); |
| | | }; |
| | | |
| | |
| | | MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY, |
| | | MODIFY_DESTINATION_SESSION_KEY |
| | | } from '../begin-route/constants/modify-destination'; |
| | | import { |
| | | HOME_START_SITE_SOURCE_MANUAL_SELECTED, |
| | | writeHomeStartSiteDisplayState |
| | | } from '../../../pages/robotaxi/home/hooks/use-home-start-site-display'; |
| | | |
| | | /** |
| | | * 安全解析路由关键字,兼容已编码和未编码两种入参。 |
| | |
| | | } else { |
| | | Taro.setStorageSync('endSite', item) |
| | | } |
| | | const returnToBeginRouteUrl = modifySession?.active |
| | | ? '/robotaxi/pages/begin-route/index?skipPageFlag=1&fromModifyDestination=1' |
| | | : (Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index'); |
| | | Taro.reLaunch({ |
| | | url: Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index', |
| | | url: returnToBeginRouteUrl, |
| | | }) |
| | | } |
| | | |
| | |
| | | } |
| | | } catch { } |
| | | Taro.setStorageSync('startSite', item); |
| | | writeHomeStartSiteDisplayState(item, HOME_START_SITE_SOURCE_MANUAL_SELECTED); |
| | | Taro.navigateBack() |
| | | } else if (pageType == 'end') { |
| | | const otp = await checkOtp(false); |