import Taro, { useDidHide, useDidShow } from "@tarojs/taro"
|
import { useEffect, useState, useRef, useMemo } from 'react';
|
import { View, Map, Text, Image, Button } from '@tarojs/components';
|
import './index.less';
|
import PrivacyPopup from './components/privacy-popup';
|
import CustomerServicePopup from '../../../robotaxi/components/customer-service';
|
import OrderStatusCard from './components/order-status-card';
|
import MapOverlayTips from './components/map-overlay-tips';
|
import MapPointer from './components/map-pointer';
|
import selectCityIcon from '../../../robotaxi/assets/select-city.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';
|
import originIcon from '../../../robotaxi/assets/origin-icon.png'
|
import pointer from '../../../robotaxi/assets/pointer.png'
|
import rightIcon from '../../../robotaxi/assets/right-icon.png'
|
import wsManager from '../../../utils/websocket';
|
import {
|
filterRenderableGeofence,
|
formatGeofencePolygons,
|
getCityLocation,
|
mapOrderStatusToTripStatus,
|
safeParse,
|
getWeatherInfo
|
} from '../../../utils/Robotaxi-public-method';
|
import { _ROBOTAXI_WS_HOST, CDN } from '../../../constants/api';
|
// 城市切换状态同步
|
import useCitySwitch from './hooks/use-city-switch';
|
// 地图状态管理
|
import useMapState from './hooks/use-map-state';
|
// 站点与吸附状态管理
|
import useSiteState from './hooks/use-site-state';
|
// 城市状态管理
|
import useCityState from './hooks/use-city-state';
|
// 定位流程编排
|
import useLocationFlow from './hooks/use-location-flow';
|
// 地图区域变化编排
|
import useRegionChange from './hooks/use-region-change';
|
// 底部卡片拖拽编排
|
import useDragGesture from './hooks/use-drag-gesture';
|
// 页面导航与操作编排
|
import useNavigation from './hooks/use-navigation';
|
// 地图回中动作编排
|
import useOriginAction from './hooks/use-origin-action';
|
// 首页启动流程编排
|
import useHomeBootstrap from './hooks/use-home-bootstrap';
|
// 快捷胶囊编排
|
import useQuickCapsule from './hooks/use-quick-capsule';
|
// 附近车辆编排
|
import useNearbyVehicles from './hooks/use-nearby-vehicles';
|
import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE, FIRST_ENTER_KEY } from '../../../robotaxi/constants/map';
|
import { TripStatus, toTripStatus } from '../../../robotaxi/pages/begin-route/constants/trip-status';
|
import {
|
getGeofence,
|
getBannerList,
|
getAgreement,
|
getOrderActive,
|
getHotFixInfo
|
} from '../../../robotaxi/api/home';
|
import { getServicePhone } from '../../../robotaxi/api/customer-service';
|
import { getOrderDistance } from '../../../robotaxi/api/select-map-site';
|
import { checkOtp } from '../../../utils/tools';
|
|
type HomeTripStatus = TripStatus | 0;
|
|
export default function Home() {
|
const renderTailPreserveText = (text: string, tailCount = 3) => {
|
const safeText = String(text || '');
|
if (!safeText) return null;
|
if (safeText.length <= tailCount) {
|
return <Text className='to-site-fast-full'>{safeText}</Text>;
|
}
|
const headText = safeText.slice(0, -tailCount);
|
const tailText = safeText.slice(-tailCount);
|
return (
|
<>
|
<Text className='to-site-fast-head'>{headText}</Text>
|
<Text className='to-site-fast-tail'>{tailText}</Text>
|
</>
|
);
|
};
|
|
const [showPrivacy, setShowPrivacy] = useState(false); // 隐私政策弹窗是否显示
|
const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示
|
const mapState = useMapState(); // 地图状态容器
|
const siteState = useSiteState(); // 站点状态容器
|
const cityState = useCityState(); // 城市状态容器
|
const {
|
longitude,
|
latitude,
|
markers: siteMarkers,
|
polygons,
|
mapScale,
|
isOperatingArea,
|
electronicAreaId,
|
isLocationAuthDenied,
|
setLongitude,
|
setLatitude,
|
setPolygons,
|
setMapScale,
|
setIsLocationAuthDenied,
|
shouldIgnoreRegionChange,
|
isReturningToOrigin,
|
resetTimer,
|
updateMapData,
|
nearbyLoading
|
} = mapState; // 地图状态解构
|
const {
|
currentSite,
|
isAdsorbed,
|
showToolTip,
|
bounce,
|
adsorptionDistance,
|
setCurrentSite,
|
setCurrentAddress,
|
setIsAdsorbed,
|
setShowToolTip,
|
setBounce,
|
setAdsorptionDistance
|
} = siteState; // 站点状态解构
|
const { selectedCity, setSelectedCity, skipCityChangeEffectRef, ensureSelectedCityReady } = cityState; // 城市状态解构
|
const [isMaintain, setIsMaintain] = useState(false); // 是否维护中
|
const [maintainBounce, setMaintainBounce] = useState(false);
|
const [hotfixEndText, setHotfixEndText] = useState('');
|
const [tripStatus, setTripStatus] = useState<HomeTripStatus>(0); // 行程状态(0 表示无进行中行程)
|
const [bannerList, setBannerList] = useState<any[]>([]); // 轮播图列表
|
const [weatherData, setWeatherData] = useState<any>(null); // 天气数据
|
const [orderData, setOrderData] = useState<any>({}); // 订单数据
|
const [isLogin, setIsLogin] = useState(false); // 是否登录
|
const {
|
boxHeight,
|
originOpacity,
|
isDragging,
|
handleTouchStart,
|
handleTouchMove,
|
handleTouchEnd
|
} = useDragGesture({
|
bannerList,
|
tripStatus
|
}); // 拖拽手势编排
|
const {
|
skipNextLocationCenterRef,
|
lastSelectedCityCodeRef,
|
syncSelectedCityFromStorage,
|
consumeCitySwitchFlag
|
} = useCitySwitch(); // 切城同步编排
|
const orderStatusBound = useRef(false);
|
const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp';
|
const isAndroid = useMemo(() => {
|
try {
|
const info = Taro.getSystemInfoSync();
|
return info?.platform === 'android' || /android/i.test(info?.system || '');
|
} catch (error) {
|
return false;
|
}
|
}, []);
|
|
const ORDER_STATUS: Partial<Record<TripStatus, string>> = {
|
[TripStatus.VehicleApproaching]: '车辆正在赶来',
|
[TripStatus.VehicleArrivedPickup]: '车辆已到达上车点,请尽快前往',
|
[TripStatus.InService]: '正在前往目的地',
|
[TripStatus.ArrivedDestination]: '车辆已到达目的地,请尽快结束行程',
|
[TripStatus.TripFinished]: '行程已结束,请尽快支付'
|
}; // 订单状态文案映射
|
|
useEffect(() => {
|
let timer: any = null;
|
const check = () => {
|
getHotFixInfo().then((res: any) => {
|
const status = Number(res?.hotfixStatus);
|
if (status === 1) {
|
setIsMaintain(true);
|
setHotfixEndText(res?.hotfixEndTime);
|
} else {
|
setIsMaintain(false);
|
setHotfixEndText('');
|
}
|
}).catch(() => { });
|
};
|
check();
|
timer = setInterval(check, 1000 * 30);
|
return () => {
|
if (timer) clearInterval(timer);
|
};
|
}, []);
|
|
// 监听隐私政策显示状态
|
useEffect(() => {
|
getBannerList().then((res: any) => {
|
if (res && Array.isArray(res)) {
|
setBannerList(res);
|
}
|
}).catch(err => {
|
console.error('获取Banner列表失败', err);
|
});
|
|
getOrderDistance().then((res: any) => {
|
if (res && res.adsorptionDistance) {
|
setAdsorptionDistance(res.adsorptionDistance);
|
}
|
}).catch(err => {
|
console.error('获取吸附距离失败', err);
|
});
|
}, []);
|
|
// 监听订单状态变化
|
useEffect(() => {
|
const orderStatusHandler = (payload) => {
|
const mappedTripStatus = payload?.tripStatus; // ws 推送的行程状态
|
if (typeof mappedTripStatus === 'number') {
|
setTripStatus(mappedTripStatus === 0 ? 0 : toTripStatus(mappedTripStatus));
|
}
|
};
|
let openHandler: (() => void) | null = null;
|
const bindOrderStatus = () => {
|
if (orderStatusBound.current) return;
|
wsManager.on('order_status', orderStatusHandler);
|
orderStatusBound.current = true;
|
};
|
checkOtp(false).then((otp) => {
|
if (!otp) return;
|
if (wsManager.isConnectedStatus()) {
|
bindOrderStatus();
|
return;
|
}
|
openHandler = () => {
|
bindOrderStatus();
|
if (openHandler) {
|
wsManager.off('open', openHandler);
|
}
|
};
|
wsManager.on('open', openHandler);
|
});
|
return () => {
|
if (openHandler) {
|
wsManager.off('open', openHandler);
|
}
|
if (orderStatusBound.current) {
|
wsManager.off('order_status', orderStatusHandler);
|
orderStatusBound.current = false;
|
}
|
};
|
}, []);
|
|
// 监听城市变化,更新电子围栏
|
useEffect(() => {
|
if (skipCityChangeEffectRef.current) {
|
skipCityChangeEffectRef.current = false;
|
return;
|
}
|
if (!selectedCity.code) return;
|
const allowMapMove = !isLocationAuthDenied;
|
getGeofence({ cityCode: selectedCity.code }).then((res: any) => {
|
|
const geofenceList = filterRenderableGeofence(res);
|
if (geofenceList.length > 0) {
|
const newPolygons = formatGeofencePolygons(geofenceList);
|
setPolygons(newPolygons);
|
} else {
|
setPolygons([]);
|
if ((orderData || orderData?.id) && JSON.stringify(orderData) != '{}') return;
|
// 定位到城市中心
|
if (selectedCity.name && allowMapMove) {
|
getCityLocation(selectedCity.name).then(loc => {
|
skipNextLocationCenterRef.current = true;
|
shouldIgnoreRegionChange.current = true;
|
const mapCtx = Taro.createMapContext('map');
|
mapCtx.moveToLocation({
|
latitude: loc.latitude,
|
longitude: loc.longitude,
|
});
|
}).catch(err => {
|
console.error('获取城市中心失败', err);
|
});
|
}
|
}
|
}).catch(err => {
|
console.error('获取电子围栏失败', err);
|
setPolygons([]);
|
// 定位到城市中心
|
if (selectedCity.name && allowMapMove) {
|
getCityLocation(selectedCity.name).then(loc => {
|
skipNextLocationCenterRef.current = true;
|
shouldIgnoreRegionChange.current = true;
|
const mapCtx = Taro.createMapContext('map');
|
mapCtx.moveToLocation({
|
latitude: loc.latitude,
|
longitude: loc.longitude,
|
});
|
}).catch(err => {
|
console.error('获取城市中心失败', err);
|
});
|
}
|
});
|
}, [selectedCity.code, selectedCity.name, isLocationAuthDenied]);
|
|
const { getLocationInfo } = useLocationFlow({
|
mapState,
|
siteState,
|
cityState,
|
isLogin
|
}); // 定位流程编排
|
|
const handleClosePrivacy = () => {
|
setShowPrivacy(false);
|
};
|
|
const handleAgreePrivacy = () => {
|
// 处理用户同意逻辑
|
setShowPrivacy(false);
|
};
|
|
const { toOrigin } = useOriginAction({
|
isLocationAuthDenied,
|
shouldIgnoreRegionChange,
|
isReturningToOrigin,
|
setMapScale,
|
setLatitude,
|
setLongitude,
|
ensureSelectedCityReady,
|
lastSelectedCityCodeRef
|
}); // 地图回中动作
|
|
const bootstrap = useHomeBootstrap({
|
wsHost: _ROBOTAXI_WS_HOST,
|
getServicePhone,
|
syncSelectedCityFromStorage,
|
setSelectedCity,
|
setCurrentSite,
|
setCurrentAddress,
|
setIsAdsorbed,
|
setTripStatus,
|
checkOtp,
|
setIsLogin,
|
getAgreement,
|
setShowPrivacy,
|
getOrderActive,
|
setOrderData,
|
setIsLocationAuthDenied,
|
setMapScale,
|
setLatitude,
|
setLongitude,
|
getLocationInfo,
|
consumeCitySwitchFlag,
|
skipNextLocationCenterRef,
|
shouldIgnoreRegionChange,
|
getCityLocation,
|
updateMapData,
|
ensureSelectedCityReady,
|
setCurrentSiteFromStart: setCurrentSite,
|
firstEnterKey: FIRST_ENTER_KEY,
|
defaultLatitude: DEFAULT_LATITUDE,
|
defaultLongitude: DEFAULT_LONGITUDE,
|
toOrigin
|
}); // 首页启动编排
|
const {
|
quickCapsuleText,
|
showQuickCapsule,
|
refreshQuickCapsule,
|
handleQuickCapsuleClick
|
} = useQuickCapsule({
|
isGuest: isLogin,
|
checkOtp,
|
currentSite
|
}); // 快捷胶囊编排
|
|
useEffect(() => {
|
if (selectedCity && selectedCity.name) {
|
getWeatherInfo(selectedCity.name).then(res => {
|
console.log('天气天气', res);
|
if (res && res.weather && res.temperature) {
|
setWeatherData({
|
weather: res.weather.data,
|
temperature: res.temperature.data
|
});
|
}
|
}).catch(err => {
|
console.error('获取天气失败', err);
|
});
|
}
|
}, [selectedCity]);
|
|
useDidShow(() => {
|
bootstrap.onDidShow();
|
refreshQuickCapsule();
|
if (typeof latitude === 'number' && typeof longitude === 'number') {
|
tryStartPollingOnInit({
|
lat: latitude,
|
lon: longitude,
|
isInside: isOperatingArea
|
});
|
}
|
});
|
|
useDidHide(() => {
|
disposeVehicles();
|
});
|
|
const {
|
vehicleMarkers,
|
handleRegionBegin: handleVehicleRegionBegin,
|
handleRegionSettled: handleVehicleRegionSettled,
|
tryStartPollingOnInit,
|
disposeVehicles
|
} = useNearbyVehicles({
|
isOperatingArea
|
}); // 附近车辆编排
|
|
useEffect(() => {
|
if (nearbyLoading || isLocationAuthDenied) return;
|
if (typeof latitude !== 'number' || typeof longitude !== 'number') return;
|
tryStartPollingOnInit({
|
lat: latitude,
|
lon: longitude,
|
isInside: isOperatingArea
|
});
|
}, [nearbyLoading, isLocationAuthDenied, latitude, longitude, isOperatingArea, tryStartPollingOnInit]);
|
|
const mergedMapMarkers = useMemo(() => {
|
return [...siteMarkers, ...vehicleMarkers];
|
}, [siteMarkers, vehicleMarkers]); // 地图合并 marker 列表
|
|
const { handleRegionChange, queryPointerByCenter } = useRegionChange({
|
mapState,
|
siteState,
|
toOrigin,
|
onRegionBegin: handleVehicleRegionBegin,
|
onRegionSettled: handleVehicleRegionSettled
|
}); // 地图 region-change 编排
|
const {
|
ensureNetwork,
|
toSelectSitePage,
|
toCoupon,
|
toOrder,
|
toOrderDetail,
|
toSelectCityPage,
|
toAgreementPage,
|
toLogin,
|
openService,
|
handleOpenSetting,
|
handleViewOperatingArea,
|
handleBannerClick
|
} = useNavigation({
|
isGuest: isLogin,
|
showPrivacy,
|
setShowPrivacy,
|
isLocationAuthDenied,
|
electronicAreaId,
|
currentSite,
|
selectedCity,
|
polygons,
|
setPolygons,
|
setIsLocationAuthDenied,
|
setMapScale,
|
getLocationInfo,
|
queryPointerByCenter
|
}); // 首页导航与操作编排
|
|
const handleCloseCustomerService = () => {
|
setShowCustomerService(false);
|
};
|
|
// 处理点击地图 marker
|
const handleMarkerTap = (e: any) => {
|
if (isLocationAuthDenied) return;
|
const markerId = e?.detail?.markerId;
|
if (markerId === undefined) return;
|
|
const targetMarker = mergedMapMarkers.find(m => m.id === markerId);
|
if (targetMarker) {
|
if (targetMarker.markerType === 'vehicle') {
|
return;
|
}
|
// 忽略因调用 moveToLocation 引起的 region change,避免重复吸附计算
|
shouldIgnoreRegionChange.current = true;
|
|
// 先隐藏tooltip,并触发指针跳动动画
|
setShowToolTip(false);
|
setBounce(true);
|
|
// 直接更新经纬度实现跳转
|
setLatitude(targetMarker.latitude);
|
setLongitude(targetMarker.longitude);
|
|
// 等待指针动画(600ms)结束后,再展示tooltip和吸附状态
|
setTimeout(() => {
|
setBounce(false);
|
setIsAdsorbed(true);
|
setCurrentSite(targetMarker);
|
Taro.setStorageSync('startSite', targetMarker);
|
setShowToolTip(true);
|
}, 600);
|
}
|
};
|
|
return (
|
<View className="home-container">
|
<Map
|
id="map"
|
longitude={longitude ?? DEFAULT_LONGITUDE}
|
latitude={latitude ?? DEFAULT_LATITUDE}
|
scale={mapScale}
|
markers={isLocationAuthDenied ? [] : mergedMapMarkers.map(m => ({ ...m, title: '', name: '' }))}
|
polygons={polygons}
|
showLocation={!!longitude}
|
onRegionChange={handleRegionChange}
|
onMarkerTap={handleMarkerTap}
|
style={{ width: '100%', height: '85vh', flex: 'none' }}
|
>
|
{/* 选择城市 */}
|
<View className="select-city-container">
|
<View onClick={toSelectCityPage} className="select-city">
|
<Text>{selectedCity.name || '北京市'}</Text>
|
<Image className="select-city-icon" src={selectCityIcon} />
|
</View>
|
{weatherData && (
|
<View className="weather-info">
|
<Image className="weather-icon" src={getWeatherIconByDesc(weatherData.weather)} />
|
<Text>{weatherData.temperature}°C</Text>
|
</View>
|
)}
|
</View>
|
{/* 定位图标 */}
|
<View className="origin-box" style={{ opacity: originOpacity, transition: isDragging.current ? 'none' : 'opacity 0.2s ease-out' }}>
|
<Image onClick={toOrigin} className="origin-icon" src={originIcon} />
|
</View>
|
<MapOverlayTips
|
isGuest={Taro.getStorageSync('has_show_privicy_Robotaxi') != 'true' && !isLogin}
|
isLocationAuthDenied={isLocationAuthDenied}
|
isOperatingArea={isOperatingArea}
|
isMaintain={isMaintain}
|
maintainBounce={maintainBounce}
|
hotfixEndText={hotfixEndText}
|
onShowPrivacy={() => setShowPrivacy(true)}
|
onOpenSetting={handleOpenSetting}
|
onViewOperatingArea={handleViewOperatingArea}
|
/>
|
{/* 地图浮层入口 */}
|
<View className="map-entry">
|
<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>
|
<Image onClick={toOrder} className="map-entry-icon" src={suggest} />
|
</View>
|
<MapPointer
|
showToolTip={showToolTip}
|
bounce={bounce}
|
nearbyLoading={nearbyLoading}
|
isAndroid={isAndroid}
|
isLocationAuthDenied={isLocationAuthDenied}
|
isOperatingArea={isOperatingArea}
|
isAdsorbed={isAdsorbed}
|
currentSite={currentSite}
|
pointerSrc={pointer}
|
rightIconSrc={rightIcon}
|
waitingRadarSrc={waitingRadarSrc}
|
onSelectSite={(type) => toSelectSitePage(type)}
|
/>
|
</Map>
|
{/* 选择站点 */}
|
<View
|
className="content-box"
|
style={{ height: bannerList.length > 1 ? `${boxHeight}px` : `auto`, transition: isDragging.current ? 'none' : 'height 0.3s ease-out' }}
|
catchMove
|
onTouchStart={(e) => {
|
e.stopPropagation();
|
handleTouchStart(e);
|
}}
|
onTouchMove={(e) => {
|
e.stopPropagation();
|
handleTouchMove(e);
|
}}
|
onTouchEnd={(e) => {
|
e.stopPropagation();
|
handleTouchEnd();
|
}}
|
>
|
<View className="content-body">
|
<View className="select-site">
|
<Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' />
|
<View className="site-boarding-point">
|
{/* 起点 */}
|
<View onClick={() => toSelectSitePage('start', true)} className="now-site">
|
<View className="now-site-dot"></View>
|
<View className="now-site-address">
|
<View className="now-site-address-content">
|
从
|
<Text className="now-site-address-text">
|
{(!isOperatingArea || isAdsorbed) ? (currentSite.name || '请选择上车点') : '推荐上车点'}
|
</Text>
|
出发
|
</View>
|
<Image className="now-site-icon" src={rightIcon} />
|
</View>
|
</View>
|
{/* 终点 */}
|
<View onClick={() => toSelectSitePage('end')} className="to-site">
|
<View className="to-site-dot"></View>
|
<View className="to-site-address">
|
<View className="to-site-text">你想去哪?</View>
|
{showQuickCapsule && (
|
<View
|
onClick={(e) => {
|
e.stopPropagation();
|
handleQuickCapsuleClick();
|
}}
|
className="to-site-fast"
|
>
|
{renderTailPreserveText(quickCapsuleText)}
|
</View>
|
)}
|
</View>
|
</View>
|
</View>
|
<OrderStatusCard
|
tripStatus={tripStatus}
|
orderData={orderData}
|
onViewDetail={toOrderDetail}
|
orderStatusMap={ORDER_STATUS}
|
/>
|
{/* 运营活动 */}
|
{bannerList.length > 0 && (
|
<View className="activity">
|
{
|
bannerList.map((item, index) => (
|
<View key={item.id || index} className="activity-item" onClick={() => handleBannerClick(item)}>
|
<Image mode="aspectFill" className="activity-img" src={safeParse(item.imageUrl, [])?.[0]?.url} />
|
</View>
|
))
|
}
|
</View>
|
)}
|
{/* 产品名 */}
|
<View onClick={toAgreementPage} className="product-name">XPENG Robotaxi</View>
|
</View>
|
</View>
|
</View>
|
{
|
isMaintain && (
|
<View
|
onClick={() => {
|
setMaintainBounce(true);
|
setTimeout(() => setMaintainBounce(false), 300);
|
}}
|
style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', zIndex: 9999 }}
|
/>
|
)
|
}
|
{/* 是否登录 */}
|
{
|
isLogin &&
|
<View className={`login-bar ${isLogin ? '' : 'hide'}`}>
|
<View className='text'>立即登录,即可体验Robotaxi行程服务</View>
|
<View className='more'
|
onClick={toLogin}
|
>
|
<Button className='more-btn'>立即登录</Button>
|
<Image src={`${CDN}/common/arrow-white-new.png`} className='more-icon'></Image>
|
</View>
|
</View>
|
}
|
{/* 隐私政策弹窗 */}
|
<PrivacyPopup
|
show={showPrivacy}
|
onClose={handleClosePrivacy}
|
onAgree={handleAgreePrivacy}
|
/>
|
{/* 客服弹窗 */}
|
<CustomerServicePopup
|
show={showCustomerService}
|
onClose={handleCloseCustomerService}
|
/>
|
</View>
|
)
|
}
|