refactor(robotaxi): 优化地图缩放逻辑,修复地图抖动并更新蓝牙操作卡片
- 重构 useStationScaleFocus 钩子,替换强制地图缩放逻辑为缩放等级跟踪,优化场站围栏展示规则
- 为首页和选点页接入地图缩放回调,统一处理缩放事件同步
- 优化地图经纬度更新判断,仅坐标变化超阈值时更新状态,解决组件抖动问题
- 更新蓝牙操作卡片,使用本地图片替代远程资源,新增 authSuccess 属性控制图标显示
- 新增关闭蓝牙图标本地资源文件
| | |
| | | getMarkerCommitDelayMs?: () => number; |
| | | onRegionBegin?: () => void; |
| | | onRegionSettled?: (payload: { lat: number; lon: number; isInside: boolean }) => void; |
| | | onScaleChange?: (scale: number) => void; |
| | | }; |
| | | |
| | | /** |
| | | * 地图 region-change 编排:处理地图拖拽/缩放后的吸附、回填和自动归位。 |
| | | */ |
| | | export default function useRegionChange({ mapState, siteState, toOrigin, getMarkerCommitDelayMs, onRegionBegin, onRegionSettled }: Deps) { |
| | | export default function useRegionChange({ mapState, siteState, toOrigin, getMarkerCommitDelayMs, onRegionBegin, onRegionSettled, onScaleChange }: Deps) { |
| | | /** |
| | | * 查询完成后提交站点 marker:命中吸附时立即提交,未吸附时避让车辆动画窗口再提交。 |
| | | */ |
| | |
| | | toOrigin(); |
| | | }, 6000); |
| | | } |
| | | } else if (causedBy === 'gesture' || causedBy === 'scale') { |
| | | mapState.setMapScale(res.scale); |
| | | } |
| | | if (typeof onScaleChange === 'function') { |
| | | onScaleChange(res.scale); |
| | | } |
| | | } |
| | | }); |
| | |
| | | } |
| | | |
| | | if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') { |
| | | mapState.setLatitude(lat); |
| | | mapState.setLongitude(lng); |
| | | // 只有发生显著位移时才同步经纬度,避免微小抖动引发的二次重绘 |
| | | if (Math.abs(mapState.latitude - lat) > 0.0001 || Math.abs(mapState.longitude - lng) > 0.0001) { |
| | | mapState.setLatitude(lat); |
| | | mapState.setLongitude(lng); |
| | | } |
| | | } |
| | | |
| | | try { |
| | |
| | | syncSelectedCityFromStorage, |
| | | consumeCitySwitchFlag |
| | | } = useCitySwitch(); |
| | | const { displayPolygons, displayFencePolylines } = useStationScaleFocus({ |
| | | const { displayPolygons, displayFencePolylines, handleScaleChange } = useStationScaleFocus({ |
| | | currentSite: siteState.currentSite, |
| | | setMapScale: mapState.setMapScale, |
| | | originalPolygons: mapState.polygons, |
| | | originalFencePolylines: mapState.fencePolylines |
| | | }); // 场站缩放聚焦与围栏展示控制 |
| | |
| | | onRegionSettled: (payload) => { |
| | | handleVehicleRegionSettled(payload); |
| | | syncStationFenceVisibility(payload.lat, payload.lon, payload.isInside); |
| | | } |
| | | }, |
| | | onScaleChange: handleScaleChange |
| | | }); |
| | | const { |
| | | toSelectSitePage, |
| | |
| | | setBounce(false); |
| | | setIsAdsorbed(true); |
| | | setCurrentSite(normalizedTargetMarker); |
| | | syncStationFenceVisibility(normalizedTargetMarker.latitude, normalizedTargetMarker.longitude, isOperatingArea); |
| | | setPreferredStartSite(normalizedTargetMarker); |
| | | Taro.setStorageSync('startSite', normalizedTargetMarker); |
| | | writeConfirmRideSiteSourceType('start', RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE); |
| | |
| | | import { useEffect, useRef, useMemo } from 'react'; |
| | | import { useMemo, useState, useEffect, useRef, useCallback } from 'react'; |
| | | import { GEOFENCE_STATION_FILL_COLOR, GEOFENCE_STATION_STROKE_COLOR } from '../constants/geofence'; |
| | | |
| | | type Deps = { |
| | | /** 当前选中的站点对象 */ |
| | | currentSite: any; |
| | | /** 设置地图缩放等级的方法 */ |
| | | setMapScale: (scale: number | ((prev: number) => number)) => void; |
| | | /** 原始的多边形围栏列表 */ |
| | | originalPolygons: any[]; |
| | | /** 原始的虚线边框列表 */ |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 场站站点地图缩放与围栏展示聚焦控制 Hook。 |
| | | * 场站站点地图围栏展示聚焦控制 Hook。 |
| | | * |
| | | * 职责: |
| | | * 1. 当选中“场站内”的站点时,自动将地图缩放等级设置为 18,并仅展示场站围栏(隐藏蓝色运营区围栏)。 |
| | | * 2. 当选中非场站站点或取消选中时,恢复展示所有围栏。 |
| | | * 3. 使用 ref 记录最后一次处理的站点,避免用户手动缩放地图后被反复拉回。 |
| | | * 1. 当选中“场站内”的站点,并且发生地图放大操作使得缩放达到 17~18 级(即 >= 16.9)时,去掉运营区围栏蒙层,仅展示场站围栏。 |
| | | * 2. 移出场站时、选择其他站点时,或缩放比例小于 17 时,恢复展示所有围栏。 |
| | | */ |
| | | export default function useStationScaleFocus({ currentSite, setMapScale, originalPolygons, originalFencePolylines }: Deps) { |
| | | const lastHandledStationSiteIdRef = useRef<string>(''); |
| | | |
| | | export default function useStationScaleFocus({ currentSite, originalPolygons, originalFencePolylines }: Deps) { |
| | | const isStationSite = useMemo(() => { |
| | | if (!currentSite) return false; |
| | | return !!currentSite.stationId; |
| | | }, [currentSite]); |
| | | |
| | | const [shouldFocusFence, setShouldFocusFence] = useState(false); |
| | | const lastScaleRef = useRef<number | undefined>(undefined); |
| | | |
| | | useEffect(() => { |
| | | const stationId = currentSite?.stationId ? String(currentSite.stationId) : ''; |
| | | |
| | | // 如果当前选中的是场站子站点,并且是一个新的场站(或者第一次进来),则触发缩放 |
| | | if (isStationSite && stationId) { |
| | | if (lastHandledStationSiteIdRef.current !== stationId) { |
| | | // 通过微调强制触发 React 状态与原生组件属性的同步 |
| | | // 避免因为用户手动缩放地图但未同步到 React 状态,导致 setMapScale(18) 被认为没改变而不生效 |
| | | setMapScale(prev => { |
| | | if (prev === 18) return 17.99999; |
| | | if (prev === 17.99999) return 18; |
| | | return 18; |
| | | }); |
| | | lastHandledStationSiteIdRef.current = stationId; |
| | | if (!isStationSite) { |
| | | setShouldFocusFence(false); |
| | | } else { |
| | | // 刚选中场站时,重置一次参照物,避免拿上一次旧的值去对比 |
| | | lastScaleRef.current = undefined; |
| | | } |
| | | }, [isStationSite]); |
| | | |
| | | const handleScaleChange = useCallback((scale: number) => { |
| | | if (!isStationSite) { |
| | | lastScaleRef.current = scale; |
| | | return; |
| | | } |
| | | |
| | | if (lastScaleRef.current !== undefined) { |
| | | if (Math.abs(scale - lastScaleRef.current) > 0.001) { |
| | | // 如果是有效放大操作(规避细微浮点误差),并且达到了 17 级及以上 |
| | | if (scale - lastScaleRef.current > 0.001 && scale >= 16.9) { |
| | | setShouldFocusFence(true); |
| | | } |
| | | // 如果缩放比例掉到了 17 以下,恢复围栏 |
| | | if (scale < 16.9) { |
| | | setShouldFocusFence(false); |
| | | } |
| | | } |
| | | } else { |
| | | // 离开场站站点,清除记录 |
| | | lastHandledStationSiteIdRef.current = ''; |
| | | // 初始化第一次赋予初始值 |
| | | lastScaleRef.current = scale; |
| | | return; |
| | | } |
| | | }, [isStationSite, currentSite?.stationId, setMapScale]); |
| | | lastScaleRef.current = scale; |
| | | }, [isStationSite]); |
| | | |
| | | // 根据当前是否选中场站站点,过滤围栏数据 |
| | | // 根据当前是否选中场站站点且满足聚焦条件时,过滤围栏数据 |
| | | const displayPolygons = useMemo(() => { |
| | | if (!isStationSite) { |
| | | if (!shouldFocusFence) { |
| | | return originalPolygons; |
| | | } |
| | | // 选中场站时,仅保留场站的围栏(通过颜色或透明度特征区分,因为 format 后的多边形已经丢失了 type 属性) |
| | | // 选中场站且满足条件时,仅保留场站的围栏 |
| | | return originalPolygons.filter(polygon => polygon.fillColor === GEOFENCE_STATION_FILL_COLOR); |
| | | }, [isStationSite, originalPolygons]); |
| | | }, [shouldFocusFence, originalPolygons]); |
| | | |
| | | const displayFencePolylines = useMemo(() => { |
| | | if (!isStationSite) { |
| | | if (!shouldFocusFence) { |
| | | return originalFencePolylines; |
| | | } |
| | | // 选中场站时,仅保留场站的虚线边框(通常全都是场站的,这里保险起见通过颜色过滤) |
| | | // 选中场站且满足条件时,仅保留场站的虚线边框 |
| | | return originalFencePolylines.filter(polyline => polyline.color === GEOFENCE_STATION_STROKE_COLOR); |
| | | }, [isStationSite, originalFencePolylines]); |
| | | }, [shouldFocusFence, originalFencePolylines]); |
| | | |
| | | return { |
| | | displayPolygons, |
| | | displayFencePolylines, |
| | | isStationSite |
| | | isStationSite, |
| | | handleScaleChange |
| | | }; |
| | | } |
| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import closeBluetoothIcon from './close-bluetooth.png'; |
| | | |
| | | type Props = { |
| | | description: string; |
| | | actionText?: string; |
| | | onClick: () => void; |
| | | authSuccess?: boolean; |
| | | }; |
| | | |
| | | export default function BluetoothOperateCard({ description, actionText = '', onClick }: Props) { |
| | | const iconSrc = actionText === '去开启' |
| | | ? "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png" |
| | | export default function BluetoothOperateCard({ description, actionText = '', onClick, authSuccess = false }: Props) { |
| | | // 只有连接成功时才展示蓝色状态的图标 |
| | | const iconSrc = actionText === '去开启' || actionText === '去授权' || !authSuccess |
| | | ? closeBluetoothIcon |
| | | : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png"; |
| | | |
| | | return ( |
| | |
| | | description={bluetoothCardDescription} |
| | | actionText={bluetoothCardActionText} |
| | | onClick={handleBluetoothCardClick || (() => { })} |
| | | authSuccess={authSuccess} |
| | | /> |
| | | } |
| | | |
| | |
| | | /** |
| | | * 选点页业务编排:聚合状态容器与副作用 hooks,并对外提供稳定页面接口。 |
| | | */ |
| | | export default function useSelectMapSite() { |
| | | export default function useSelectMapSite(props?: { onScaleChange?: (scale: number) => void }) { |
| | | const state = useSelectMapSiteState(); // 选点页状态容器 |
| | | |
| | | /** |
| | |
| | | } |
| | | if (type !== 'end') return; |
| | | state.setBounce(true); |
| | | if (causedBy === 'gesture' || causedBy === 'drag') { |
| | | |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | // 获取真实的缩放等级并通过回调传递出去,彻底阻断与 mapScale 状态的绑定,避免触发原生组件抖动 |
| | | mapCtx.getScale({ |
| | | success: (res) => { |
| | | if (typeof props?.onScaleChange === 'function') { |
| | | props.onScaleChange(res.scale); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') { |
| | | state.pickupConfirmTypeRef.current = 'drag'; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.getCenterLocation({ |
| | | success: async (res) => { |
| | | const { latitude: lat, longitude: lng } = res; |
| | |
| | | resetStationSelection(); |
| | | state.setSelectedSite({}); |
| | | state.setOutOfServiceRange(getOutOfServiceRange(isInside, {})); |
| | | state.setLatitude(lat); |
| | | state.setLongitude(lng); |
| | | // 只有当坐标发生实质性变化才去更新 React 状态中的经纬度,避免频繁反推给原生组件导致抖动 |
| | | if (Math.abs(state.latitude - lat) > 0.0001 || Math.abs(state.longitude - lng) > 0.0001) { |
| | | state.setLatitude(lat); |
| | | state.setLongitude(lng); |
| | | } |
| | | syncPinLocation(lat, lng); |
| | | state.setShowToolTip(true); |
| | | } |
| | |
| | | * 地图选点页入口:负责 hook 编排与视图组装。 |
| | | */ |
| | | function SelectMapSite() { |
| | | const selectMapSite = useSelectMapSite(); |
| | | const handleScaleChangeRef = useRef<(scale: number) => void>(); |
| | | |
| | | const { displayPolygons, displayFencePolylines } = useStationScaleFocus({ |
| | | const selectMapSite = useSelectMapSite({ |
| | | onScaleChange: (scale) => handleScaleChangeRef.current?.(scale) |
| | | }); |
| | | |
| | | const { displayPolygons, displayFencePolylines, handleScaleChange } = useStationScaleFocus({ |
| | | currentSite: selectMapSite.selectedSite, |
| | | setMapScale: selectMapSite.setMapScale, |
| | | originalPolygons: selectMapSite.polygons, |
| | | originalFencePolylines: selectMapSite.mapPolylines |
| | | }); // 场站缩放聚焦与围栏展示控制 |
| | | |
| | | useEffect(() => { |
| | | handleScaleChangeRef.current = handleScaleChange; |
| | | }, [handleScaleChange]); |
| | | |
| | | const hasReportedPickupPageViewRef = useRef(false); |
| | | const hasReportedDropoffPageViewRef = useRef(false); |
| | | |