Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
| | |
| | | import MapOverlayTips from './components/map-overlay-tips'; |
| | | import MapPointer from './components/map-pointer'; |
| | | import selectCityIcon from '../../../robotaxi/assets/select-city.png' |
| | | import contactService from '../../../robotaxi/assets/contact-service.png' |
| | | import personalCenter from '../../../robotaxi/assets/personal-center.png' |
| | | import coupon from '../../../robotaxi/assets/coupon.png' |
| | | import suggest from '../../../robotaxi/assets/suggest.png' |
| | | import { getWeatherIconByDesc } from '../../../utils/weather'; |
| | |
| | | /> |
| | | {/* 地图浮层入口 */} |
| | | <View className="map-entry"> |
| | | <Image onClick={openService} className="map-entry-icon" src={contactService} /> |
| | | <Image onClick={openService} className="map-entry-icon" src={personalCenter} /> |
| | | <View className="split-line"></View> |
| | | <Image onClick={toCoupon} className="map-entry-icon" src={coupon} /> |
| | | <View className="split-line"></View> |
| | |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/vehicle/unlock', |
| | | method: 'POST', |
| | | payload |
| | | payload, |
| | | showToast: true, |
| | | skipException: true |
| | | }, true); |
| | | } |
| | | |
| | |
| | | onServiceEntry: () => void; |
| | | onCouponEntry: () => void; |
| | | onOrderEntry: () => void; |
| | | contactService: string; |
| | | personalCenter: string; |
| | | coupon: string; |
| | | suggest: string; |
| | | shouldShowOrigin: boolean; |
| | |
| | | onServiceEntry, |
| | | onCouponEntry, |
| | | onOrderEntry, |
| | | contactService, |
| | | personalCenter, |
| | | coupon, |
| | | suggest, |
| | | shouldShowOrigin, |
| | |
| | | <Image className='select-city-icon' src={backIcon} /> |
| | | </View> |
| | | <View className='map-entry'> |
| | | <Image onClick={onServiceEntry} className='map-entry-icon' src={contactService} /> |
| | | <Image onClick={onServiceEntry} className='map-entry-icon' src={personalCenter} /> |
| | | <View className='split-line'></View> |
| | | <Image onClick={onCouponEntry} className='map-entry-icon' src={coupon} /> |
| | | <View className='split-line'></View> |
| New file |
| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import bluetoothIcon from "@/assets/images/bluetooth-icon.png"; |
| | | import blueRightIcon from "@/assets/images/blue-right-icon.png"; |
| | | |
| | | export default function BluetoothOperateCard() { |
| | | |
| | | return ( |
| | | <View className="bluetooth-operate-card"> |
| | | <View className="bluetooth-operate-card-left"> |
| | | <Image src={bluetoothIcon} /> |
| | | <View>系统蓝牙未开启,无法自动解锁车辆</View> |
| | | </View> |
| | | <View className="bluetooth-operate-card-right"> |
| | | <View>去开启</View> |
| | | <Image src={blueRightIcon} /> |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |
| | |
| | | doorType, |
| | | ...locationPayload |
| | | }).then((res: any) => { |
| | | console.log(res,'***********'); |
| | | |
| | | if (!res) { |
| | | Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' }); |
| | | return; |
| | |
| | | } |
| | | } |
| | | }).catch(() => { |
| | | console.log(res,'***********'); |
| | | |
| | | Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' }); |
| | | }); |
| | | }; |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | .cancel { |
| | | flex-shrink: 0; |
| | | width: 144px; |
| | |
| | | mask-composite: exclude; |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | |
| | | .bluetooth-operate-card { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 16px 24px 16px 16px; |
| | | border-radius: 16px; |
| | | background: #FFFFFF; |
| | | |
| | | .bluetooth-operate-card-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | font-size: 24px; |
| | | font-weight: normal; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | |
| | | image { |
| | | flex-shrink: 0; |
| | | width: 24px; |
| | | height: 28px; |
| | | } |
| | | } |
| | | |
| | | .bluetooth-operate-card-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 11px; |
| | | font-size: 24px; |
| | | font-weight: normal; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: #6D7FD9; |
| | | |
| | | image { |
| | | flex-shrink: 0; |
| | | width: 11.97px; |
| | | height: 17.5px; |
| | | } |
| | | } |
| | | } |
| | |
| | | const cachedOrderId = Taro.getStorageSync('orderId'); |
| | | const cachedTripStatusStorage = Taro.getStorageSync('tripStatus'); |
| | | const cachedTripStatus = Number(cachedTripStatusStorage) || deps.getCurrentTripStatus(); |
| | | deps.setTripStatus(cachedTripStatus); |
| | | syncCachedOrderVehicle(cachedOrderId); |
| | | syncSitesFromActiveOrder(cachedTripStatusStorage, s, e); |
| | | if (cachedOrderId && [4, 5, 6, 7, 8, 9].includes(cachedTripStatus)) { |
| | |
| | | const [trunkStatus, setTrunkStatus] = useState(false); |
| | | const lastVehiclePointRef = useRef<{ latitude: number; longitude: number } | null>(null); |
| | | const lastVehicleUpdateAtRef = useRef<number>(0); |
| | | const translateDurationRef = useRef<number>(900); |
| | | |
| | | // 更新车辆位置并维护最近位置引用 |
| | | const updateVehicleLocationThrottled = (lat: number, lng: number, force = false) => { |
| | | const now = Date.now(); |
| | | const prevUpdateAt = lastVehicleUpdateAtRef.current; |
| | | if (prevUpdateAt > 0) { |
| | | // 基于推送间隔动态调整平移时长,适配 1-2 秒频率的轨迹推送 |
| | | const interval = now - prevUpdateAt; |
| | | translateDurationRef.current = Math.max(350, Math.min(1600, Math.floor(interval * 0.85))); |
| | | } |
| | | if (Number.isFinite(lat) && Number.isFinite(lng)) { |
| | | lastVehiclePointRef.current = { latitude: lat, longitude: lng }; |
| | | lastVehicleUpdateAtRef.current = now; |
| | |
| | | |
| | | useEffect(() => { |
| | | if (!statusRule.translateCar) return; |
| | | if (vehicleMoveAlongPoints.length > 1) return; |
| | | if (!vehicleLocation) return; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const timer = setTimeout(() => { |
| | |
| | | autoRotate: false, |
| | | moveWithRotate: true, |
| | | rotate: typeof vehicleDirection === 'number' ? ((vehicleDirection % 360) + 360) % 360 : 0, |
| | | duration: 100 |
| | | duration: translateDurationRef.current |
| | | }); |
| | | }, 100); |
| | | return () => clearTimeout(timer); |
| | | }, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId, vehicleMoveAlongPoints]); |
| | | |
| | | useEffect(() => { |
| | | if (!statusRule.translateCar) return; |
| | | if (vehicleMoveAlongPoints.length < 2) return; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const duration = Math.min(2000, Math.max(300, vehicleMoveAlongPoints.length * 100)); |
| | | const timer = setTimeout(() => { |
| | | mapCtx.moveAlong({ |
| | | markerId: vehicleMarkerId, |
| | | path: vehicleMoveAlongPoints, |
| | | autoRotate: true, |
| | | duration |
| | | }); |
| | | }, 50); |
| | | return () => clearTimeout(timer); |
| | | }, [statusRule.translateCar, vehicleMoveAlongPoints, vehicleMarkerId]); |
| | | }, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId]); |
| | | |
| | | return { |
| | | vehicleLocation, |
| | |
| | | if (typeof mappedTripStatus !== 'number') { |
| | | return null; |
| | | } |
| | | const currentLocalStatus = Number(Taro.getStorageSync('tripStatus')); |
| | | if (currentLocalStatus > mappedTripStatus && currentLocalStatus !== 10) { |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | if (currentTripStatus > mappedTripStatus && currentTripStatus !== 10) { |
| | | return null; |
| | | } |
| | | deps.setTripStatus(mappedTripStatus); |
| | |
| | | } |
| | | }; |
| | | |
| | | const syncVehiclePointWithRoute = (points: any[], deps: UseWebSocketEventsDeps) => { |
| | | const resolveVehiclePoint = (points: any[], servedPoints: any[] = []) => { |
| | | const servedLastPoint = Array.isArray(servedPoints) && servedPoints.length > 0 |
| | | ? servedPoints[servedPoints.length - 1] |
| | | : null; |
| | | if (servedLastPoint && typeof servedLastPoint.latitude === 'number' && typeof servedLastPoint.longitude === 'number') { |
| | | return servedLastPoint; |
| | | } |
| | | const firstPoint = points?.[0]; |
| | | if (!firstPoint || typeof firstPoint.latitude !== 'number' || typeof firstPoint.longitude !== 'number') { |
| | | if (firstPoint && typeof firstPoint.latitude === 'number' && typeof firstPoint.longitude === 'number') { |
| | | return firstPoint; |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const syncVehiclePointWithRoute = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => { |
| | | const targetPoint = resolveVehiclePoint(points, servedPoints); |
| | | if (!targetPoint) { |
| | | return; |
| | | } |
| | | deps.updateVehicleLocationThrottled(firstPoint.latitude, firstPoint.longitude, true); |
| | | deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); |
| | | }; |
| | | |
| | | const handleEraseRouteStatus = (points: any[], servedPoints: any[], deps: UseWebSocketEventsDeps) => { |
| | | const firstPoint = points?.[0]; |
| | | const targetPoint = resolveVehiclePoint(points, servedPoints); |
| | | if (deps.orderRouteInitializedRef.current && deps.currentFullRouteRef.current.length > 0) { |
| | | deps.erasePassedRouteSegments(points, servedPoints); |
| | | } else { |
| | |
| | | deps.applyRoutePoints(points); |
| | | }); |
| | | } |
| | | if (firstPoint && typeof firstPoint.latitude === 'number' && typeof firstPoint.longitude === 'number') { |
| | | if (targetPoint) { |
| | | setTimeout(() => { |
| | | deps.updateVehicleLocationThrottled(firstPoint.latitude, firstPoint.longitude, true); |
| | | deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); |
| | | }, 50); |
| | | } |
| | | }; |
| | | |
| | | const handleNormalRouteStatus = (points: any[], deps: UseWebSocketEventsDeps) => { |
| | | const handleNormalRouteStatus = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => { |
| | | Taro.nextTick(() => { |
| | | const updated = deps.applyRoutePoints(points); |
| | | if (!updated) return; |
| | | if (![4, 5, 6].includes(deps.tripStatusRef.current)) return; |
| | | syncVehiclePointWithRoute(points, deps); |
| | | syncVehiclePointWithRoute(points, deps, servedPoints); |
| | | }); |
| | | }; |
| | | |
| | | const handleForceFullRouteRedraw = (points: any[], deps: UseWebSocketEventsDeps) => { |
| | | const handleForceFullRouteRedraw = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => { |
| | | deps.setRoutePoints([]); |
| | | deps.setPolyline([]); |
| | | deps.orderRouteInitializedRef.current = false; |
| | |
| | | deps.currentFullRouteRef.current = [...points]; |
| | | deps.lastErasedIndexRef.current = 0; |
| | | deps.applyRoutePoints(points); |
| | | syncVehiclePointWithRoute(points, deps); |
| | | syncVehiclePointWithRoute(points, deps, servedPoints); |
| | | }); |
| | | }; |
| | | |
| | |
| | | deps.setVehicleMoveAlongPoints(servedPoints); |
| | | } |
| | | if (forceFullRedraw) { |
| | | handleForceFullRouteRedraw(points, deps); |
| | | handleForceFullRouteRedraw(points, deps, servedPoints); |
| | | return; |
| | | } |
| | | const isEraseStatus = [4, 6].includes(deps.tripStatusRef.current); |
| | |
| | | handleEraseRouteStatus(points, servedPoints, deps); |
| | | return; |
| | | } |
| | | handleNormalRouteStatus(points, deps); |
| | | handleNormalRouteStatus(points, deps, servedPoints); |
| | | }; |
| | | |
| | | export default function useWebSocketEvents(deps: UseWebSocketEventsDeps) { |
| | |
| | | import useViewDelegates from './hooks/use-view-delegates'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png' |
| | | import contactService from '../../../robotaxi/assets/contact-service.png' |
| | | import personalCenter from '../../../robotaxi/assets/personal-center.png' |
| | | import coupon from '../../../robotaxi/assets/coupon.png' |
| | | import suggest from '../../../robotaxi/assets/suggest.png' |
| | | import originIcon from '../../../robotaxi/assets/origin-icon.png' |
| | |
| | | const tripStatusRef = useRef(tripStatus); // 行程状态引用 |
| | | const lastTripStatusRef = useRef(tripStatus); // 上一次行程状态,用于检测状态变化 |
| | | const tripStatusForMapSyncRef = useRef(tripStatus); // 用于地图同步的行程状态引用 |
| | | useEffect(() => { |
| | | tripStatusRef.current = tripStatus; |
| | | tripStatusForMapSyncRef.current = tripStatus; |
| | | Taro.setStorageSync('tripStatus', tripStatus); |
| | | }, [tripStatus]); |
| | | // 行程状态显示规则配置:showPickup-显示上车点,showDropoff-显示下车点,showCar-显示车辆,showRoute-显示路线,showWalking-显示步行路线,viewport-视口类型,translateCar-车辆是否平移 |
| | | const STATUS_RULES: Record<number, any> = { |
| | | 1: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false }, |
| | |
| | | onServiceEntry={handleServiceEntry} |
| | | onCouponEntry={handleCouponEntry} |
| | | onOrderEntry={handleOrderEntry} |
| | | contactService={contactService} |
| | | personalCenter={personalCenter} |
| | | coupon={coupon} |
| | | suggest={suggest} |
| | | shouldShowOrigin={shouldShowOrigin} |
| | |
| | | import TipPopup from './components/tip-popup'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png' |
| | | import contactService from '../../../robotaxi/assets/contact-service.png' |
| | | import personalCenter from '../../../robotaxi/assets/personal-center.png' |
| | | import coupon from '../../../robotaxi/assets/coupon.png' |
| | | import suggest from '../../../robotaxi/assets/suggest.png' |
| | | import originIcon from '../../../robotaxi/assets/origin-icon.png' |
| | |
| | | </View> |
| | | {/* 地图浮层入口 */} |
| | | <View className="map-entry"> |
| | | <Image onClick={openService} className="map-entry-icon" src={contactService} /> |
| | | <Image onClick={openService} className="map-entry-icon" src={personalCenter} /> |
| | | <View className="split-line"></View> |
| | | <Image onClick={toCoupon} className="map-entry-icon" src={coupon} /> |
| | | <View className="split-line"></View> |