Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { formatGeofencePolygons, extractPointsFromPolygons, handleBannerJump, reportEvent } from '../../../../utils/Robotaxi-public-method'; |
| | | import { formatGeofencePolygons, extractPointsFromPolygons, handleBannerJump, reportEvent, filterRenderableGeofence } from '../../../../utils/Robotaxi-public-method'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | | import { getGeofence } from '../../../../robotaxi/api/home'; |
| | | |
| | |
| | | let nextPolygons = deps.polygons; |
| | | try { |
| | | const res: any = await getGeofence({ cityCode: deps.selectedCity.code }); |
| | | if (res && Array.isArray(res)) { |
| | | nextPolygons = formatGeofencePolygons(res); |
| | | const geofenceList = filterRenderableGeofence(res); |
| | | if (geofenceList.length > 0) { |
| | | nextPolygons = formatGeofencePolygons(geofenceList); |
| | | deps.setPolygons(nextPolygons); |
| | | } else { |
| | | nextPolygons = []; |
| | |
| | | import rightIcon from '../../../robotaxi/assets/right-icon.png' |
| | | import wsManager from '../../../utils/websocket'; |
| | | import { |
| | | filterRenderableGeofence, |
| | | formatGeofencePolygons, |
| | | getCityLocation, |
| | | mapOrderStatusToTripStatus, |
| | |
| | | const allowMapMove = !isLocationAuthDenied; |
| | | getGeofence({ cityCode: selectedCity.code }).then((res: any) => { |
| | | |
| | | if (res && Array.isArray(res) && res.length > 0) { |
| | | const newPolygons = formatGeofencePolygons(res); |
| | | const geofenceList = filterRenderableGeofence(res); |
| | | if (geofenceList.length > 0) { |
| | | const newPolygons = formatGeofencePolygons(geofenceList); |
| | | setPolygons(newPolygons); |
| | | } else { |
| | | setPolygons([]); |
| New file |
| | |
| | | import request from '../../utils/request'; |
| | | import { _ROBOTAXI_HOST } from '../../constants/api'; |
| | | |
| | | /** |
| | | * 保存用户已同意的订阅模板记录。 |
| | | */ |
| | | export const saveUserSubscribeMessage = async (payload: { templateIds: string[]; jscode: string }) => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/account/api/v1/mini-program/subscribeMessage/saveUserSubscribeMessage', |
| | | method: 'POST', |
| | | payload |
| | | }, true); |
| | | }; |
| New file |
| | |
| | | /** |
| | | * 围栏类型枚举。 |
| | | */ |
| | | export enum GeofenceType { |
| | | OPERATING = 1, |
| | | STATION = 2, |
| | | } |
| | | |
| | | export const GEOFENCE_TYPE_OPERATING = GeofenceType.OPERATING; // 运营区围栏类型常量 |
| | | export const GEOFENCE_TYPE_STATION = GeofenceType.STATION; // 场站围栏类型常量 |
| New file |
| | |
| | | import { View, Map, Text, Image, ScrollView } from '@tarojs/components'; |
| | | import CustomerServicePopup from '../../../components/customer-service'; |
| | | import TipPopup from '../components/tip-popup'; |
| | | import { formatTripDistance, formatTripDurationFromSeconds } from '../../../../utils/Robotaxi-public-method'; |
| | | |
| | | type Deps = { |
| | | longitude: number | null; |
| | | latitude: number | null; |
| | | mapScale: number; |
| | | markers: any[]; |
| | | mapPolylines: any[]; |
| | | polygons: any[]; |
| | | handleRegionChange: (e: any) => void; |
| | | handleMarkerTap: (e: any) => void; |
| | | toBack: () => void; |
| | | backIcon: string; |
| | | openService: () => void; |
| | | toCoupon: () => void; |
| | | toOrder: () => void; |
| | | personalCenter: string; |
| | | coupon: string; |
| | | suggest: string; |
| | | nearbyLoading: boolean; |
| | | isAndroid: boolean; |
| | | waitingRadarSrc: string; |
| | | showToolTip: boolean; |
| | | selectedSite: any; |
| | | selectionMode: 'point' | 'station'; |
| | | selectedStation: any; |
| | | stationSiteList: any[]; |
| | | outOfServiceRange: boolean; |
| | | tooltipBgSrc: string; |
| | | pageType: string; |
| | | bounce: boolean; |
| | | pointer: string; |
| | | toOrigin: () => void; |
| | | originIcon: string; |
| | | walkingDuration: number | null; |
| | | walkingDistance: number | null; |
| | | nearbySites: any[]; |
| | | handleCollect: (e: any) => void; |
| | | changeStationSite: (site: any) => void; |
| | | activeStar: string; |
| | | noStar: string; |
| | | confirmSite: () => void; |
| | | showCustomerService: boolean; |
| | | handleCloseCustomerService: () => void; |
| | | showTipPopup: boolean; |
| | | handleCloseTipPopup: () => void; |
| | | tipType: string; |
| | | }; |
| | | |
| | | /** |
| | | * 选点页视图组件:仅负责渲染地图与页面 UI,不包含业务状态编排。 |
| | | */ |
| | | export default function SelectMapSiteView(deps: Deps) { |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | | id="map" |
| | | longitude={deps.longitude} |
| | | latitude={deps.latitude} |
| | | scale={deps.mapScale} |
| | | markers={deps.markers.map((m) => ({ ...m, title: '', name: '' }))} |
| | | polyline={deps.mapPolylines} |
| | | polygons={deps.polygons} |
| | | showLocation={!!deps.longitude} |
| | | onRegionChange={deps.handleRegionChange} |
| | | onMarkerTap={deps.handleMarkerTap} |
| | | style={{ width: '100%', height: '100%' }} |
| | | > |
| | | {/* 返回 */} |
| | | <View onClick={deps.toBack} className="select-city"> |
| | | <Image className="select-city-icon" src={deps.backIcon} /> |
| | | </View> |
| | | {/* 地图浮层入口 */} |
| | | <View className="map-entry"> |
| | | <Image onClick={deps.openService} className="map-entry-icon" src={deps.personalCenter} /> |
| | | <View className="split-line"></View> |
| | | <Image onClick={deps.toCoupon} className="map-entry-icon" src={deps.coupon} /> |
| | | <View className="split-line"></View> |
| | | <Image onClick={deps.toOrder} className="map-entry-icon" src={deps.suggest} /> |
| | | </View> |
| | | {deps.nearbyLoading && deps.isAndroid && ( |
| | | <Image className="waiting-radar-img" src={deps.waitingRadarSrc} /> |
| | | )} |
| | | {/* 指针 */} |
| | | <View className="pointer-box"> |
| | | {deps.nearbyLoading && !deps.isAndroid && ( |
| | | <Image className="waiting-radar" src={deps.waitingRadarSrc} /> |
| | | )} |
| | | {((deps.showToolTip && deps.selectedSite.name) || deps.outOfServiceRange) && ( |
| | | <View className="toolTip"> |
| | | {(deps.outOfServiceRange || deps.selectedSite.time) && ( |
| | | <View className="toolTip-bg" style={{ backgroundImage: `url(${deps.tooltipBgSrc})` }}> |
| | | <Text className="toolTip-bg-text"> |
| | | {deps.outOfServiceRange ? '不在运营区' : `限停${deps.selectedSite.time}分钟`} |
| | | </Text> |
| | | </View> |
| | | )} |
| | | <View className="toolTip-txt"> |
| | | <View className="toolTip-txt-content"> |
| | | {deps.outOfServiceRange ? `请拖动至${deps.pageType == 'end' ? '下车' : '上车'}点` : deps.selectedSite.name} |
| | | </View> |
| | | </View> |
| | | </View> |
| | | )} |
| | | <Image |
| | | className={`pointer-icon ${deps.bounce ? 'bounce' : ''}`} |
| | | src={deps.pointer} |
| | | /> |
| | | </View> |
| | | <View className="container"> |
| | | <View className="container-inner"> |
| | | {/* 复位按钮 */} |
| | | <View className="origin-box"> |
| | | <Image onClick={deps.toOrigin} className="origin-icon" src={deps.originIcon} /> |
| | | </View> |
| | | <View catchMove onClick={(e) => e.stopPropagation()}> |
| | | {/* 确认站点 */} |
| | | <View className="content-box"> |
| | | <View className="content-box-item"> |
| | | <View className='content-box-item-title'> |
| | | <View className="content-title">{deps.selectionMode === 'station' ? deps.selectedStation?.name : (deps.pageType === 'start' ? '确认上车站点' : '确认目的地')}</View> |
| | | {deps.pageType === 'start' && deps.walkingDuration !== null && deps.walkingDistance !== null && ( |
| | | <View className="limit-tip"> |
| | | 从当前位置出发步行{formatTripDurationFromSeconds(deps.walkingDuration)}({formatTripDistance(deps.walkingDistance)})上车 |
| | | </View> |
| | | )} |
| | | {(deps.nearbySites.length > 0 && deps.pageType === 'end') && ( |
| | | <View className="limit-tip">点击其他选项或紫色原点,可更改{deps.pageType === 'start' ? '上车' : '下车'}点</View> |
| | | )} |
| | | </View> |
| | | {deps.selectionMode === 'station' && ( |
| | | <Image |
| | | onClick={deps.handleCollect} |
| | | className='collect-icon' |
| | | src={deps.selectedStation?.isCollect ? deps.activeStar : deps.noStar} |
| | | /> |
| | | )} |
| | | </View> |
| | | <View className='site-list-wrap'> |
| | | <ScrollView |
| | | className="site-list" |
| | | scrollY |
| | | enhanced |
| | | showScrollbar={true} |
| | | > |
| | | <View className='site-list-inner'> |
| | | {deps.selectionMode === 'station' ? ( |
| | | deps.stationSiteList.length > 0 ? deps.stationSiteList.map((item) => ( |
| | | <View |
| | | key={item.id || item.pointId || `${item.latitude}_${item.longitude}`} |
| | | className={`site-item ${String(item.id || item.pointId || '') === String(deps.selectedSite?.id || deps.selectedSite?.pointId || '') ? 'site-active' : ''}`} |
| | | onClick={() => deps.changeStationSite(item)} |
| | | > |
| | | <View>{item.name}</View> |
| | | </View> |
| | | )) : <View className="no-service">当前场站暂无可选站点</View> |
| | | ) : deps.selectedSite.name ? ( |
| | | <View className='site-item site-active'> |
| | | <View>{deps.selectedSite.name}</View> |
| | | {deps.selectionMode === 'point' && ( |
| | | <Image onClick={deps.handleCollect} className='collect-icon' src={deps.selectedSite.isCollect === 1 ? deps.activeStar : deps.noStar} /> |
| | | )} |
| | | </View> |
| | | ) : ( |
| | | <View className="no-service">超出服务范围,请在运营区内选择</View> |
| | | )} |
| | | </View> |
| | | </ScrollView> |
| | | </View> |
| | | <View onClick={deps.confirmSite} className={`site-confirm ${deps.selectedSite.name ? '' : 'no-confirm'}`}> |
| | | {deps.pageType === 'start' ? '确认上车站点' : '确认目的地站点'} |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </Map> |
| | | {/* 客服弹窗 */} |
| | | <CustomerServicePopup |
| | | show={deps.showCustomerService} |
| | | onClose={deps.handleCloseCustomerService} |
| | | /> |
| | | {/* 提示弹窗 */} |
| | | <TipPopup |
| | | show={deps.showTipPopup} |
| | | onClose={deps.handleCloseTipPopup} |
| | | tipType={deps.tipType} |
| | | /> |
| | | </View> |
| | | ); |
| | | } |
| New file |
| | |
| | | import Taro from "@tarojs/taro"; |
| | | import { useMemo, useRef, useState } from 'react'; |
| | | |
| | | /** |
| | | * 选点页状态容器:仅维护页面状态、refs 与轻量派生数据,不承载业务副作用。 |
| | | */ |
| | | export default function useSelectMapSiteState() { |
| | | const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示 |
| | | const [longitude, setLongitude] = useState<number | null>(null); // 经度 |
| | | const [latitude, setLatitude] = useState<number | null>(null); // 纬度 |
| | | const [mapScale, setMapScale] = useState(14); // 地图缩放等级 |
| | | const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示 |
| | | const [bounce, setBounce] = useState(false); // 是否弹跳 |
| | | const [markers, setMarkers] = useState<any[]>([]); // 标记点列表 |
| | | const [pageType, setPageType] = useState('end'); // 起点start 终点end |
| | | const [selectedSite, setSelectedSite] = useState<any>({}); // 选择的站点 |
| | | const [selectionMode, setSelectionMode] = useState<'point' | 'station'>('point'); // 当前选择模式 |
| | | const [selectedStation, setSelectedStation] = useState<any>(null); // 当前选中的场站 |
| | | const [stationSiteList, setStationSiteList] = useState<any[]>([]); // 当前场站下站点列表 |
| | | const [showTipPopup, setShowTipPopup] = useState(false); // 提示弹窗是否显示 |
| | | const [tipType, setTipType] = useState(''); // 提示类型 1:上下车点近 2:距上车点远 |
| | | const [polygons, setPolygons] = useState<any[]>([]); // 地图围栏 |
| | | const [fencePolylines, setFencePolylines] = useState<any[]>([]); // 围栏虚线边框 |
| | | const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内 |
| | | const [nearbySites, setNearbySites] = useState<any[]>([]); // 附近站点列表 |
| | | const [distanceConfig, setDistanceConfig] = useState<any>({}); // 距离限制配置 |
| | | const [polyline, setPolyline] = useState<any[]>([]); // 步行路线 |
| | | const [userLongitude, setUserLongitude] = useState<number | null>(null); // 用户经度 |
| | | const [userLatitude, setUserLatitude] = useState<number | null>(null); // 用户纬度 |
| | | const [outOfServiceRange, setOutOfServiceRange] = useState(false); // 是否超出运营范围 |
| | | const [walkingDistance, setWalkingDistance] = useState<number | null>(null); // 步行距离 |
| | | const [walkingDuration, setWalkingDuration] = useState<number | null>(null); // 步行时长 |
| | | const tooltipBgUrl = require('../../../../robotaxi/assets/tooltip-bg.png'); // 提示背景图资源 |
| | | const [tooltipBgSrc, setTooltipBgSrc] = useState(tooltipBgUrl); // 提示背景图路径 |
| | | const [isAndroid, setIsAndroid] = useState(false); // 是否安卓机型 |
| | | const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp'; // 加载雷达图 |
| | | const [nearbyLoading, setNearbyLoading] = useState(false); // 附近站点加载态 |
| | | |
| | | const pendingNearbyRequestCountRef = useRef(0); // 待完成附近站点请求数 |
| | | const autoScaleLockedRef = useRef(false); // 自动缩放锁:仅初始化计算一次 |
| | | const stationMetaMapRef = useRef<Record<string, any>>({}); // 场站元数据映射(来自围栏) |
| | | const router = Taro.useRouter(); // 路由实例 |
| | | |
| | | const mapPolylines = useMemo(() => { |
| | | return [...fencePolylines, ...polyline]; |
| | | }, [fencePolylines, polyline]); // 地图折线集合(围栏边框 + 步行线) |
| | | |
| | | return { |
| | | showCustomerService, |
| | | setShowCustomerService, |
| | | longitude, |
| | | setLongitude, |
| | | latitude, |
| | | setLatitude, |
| | | mapScale, |
| | | setMapScale, |
| | | showToolTip, |
| | | setShowToolTip, |
| | | bounce, |
| | | setBounce, |
| | | markers, |
| | | setMarkers, |
| | | pageType, |
| | | setPageType, |
| | | selectedSite, |
| | | setSelectedSite, |
| | | selectionMode, |
| | | setSelectionMode, |
| | | selectedStation, |
| | | setSelectedStation, |
| | | stationSiteList, |
| | | setStationSiteList, |
| | | showTipPopup, |
| | | setShowTipPopup, |
| | | tipType, |
| | | setTipType, |
| | | polygons, |
| | | setPolygons, |
| | | fencePolylines, |
| | | setFencePolylines, |
| | | isOperatingArea, |
| | | setIsOperatingArea, |
| | | nearbySites, |
| | | setNearbySites, |
| | | distanceConfig, |
| | | setDistanceConfig, |
| | | polyline, |
| | | setPolyline, |
| | | userLongitude, |
| | | setUserLongitude, |
| | | userLatitude, |
| | | setUserLatitude, |
| | | outOfServiceRange, |
| | | setOutOfServiceRange, |
| | | walkingDistance, |
| | | setWalkingDistance, |
| | | walkingDuration, |
| | | setWalkingDuration, |
| | | tooltipBgUrl, |
| | | tooltipBgSrc, |
| | | setTooltipBgSrc, |
| | | isAndroid, |
| | | setIsAndroid, |
| | | waitingRadarSrc, |
| | | nearbyLoading, |
| | | setNearbyLoading, |
| | | pendingNearbyRequestCountRef, |
| | | autoScaleLockedRef, |
| | | stationMetaMapRef, |
| | | router, |
| | | mapPolylines, |
| | | }; |
| | | } |
| New file |
| | |
| | | const FENCE_TYPE_SITE = 2; // 场站围栏类型 |
| | | const FENCE_FILL_COLOR_TYPE_SITE = 'rgba(255, 255, 255, 0.3)'; // 场站围栏填充色 |
| | | const FENCE_STROKE_COLOR_TYPE_SITE = '#FFFFFF'; // 场站围栏边框色 |
| | | |
| | | /** |
| | | * 按围栏类型格式化地图覆盖物:type=2 使用白色填充与白色虚线边框。 |
| | | */ |
| | | export const formatGeofenceByType = (geofenceList: any[]) => { |
| | | if (!Array.isArray(geofenceList)) { |
| | | return { polygons: [], fencePolylines: [] }; |
| | | } |
| | | const polygons = geofenceList.map((item) => { |
| | | const type = Number(item?.type); |
| | | const points = Array.isArray(item?.points) ? item.points : []; |
| | | return { |
| | | points: points.map((p: any) => ({ |
| | | latitude: p.lat, |
| | | longitude: p.lng |
| | | })), |
| | | strokeWidth: type === FENCE_TYPE_SITE ? 1 : 2, |
| | | strokeColor: type === FENCE_TYPE_SITE ? FENCE_STROKE_COLOR_TYPE_SITE : '#A1B1FF', |
| | | fillColor: type === FENCE_TYPE_SITE ? FENCE_FILL_COLOR_TYPE_SITE : '#6d7fd933', |
| | | zIndex: 1 |
| | | }; |
| | | }); |
| | | |
| | | const fencePolylines = geofenceList.reduce((acc: any[], item: any, index: number) => { |
| | | const type = Number(item?.type); |
| | | if (type !== FENCE_TYPE_SITE || !Array.isArray(item?.points) || item.points.length < 2) { |
| | | return acc; |
| | | } |
| | | const linePoints = item.points.map((p: any) => ({ |
| | | latitude: p.lat, |
| | | longitude: p.lng |
| | | })); |
| | | // 虚线边框通过 polyline dottedLine 实现,首尾闭合形成围栏边界。 |
| | | acc.push({ |
| | | id: 900000 + index, |
| | | points: [...linePoints, linePoints[0]], |
| | | color: FENCE_STROKE_COLOR_TYPE_SITE, |
| | | width: 2, |
| | | dottedLine: true, |
| | | arrowLine: false |
| | | }); |
| | | return acc; |
| | | }, []); |
| | | |
| | | return { polygons, fencePolylines }; |
| | | }; |
| | | |
| | | /** |
| | | * 减少安卓端绘制点位,降低地图折线渲染压力。 |
| | | */ |
| | | export const reduceRoutePoints = (points: any[], maxCount: number) => { |
| | | if (!Array.isArray(points) || points.length <= maxCount) { |
| | | return points; |
| | | } |
| | | const step = Math.ceil(points.length / maxCount); |
| | | const reduced = []; |
| | | for (let i = 0; i < points.length; i += step) { |
| | | reduced.push(points[i]); |
| | | } |
| | | const last = points[points.length - 1]; |
| | | if (reduced[reduced.length - 1] !== last) { |
| | | reduced.push(last); |
| | | } |
| | | return reduced; |
| | | }; |
| | | |
| | | /** |
| | | * 从围栏数据提取场站元信息映射:stationId -> 场站名称/收藏状态。 |
| | | */ |
| | | export const buildStationMetaMap = (geofenceList: any[]) => { |
| | | const stationMetaMap: Record<string, any> = {}; |
| | | if (!Array.isArray(geofenceList)) return stationMetaMap; |
| | | geofenceList.forEach((item) => { |
| | | const stationId = item?.stationId; |
| | | if (!stationId) return; |
| | | stationMetaMap[String(stationId)] = { |
| | | stationId: String(stationId), |
| | | name: item?.name || '', |
| | | isCollect: !!item?.stationIsCollect, // 围栏返回的场站收藏字段:stationIsCollect(boolean) |
| | | }; |
| | | }); |
| | | return stationMetaMap; |
| | | }; |
| New file |
| | |
| | | import { useEffect } from 'react'; |
| | | import { getWalkingRoute } from '../../../../utils/Robotaxi-public-method'; |
| | | import { reduceRoutePoints } from './use-select-map-site-utils'; |
| | | |
| | | type Deps = { |
| | | pageType: string; |
| | | userLatitude: number | null; |
| | | userLongitude: number | null; |
| | | latitude: number | null; |
| | | longitude: number | null; |
| | | selectedSite: any; |
| | | isAndroid: boolean; |
| | | setPolyline: (value: any[]) => void; |
| | | setWalkingDistance: (value: number | null) => void; |
| | | setWalkingDuration: (value: number | null) => void; |
| | | }; |
| | | |
| | | /** |
| | | * 步行路线副作用:根据当前位置与上车点变化计算步行路线并渲染折线。 |
| | | */ |
| | | export default function useSelectMapSiteWalkingRoute(deps: Deps) { |
| | | useEffect(() => { |
| | | if (deps.pageType !== 'start') { |
| | | deps.setPolyline([]); |
| | | deps.setWalkingDistance(null); |
| | | deps.setWalkingDuration(null); |
| | | return; |
| | | } |
| | | const originLat = deps.userLatitude ?? deps.latitude; |
| | | const originLng = deps.userLongitude ?? deps.longitude; |
| | | if (!originLat || !originLng || !deps.selectedSite?.latitude || !deps.selectedSite?.longitude) { |
| | | deps.setPolyline([]); |
| | | deps.setWalkingDistance(null); |
| | | deps.setWalkingDuration(null); |
| | | return; |
| | | } |
| | | const origin = `${originLng},${originLat}`; |
| | | const destination = `${deps.selectedSite.longitude},${deps.selectedSite.latitude}`; |
| | | getWalkingRoute(origin, destination).then(({ points, distance, duration }) => { |
| | | deps.setWalkingDistance(distance); |
| | | deps.setWalkingDuration(duration); |
| | | if (!Array.isArray(points) || points.length === 0) { |
| | | deps.setPolyline([]); |
| | | return; |
| | | } |
| | | const safePoints = points.filter((item: any) => Number.isFinite(item?.latitude) && Number.isFinite(item?.longitude)); |
| | | const displayPoints = deps.isAndroid ? reduceRoutePoints(safePoints, 120) : safePoints; |
| | | if (displayPoints.length === 0) { |
| | | deps.setPolyline([]); |
| | | return; |
| | | } |
| | | deps.setPolyline([{ |
| | | points: displayPoints, |
| | | color: '#6D7FD9', |
| | | width: 6, |
| | | dottedLine: true, |
| | | arrowLine: false, |
| | | }]); |
| | | }).catch(() => { |
| | | deps.setPolyline([]); |
| | | }); |
| | | }, [ |
| | | deps.pageType, |
| | | deps.userLatitude, |
| | | deps.userLongitude, |
| | | deps.latitude, |
| | | deps.longitude, |
| | | deps.selectedSite?.latitude, |
| | | deps.selectedSite?.longitude, |
| | | deps.isAndroid |
| | | ]); |
| | | } |
| New file |
| | |
| | | import Taro from "@tarojs/taro"; |
| | | import { useEffect } from 'react'; |
| | | import { |
| | | findNearestMarker, |
| | | getAutoScaleBySites, |
| | | getDistance, |
| | | fetchMapData, |
| | | extractPointsFromPolygons, |
| | | preloadImageToTemp, |
| | | } from '../../../../utils/Robotaxi-public-method'; |
| | | import { getGeofence } from '../../../api/home'; |
| | | import { getOrderDistance } from '../../../api/select-map-site'; |
| | | import { collectPoint } from '../../../api/site-search'; |
| | | import { checkOtp } from '../../../../utils/tools'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | | import useSelectMapSiteState from './use-select-map-site-state'; |
| | | import useSelectMapSiteWalkingRoute from './use-select-map-site-walking-route'; |
| | | import { buildStationMetaMap, formatGeofenceByType } from './use-select-map-site-utils'; |
| | | |
| | | const AUTO_SCALE_MAX = 17; // 自动缩放最大等级 |
| | | const AUTO_SCALE_MIN = 11; // 自动缩放最小等级 |
| | | |
| | | /** |
| | | * 选点页业务编排:聚合状态容器与副作用 hooks,并对外提供稳定页面接口。 |
| | | */ |
| | | export default function useSelectMapSite() { |
| | | const state = useSelectMapSiteState(); // 选点页状态容器 |
| | | |
| | | /** |
| | | * 重置场站选择模式,回到普通站点模式。 |
| | | */ |
| | | const resetStationSelection = () => { |
| | | state.setSelectionMode('point'); |
| | | state.setSelectedStation(null); |
| | | state.setStationSiteList([]); |
| | | }; |
| | | |
| | | /** |
| | | * 根据当前选中站点判断是否进入场站模式,并设置默认站点与场站信息。 |
| | | */ |
| | | const applySelectionBySite = (site: any, candidateSites?: any[]) => { |
| | | const stationId = site?.stationId ? String(site.stationId) : ''; |
| | | if (!stationId) { |
| | | resetStationSelection(); |
| | | state.setSelectedSite(site || {}); |
| | | return site || {}; |
| | | } |
| | | const stationMeta = state.stationMetaMapRef.current[stationId]; |
| | | const sourceSites = Array.isArray(candidateSites) ? candidateSites : state.nearbySites; |
| | | const stationSites = sourceSites.filter((item: any) => String(item?.stationId || '') === stationId); |
| | | if (!stationMeta || stationSites.length === 0) { |
| | | resetStationSelection(); |
| | | state.setSelectedSite(site || {}); |
| | | return site || {}; |
| | | } |
| | | state.setSelectionMode('station'); |
| | | state.setSelectedStation(stationMeta); |
| | | state.setStationSiteList(stationSites); |
| | | const defaultStationSite = stationSites[0]; // 进入场站模式默认选中首个站点 |
| | | state.setSelectedSite(defaultStationSite); |
| | | return defaultStationSite; |
| | | }; |
| | | |
| | | /** |
| | | * 计算是否超出可选范围(运营区外或起终点电子围栏不一致)。 |
| | | */ |
| | | const getOutOfServiceRange = (inside: boolean, site: any) => { |
| | | const cachedStartSite = Taro.getStorageSync('startSite'); |
| | | const cachedEndSite = Taro.getStorageSync('endSite'); |
| | | const isCrossArea = (state.pageType === 'start' |
| | | ? (cachedEndSite?.electronicAreaId && site?.electronicAreaId && cachedEndSite.electronicAreaId !== site.electronicAreaId) |
| | | : (cachedStartSite?.electronicAreaId && site?.electronicAreaId && cachedStartSite.electronicAreaId !== site.electronicAreaId)); |
| | | return !inside || isCrossArea; |
| | | }; |
| | | |
| | | /** |
| | | * 更新地图业务数据:围栏内判断、站点列表、自动缩放。 |
| | | */ |
| | | const updateMapData = async (lat: number, lng: number) => { |
| | | const selectedCity = Taro.getStorageSync('selectedCity'); |
| | | state.pendingNearbyRequestCountRef.current += 1; |
| | | state.setNearbyLoading(true); |
| | | try { |
| | | const { isInside, newMarkers, sites } = await fetchMapData(lat, lng, selectedCity?.code); |
| | | state.setIsOperatingArea(isInside); |
| | | state.setMarkers(newMarkers); |
| | | state.setNearbySites(sites); |
| | | if (state.selectionMode === 'station' && state.selectedStation?.stationId) { |
| | | const nextStationSites = sites.filter((item: any) => String(item?.stationId || '') === String(state.selectedStation.stationId)); |
| | | state.setStationSiteList(nextStationSites); |
| | | } |
| | | if (!state.autoScaleLockedRef.current) { |
| | | const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers); |
| | | if (typeof autoScale === 'number') { |
| | | state.setMapScale((prev) => { |
| | | const nextScale = Math.max(AUTO_SCALE_MIN, Math.min(AUTO_SCALE_MAX, autoScale)); |
| | | if (Math.abs(prev - nextScale) < 0.01) return prev; |
| | | return nextScale; |
| | | }); |
| | | } |
| | | state.autoScaleLockedRef.current = true; |
| | | } |
| | | return { isInside, newMarkers, sites }; |
| | | } finally { |
| | | state.pendingNearbyRequestCountRef.current = Math.max(0, state.pendingNearbyRequestCountRef.current - 1); |
| | | if (state.pendingNearbyRequestCountRef.current === 0) { |
| | | state.setNearbyLoading(false); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 获取用户定位并作为地图中心点。 |
| | | */ |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }).then((res) => { |
| | | state.setLatitude(res.latitude); |
| | | state.setLongitude(res.longitude); |
| | | updateMapData(res.latitude, res.longitude).then(({ isInside }) => { |
| | | state.setOutOfServiceRange(getOutOfServiceRange(isInside, state.selectedSite)); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | /** |
| | | * 获取用户定位,仅用于步行路线起点计算。 |
| | | */ |
| | | const fetchUserLocation = () => { |
| | | Taro.getLocation({ type: 'gcj02' }).then((res) => { |
| | | state.setUserLatitude(res.latitude); |
| | | state.setUserLongitude(res.longitude); |
| | | }); |
| | | }; |
| | | |
| | | /** |
| | | * 返回当前位置。 |
| | | */ |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation(); |
| | | }; |
| | | |
| | | /** |
| | | * 地图区域变化时触发,结束后按阈值吸附到最近站点。 |
| | | */ |
| | | const handleRegionChange = (e: any) => { |
| | | const type = e?.type || e?.detail?.type; |
| | | const causedBy = e?.causedBy || e?.detail?.causedBy; |
| | | if (type === 'begin') { |
| | | state.setShowToolTip(false); |
| | | return; |
| | | } |
| | | if (type !== 'end') return; |
| | | state.setBounce(true); |
| | | if (causedBy === 'gesture' || causedBy === 'drag') { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.getCenterLocation({ |
| | | success: async (res) => { |
| | | const { latitude: lat, longitude: lng } = res; |
| | | const { newMarkers, sites, isInside } = await updateMapData(lat, lng); |
| | | const { targetMarker, minDistance } = findNearestMarker(lat, lng, newMarkers); |
| | | if (targetMarker && minDistance < state.distanceConfig.adsorptionDistance) { |
| | | mapCtx.moveToLocation({ |
| | | latitude: targetMarker.latitude, |
| | | longitude: targetMarker.longitude, |
| | | }); |
| | | const activeSite = applySelectionBySite(targetMarker, sites); |
| | | state.setOutOfServiceRange(getOutOfServiceRange(isInside, activeSite)); |
| | | state.setLatitude(activeSite?.latitude || targetMarker.latitude); |
| | | state.setLongitude(activeSite?.longitude || targetMarker.longitude); |
| | | state.setShowToolTip(true); |
| | | } else { |
| | | resetStationSelection(); |
| | | state.setSelectedSite({}); |
| | | state.setOutOfServiceRange(getOutOfServiceRange(isInside, {})); |
| | | state.setLatitude(lat); |
| | | state.setLongitude(lng); |
| | | state.setShowToolTip(true); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | setTimeout(() => { |
| | | state.setBounce(false); |
| | | state.setShowToolTip(true); |
| | | }, 600); |
| | | }; |
| | | |
| | | /** |
| | | * 地图 marker 点击:直接聚焦并更新当前选中站点。 |
| | | */ |
| | | const handleMarkerTap = (e: any) => { |
| | | const markerId = e?.detail?.markerId; |
| | | if (markerId === undefined) return; |
| | | const targetMarker = state.markers.find((m) => m.id === markerId); |
| | | if (!targetMarker) return; |
| | | state.setShowToolTip(false); |
| | | state.setBounce(true); |
| | | state.setLatitude(targetMarker.latitude); |
| | | state.setLongitude(targetMarker.longitude); |
| | | setTimeout(() => { |
| | | state.setBounce(false); |
| | | const activeSite = applySelectionBySite(targetMarker, state.nearbySites); |
| | | state.setOutOfServiceRange(getOutOfServiceRange(state.isOperatingArea, activeSite)); |
| | | state.setLatitude(activeSite?.latitude || targetMarker.latitude); |
| | | state.setLongitude(activeSite?.longitude || targetMarker.longitude); |
| | | state.setShowToolTip(true); |
| | | }, 600); |
| | | }; |
| | | |
| | | /** |
| | | * 场站模式切换子站点:仅更新选中站点,不切换场站。 |
| | | */ |
| | | const changeStationSite = (site: any) => { |
| | | if (!site?.latitude || !site?.longitude) return; |
| | | state.setSelectedSite(site); |
| | | state.setLatitude(site.latitude); |
| | | state.setLongitude(site.longitude); |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation({ |
| | | latitude: site.latitude, |
| | | longitude: site.longitude, |
| | | }); |
| | | }; |
| | | |
| | | /** |
| | | * 关闭客服弹窗。 |
| | | */ |
| | | const handleCloseCustomerService = () => { |
| | | state.setShowCustomerService(false); |
| | | }; |
| | | |
| | | /** |
| | | * 打开客服电话拨打。 |
| | | */ |
| | | const openService = () => { |
| | | const phoneNumber = Taro.getStorageSync('customerServicePhone'); |
| | | Taro.makePhoneCall({ phoneNumber }); |
| | | }; |
| | | |
| | | /** |
| | | * 返回上一页。 |
| | | */ |
| | | const toBack = () => { |
| | | Taro.navigateBack({ delta: 1 }); |
| | | }; |
| | | |
| | | /** |
| | | * 确认选择站点(包含起终点距离与电子围栏校验)。 |
| | | */ |
| | | const confirmSite = async () => { |
| | | if (!state.selectedSite.name) { |
| | | Taro.showToast({ title: '请选择站点', icon: 'none' }); |
| | | return; |
| | | } |
| | | if (state.pageType === 'start') { |
| | | const endSiteCache = Taro.getStorageSync('endSite'); |
| | | if (endSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) { |
| | | Taro.showToast({ title: '起终点需在同一运营区域', icon: 'none' }); |
| | | return; |
| | | } |
| | | if (state.userLatitude && state.userLongitude) { |
| | | const distance = getDistance(state.userLatitude, state.userLongitude, state.selectedSite.latitude, state.selectedSite.longitude); |
| | | if (distance > state.distanceConfig.maxDistance) { |
| | | state.setTipType('2'); |
| | | state.setShowTipPopup(true); |
| | | return; |
| | | } |
| | | } |
| | | Taro.setStorageSync('startSite', state.selectedSite); |
| | | if (state.router.params.skipSource) { |
| | | Taro.navigateBack(); |
| | | return; |
| | | } |
| | | Taro.navigateBack({ delta: 2 }); |
| | | return; |
| | | } |
| | | const otp = await checkOtp(false); |
| | | if (!otp) { |
| | | jumpTo({ url: '/pages/user/login?hideBack=1' }); |
| | | return; |
| | | } |
| | | const startSiteCache = Taro.getStorageSync('startSite'); |
| | | if (startSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && startSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) { |
| | | Taro.showToast({ title: '起终点需在同一运营区域', icon: 'none' }); |
| | | return; |
| | | } |
| | | if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) { |
| | | const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, state.selectedSite.latitude, state.selectedSite.longitude); |
| | | if (distance < state.distanceConfig.minDistance) { |
| | | state.setTipType('1'); |
| | | state.setShowTipPopup(true); |
| | | return; |
| | | } |
| | | } |
| | | Taro.setStorageSync('endSite', state.selectedSite); |
| | | Taro.reLaunch({ |
| | | url: Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index', |
| | | }); |
| | | }; |
| | | |
| | | /** |
| | | * 关闭提示弹窗。 |
| | | */ |
| | | const handleCloseTipPopup = () => { |
| | | state.setShowTipPopup(false); |
| | | setTimeout(() => { |
| | | state.setTipType(''); |
| | | }, 600); |
| | | }; |
| | | |
| | | /** |
| | | * 跳转优惠券列表,未登录时先引导登录。 |
| | | */ |
| | | const toCoupon = async () => { |
| | | const otp = await checkOtp(false); |
| | | if (!otp) { |
| | | jumpTo({ url: '/pages/user/login?hideBack=1' }); |
| | | return; |
| | | } |
| | | Taro.navigateTo({ url: '/robotaxi/pages/coupon/index' }); |
| | | }; |
| | | |
| | | /** |
| | | * 跳转订单页,未登录时先引导登录。 |
| | | */ |
| | | const toOrder = async () => { |
| | | const otp = await checkOtp(false); |
| | | if (!otp) { |
| | | jumpTo({ url: '/pages/user/login?hideBack=1' }); |
| | | return; |
| | | } |
| | | Taro.navigateTo({ url: '/robotaxi/pages/order/index' }); |
| | | }; |
| | | |
| | | /** |
| | | * 收藏/取消收藏当前选中站点。 |
| | | */ |
| | | const handleCollect = async (e: any) => { |
| | | e.stopPropagation(); |
| | | try { |
| | | const isStationMode = state.selectionMode === 'station' && state.selectedStation?.stationId; |
| | | const collectSource = isStationMode ? state.selectedStation : state.selectedSite; |
| | | const type = isStationMode |
| | | ? (collectSource?.isCollect ? 2 : 1) |
| | | : (collectSource?.isCollect === 1 ? 2 : 1); |
| | | const payload: any = isStationMode |
| | | ? { itemType: 2, stationId: String(state.selectedStation.stationId), type } // 场站收藏只传 stationId |
| | | : { |
| | | itemType: 1, |
| | | latitude: state.selectedSite.latitude, |
| | | longitude: state.selectedSite.longitude, |
| | | type |
| | | }; |
| | | if (!isStationMode) { |
| | | if (state.selectedSite.pointId) { |
| | | payload.pointId = state.selectedSite.pointId; |
| | | } else if (state.selectedSite.id) { |
| | | payload.pointId = state.selectedSite.id; |
| | | } |
| | | } |
| | | await collectPoint(payload); |
| | | if (isStationMode) { |
| | | const nextStation = { ...state.selectedStation, isCollect: type === 1 }; |
| | | state.setSelectedStation(nextStation); |
| | | state.stationMetaMapRef.current[String(nextStation.stationId)] = nextStation; |
| | | } else { |
| | | const nextSite = { ...state.selectedSite, isCollect: type }; |
| | | state.setSelectedSite(nextSite); |
| | | if (state.pageType === 'start') { |
| | | Taro.setStorageSync('startSite', nextSite); |
| | | } |
| | | if (state.pageType === 'end') { |
| | | Taro.setStorageSync('endSite', nextSite); |
| | | } |
| | | } |
| | | Taro.showToast({ |
| | | title: type === 2 ? '已取消收藏' : '收藏成功', |
| | | icon: 'none' |
| | | }); |
| | | } catch (error) { |
| | | console.log('收藏操作失败', error); |
| | | } |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | state.setOutOfServiceRange(getOutOfServiceRange(state.isOperatingArea, state.selectedSite)); |
| | | }, [state.isOperatingArea, state.pageType, state.selectedSite?.electronicAreaId]); |
| | | |
| | | useEffect(() => { |
| | | const systemInfo = Taro.getSystemInfoSync(); |
| | | state.setIsAndroid(systemInfo?.platform === 'android'); |
| | | const pType = state.router.params.pageType || 'end'; |
| | | state.setPageType(pType); |
| | | |
| | | let cachedSite: any = null; |
| | | if (pType === 'start') { |
| | | cachedSite = Taro.getStorageSync('startSite'); |
| | | } else if (pType === 'end') { |
| | | cachedSite = Taro.getStorageSync('endSite'); |
| | | if (!cachedSite) { |
| | | cachedSite = Taro.getStorageSync('startSite'); |
| | | } |
| | | } |
| | | |
| | | const hasCachedSite = !!(cachedSite && cachedSite.latitude && cachedSite.longitude); |
| | | if (hasCachedSite) { |
| | | state.setLatitude(cachedSite.latitude); |
| | | state.setLongitude(cachedSite.longitude); |
| | | applySelectionBySite(cachedSite); |
| | | updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside }) => { |
| | | state.setOutOfServiceRange(getOutOfServiceRange(isInside, cachedSite)); |
| | | }); |
| | | } |
| | | fetchUserLocation(); |
| | | |
| | | getOrderDistance().then((res: any) => { |
| | | if (res) { |
| | | state.setDistanceConfig({ |
| | | maxDistance: res.maxDistance, |
| | | minDistance: res.minDistance, |
| | | adsorptionDistance: res.adsorptionDistance |
| | | }); |
| | | } |
| | | }).catch((err) => { |
| | | console.error('获取距离配置失败', err); |
| | | }); |
| | | |
| | | const selectedCity = Taro.getStorageSync('selectedCity'); |
| | | if (selectedCity && selectedCity.code) { |
| | | getGeofence({ cityCode: selectedCity.code }).then((res: any) => { |
| | | if (res && Array.isArray(res)) { |
| | | const { polygons: newPolygons, fencePolylines: nextFencePolylines } = formatGeofenceByType(res); |
| | | state.stationMetaMapRef.current = buildStationMetaMap(res); |
| | | state.setPolygons(newPolygons); |
| | | state.setFencePolylines(nextFencePolylines); |
| | | if (!hasCachedSite && res.length > 0 && res[0]?.points?.length > 0) { |
| | | const firstPoint = res[0].points[0]; |
| | | const lat = Number(firstPoint.lat); |
| | | const lng = Number(firstPoint.lng); |
| | | state.setLatitude(lat); |
| | | state.setLongitude(lng); |
| | | updateMapData(lat, lng).then(({ isInside }) => { |
| | | state.setOutOfServiceRange(getOutOfServiceRange(isInside, {})); |
| | | }); |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const points = extractPointsFromPolygons(newPolygons); |
| | | mapCtx.includePoints({ |
| | | points, |
| | | padding: [100, 100, 100, 100] |
| | | }); |
| | | } else if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | } else { |
| | | state.stationMetaMapRef.current = {}; |
| | | state.setPolygons([]); |
| | | state.setFencePolylines([]); |
| | | if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | } |
| | | }).catch((err) => { |
| | | console.error('获取电子围栏失败', err); |
| | | state.stationMetaMapRef.current = {}; |
| | | state.setPolygons([]); |
| | | state.setFencePolylines([]); |
| | | if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | }); |
| | | } else if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | let cancelled = false; |
| | | const run = async () => { |
| | | const local = await preloadImageToTemp(state.tooltipBgUrl); |
| | | if (!cancelled) state.setTooltipBgSrc(local); |
| | | }; |
| | | run(); |
| | | return () => { cancelled = true; }; |
| | | }, []); |
| | | |
| | | useSelectMapSiteWalkingRoute({ |
| | | pageType: state.pageType, |
| | | userLatitude: state.userLatitude, |
| | | userLongitude: state.userLongitude, |
| | | latitude: state.latitude, |
| | | longitude: state.longitude, |
| | | selectedSite: state.selectedSite, |
| | | isAndroid: state.isAndroid, |
| | | setPolyline: state.setPolyline, |
| | | setWalkingDistance: state.setWalkingDistance, |
| | | setWalkingDuration: state.setWalkingDuration, |
| | | }); |
| | | |
| | | useEffect(() => { |
| | | if (state.selectionMode !== 'station') return; |
| | | if (!state.selectedStation?.stationId) return; |
| | | const stationId = String(state.selectedStation.stationId); |
| | | const nextStationSites = state.nearbySites.filter((item: any) => String(item?.stationId || '') === stationId); |
| | | state.setStationSiteList(nextStationSites); |
| | | if (!state.selectedSite?.stationId || String(state.selectedSite.stationId) !== stationId) { |
| | | if (nextStationSites.length > 0) { |
| | | state.setSelectedSite(nextStationSites[0]); |
| | | } |
| | | } |
| | | }, [state.nearbySites, state.selectionMode, state.selectedStation?.stationId]); |
| | | |
| | | useEffect(() => { |
| | | const stationId = state.selectedSite?.stationId ? String(state.selectedSite.stationId) : ''; |
| | | if (!stationId || state.selectionMode === 'station') return; |
| | | if (!state.stationMetaMapRef.current[stationId]) return; |
| | | if (!Array.isArray(state.nearbySites) || state.nearbySites.length === 0) return; |
| | | applySelectionBySite(state.selectedSite, state.nearbySites); |
| | | }, [state.nearbySites, state.selectedSite?.stationId, state.selectionMode]); |
| | | |
| | | return { |
| | | longitude: state.longitude, |
| | | latitude: state.latitude, |
| | | mapScale: state.mapScale, |
| | | markers: state.markers, |
| | | mapPolylines: state.mapPolylines, |
| | | polygons: state.polygons, |
| | | handleRegionChange, |
| | | handleMarkerTap, |
| | | toBack, |
| | | openService, |
| | | toCoupon, |
| | | toOrder, |
| | | nearbyLoading: state.nearbyLoading, |
| | | isAndroid: state.isAndroid, |
| | | waitingRadarSrc: state.waitingRadarSrc, |
| | | showToolTip: state.showToolTip, |
| | | selectedSite: state.selectedSite, |
| | | selectionMode: state.selectionMode, |
| | | selectedStation: state.selectedStation, |
| | | stationSiteList: state.stationSiteList, |
| | | outOfServiceRange: state.outOfServiceRange, |
| | | tooltipBgSrc: state.tooltipBgSrc, |
| | | pageType: state.pageType, |
| | | bounce: state.bounce, |
| | | toOrigin, |
| | | walkingDuration: state.walkingDuration, |
| | | walkingDistance: state.walkingDistance, |
| | | nearbySites: state.nearbySites, |
| | | handleCollect, |
| | | changeStationSite, |
| | | confirmSite, |
| | | showCustomerService: state.showCustomerService, |
| | | handleCloseCustomerService, |
| | | showTipPopup: state.showTipPopup, |
| | | handleCloseTipPopup, |
| | | tipType: state.tipType, |
| | | }; |
| | | } |
| | |
| | | padding-bottom: 82px; |
| | | background: #FFFFFF; |
| | | |
| | | .content-box-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | .content-box-item-title { |
| | | flex: 1; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .collect-icon { |
| | | margin-top: 6px; |
| | | flex-shrink: 0; |
| | | width: 30px; |
| | | height: 28.64px; |
| | | } |
| | | } |
| | | |
| | | .content-limit-bg { |
| | | position: absolute; |
| | | top: 0; |
| | |
| | | } |
| | | } |
| | | |
| | | .site-list-wrap { |
| | | position: relative; |
| | | } |
| | | |
| | | .site-list { |
| | | // height: 240px; |
| | | overflow-y: auto; |
| | | max-height: 360px; |
| | | padding-right: 16px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .site-list-inner { |
| | | min-height: 100%; |
| | | } |
| | | |
| | | .site-item { |
| | |
| | | background: rgba(0, 0, 0, 0.15); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | import Taro from "@tarojs/taro" |
| | | import { useEffect, useRef, useState } from 'react'; |
| | | import { View, Map, Text, Image, ScrollView } from '@tarojs/components'; |
| | | import './index.less'; |
| | | import SelectMapSiteView from './components/select-map-site-view'; |
| | | import useSelectMapSite from './hooks/use-select-map-site'; |
| | | |
| | | import CustomerServicePopup from '../../components/customer-service'; |
| | | import TipPopup from './components/tip-popup'; |
| | | import backIcon from '../../../robotaxi/assets/back-icon.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'; |
| | | import pointer from '../../../robotaxi/assets/pointer.png'; |
| | | import activeStar from '../../../robotaxi/assets/active-star.png'; |
| | | import noStar from '../../../robotaxi/assets/no-star.png'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.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' |
| | | import pointer from '../../../robotaxi/assets/pointer.png' |
| | | import activeStar from '../../../robotaxi/assets/active-star.png' |
| | | import noStar from '../../../robotaxi/assets/no-star.png' |
| | | import { |
| | | findNearestMarker, |
| | | getAutoScaleBySites, |
| | | getDistance, |
| | | formatGeofencePolygons, |
| | | fetchMapData, |
| | | getWalkingRoute, |
| | | extractPointsFromPolygons, |
| | | preloadImageToTemp, |
| | | formatTripDistance, |
| | | formatTripDurationFromSeconds |
| | | } from '../../../utils/Robotaxi-public-method'; |
| | | import { getGeofence } from '../../api/home'; |
| | | import { getOrderDistance } from '../../api/select-map-site'; |
| | | import { collectPoint } from '../../api/site-search'; |
| | | import { checkOtp } from '../../../utils/tools'; |
| | | import { jumpTo } from '../../../utils/jump'; |
| | | |
| | | /** |
| | | * 地图选点页入口:负责 hook 编排与视图组装。 |
| | | */ |
| | | export default function SelectMapSite() { |
| | | const AUTO_SCALE_MAX = 17; // 自动缩放最大等级 |
| | | const AUTO_SCALE_MIN = 11; // 自动缩放最小等级 |
| | | |
| | | const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示 |
| | | const [longitude, setLongitude] = useState<number | null>(null); // 经度 |
| | | const [latitude, setLatitude] = useState<number | null>(null); // 纬度 |
| | | const [mapScale, setMapScale] = useState(14); // 地图缩放等级 |
| | | const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示 |
| | | const [bounce, setBounce] = useState(false); // 是否弹跳 |
| | | const [markers, setMarkers] = useState<any[]>([]); // 标记点列表 |
| | | const [pageType, setPageType] = useState('end'); // 起点start 终点end |
| | | const [selectedSite, setSelectedSite] = useState<any>({}); // 选择的站点 |
| | | const [showTipPopup, setShowTipPopup] = useState(false); // 提示弹窗是否显示 |
| | | const [tipType, setTipType] = useState(''); // 提示类型 1:上下车点近 2:距上车点远 |
| | | const [startSite, setStartSite] = useState<any>(null); // 上车点 |
| | | const [polygons, setPolygons] = useState<any[]>([]); // 地图围栏 |
| | | const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内 |
| | | const [nearbySites, setNearbySites] = useState<any[]>([]); // 附近站点列表 |
| | | const [distanceConfig, setDistanceConfig] = useState({}); // 距离限制配置 |
| | | const [polyline, setPolyline] = useState<any[]>([]); |
| | | const [userLongitude, setUserLongitude] = useState<number | null>(null); |
| | | const [userLatitude, setUserLatitude] = useState<number | null>(null); |
| | | const [outOfServiceRange, setOutOfServiceRange] = useState(false); |
| | | const [walkingDistance, setWalkingDistance] = useState<number | null>(null); |
| | | const [walkingDuration, setWalkingDuration] = useState<number | null>(null); |
| | | const tooltipBgUrl = require('../../../robotaxi/assets/tooltip-bg.png'); |
| | | const [tooltipBgSrc, setTooltipBgSrc] = useState(tooltipBgUrl); |
| | | const [isAndroid, setIsAndroid] = useState(false); |
| | | const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp'; |
| | | const [nearbyLoading, setNearbyLoading] = useState(false); |
| | | const pendingNearbyRequestCountRef = useRef(0); |
| | | const autoScaleLockedRef = useRef(false); // 自动缩放锁:仅初始化计算一次 |
| | | |
| | | const router = Taro.useRouter(); |
| | | |
| | | const getOutOfServiceRange = (inside: boolean, site: any) => { |
| | | const cachedStartSite = Taro.getStorageSync('startSite'); |
| | | const cachedEndSite = Taro.getStorageSync('endSite'); |
| | | const isCrossArea = (pageType === 'start' |
| | | ? (cachedEndSite?.electronicAreaId && site?.electronicAreaId && cachedEndSite.electronicAreaId !== site.electronicAreaId) |
| | | : (cachedStartSite?.electronicAreaId && site?.electronicAreaId && cachedStartSite.electronicAreaId !== site.electronicAreaId)); |
| | | return !inside || isCrossArea; |
| | | }; |
| | | |
| | | // 页面初始化 |
| | | useEffect(() => { |
| | | const systemInfo = Taro.getSystemInfoSync(); |
| | | setIsAndroid(systemInfo?.platform === 'android'); |
| | | const pType = router.params.pageType || 'end'; |
| | | setPageType(pType); |
| | | |
| | | // 获取对应缓存 |
| | | let cachedSite: any = null; |
| | | if (pType === 'start') { |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if (s) { |
| | | setStartSite(s); |
| | | cachedSite = s; |
| | | } |
| | | } else if (pType === 'end') { |
| | | cachedSite = Taro.getStorageSync('endSite'); |
| | | if (!cachedSite) { |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if (s) { |
| | | setStartSite(s); |
| | | cachedSite = s; |
| | | } |
| | | } |
| | | } |
| | | |
| | | const hasCachedSite = !!(cachedSite && cachedSite.latitude && cachedSite.longitude); |
| | | // 有缓存,优先使用缓存回显 |
| | | if (cachedSite && cachedSite.latitude && cachedSite.longitude) { |
| | | setLatitude(cachedSite.latitude); |
| | | setLongitude(cachedSite.longitude); |
| | | setSelectedSite(cachedSite); |
| | | updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside }) => { |
| | | setOutOfServiceRange(getOutOfServiceRange(isInside, cachedSite)); |
| | | }); |
| | | } |
| | | fetchUserLocation(); |
| | | |
| | | // 获取距离配置 |
| | | getOrderDistance().then((res: any) => { |
| | | if (res) { |
| | | setDistanceConfig({ |
| | | maxDistance: res.maxDistance, |
| | | minDistance: res.minDistance, |
| | | adsorptionDistance: res.adsorptionDistance |
| | | }); |
| | | } |
| | | }).catch(err => { |
| | | console.error('获取距离配置失败', err); |
| | | }); |
| | | |
| | | // 获取电子围栏 |
| | | const selectedCity = Taro.getStorageSync('selectedCity'); |
| | | if (selectedCity && selectedCity.code) { |
| | | getGeofence({ cityCode: selectedCity.code }).then((res: any) => { |
| | | if (res && Array.isArray(res)) { |
| | | const newPolygons = formatGeofencePolygons(res); |
| | | setPolygons(newPolygons); |
| | | if (!hasCachedSite && res.length > 0 && res[0]?.points?.length > 0) { |
| | | const firstPoint = res[0].points[0]; |
| | | const lat = Number(firstPoint.lat); |
| | | const lng = Number(firstPoint.lng); |
| | | setLatitude(lat); |
| | | setLongitude(lng); |
| | | updateMapData(lat, lng).then(({ isInside }) => { |
| | | setOutOfServiceRange(getOutOfServiceRange(isInside, {})); |
| | | }); |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | // mapCtx.moveToLocation({ |
| | | // latitude: lat, |
| | | // longitude: lng, |
| | | // }); |
| | | const points = extractPointsFromPolygons(newPolygons); |
| | | mapCtx.includePoints({ |
| | | points: points, |
| | | padding: [100, 100, 100, 100] |
| | | }); |
| | | } else if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | } else { |
| | | setPolygons([]); |
| | | if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | } |
| | | }).catch(err => { |
| | | console.error('获取电子围栏失败', err); |
| | | setPolygons([]); |
| | | if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | }); |
| | | } else if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | let cancelled = false; |
| | | const run = async () => { |
| | | const local = await preloadImageToTemp(tooltipBgUrl); |
| | | if (!cancelled) setTooltipBgSrc(local); |
| | | }; |
| | | run(); |
| | | return () => { cancelled = true; }; |
| | | }, []); |
| | | |
| | | // 更新地图数据 |
| | | const updateMapData = async (lat: number, lng: number) => { |
| | | const selectedCity = Taro.getStorageSync('selectedCity'); |
| | | pendingNearbyRequestCountRef.current += 1; |
| | | setNearbyLoading(true); |
| | | try { |
| | | const { isInside, electronicAreaId, newMarkers, sites } = await fetchMapData(lat, lng, selectedCity?.code); |
| | | |
| | | setIsOperatingArea(isInside); |
| | | setMarkers(newMarkers); |
| | | setNearbySites(sites); |
| | | if (!autoScaleLockedRef.current) { |
| | | const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers); |
| | | if (typeof autoScale === 'number') { |
| | | setMapScale((prev) => { |
| | | const nextScale = Math.max(AUTO_SCALE_MIN, Math.min(AUTO_SCALE_MAX, autoScale)); |
| | | if (Math.abs(prev - nextScale) < 0.01) return prev; |
| | | return nextScale; |
| | | }); |
| | | } |
| | | autoScaleLockedRef.current = true; |
| | | } |
| | | |
| | | return { isInside, newMarkers, sites }; |
| | | } finally { |
| | | pendingNearbyRequestCountRef.current = Math.max(0, pendingNearbyRequestCountRef.current - 1); |
| | | if (pendingNearbyRequestCountRef.current === 0) { |
| | | setNearbyLoading(false); |
| | | } |
| | | } |
| | | } |
| | | |
| | | useEffect(() => { |
| | | setOutOfServiceRange(getOutOfServiceRange(isOperatingArea, selectedSite)); |
| | | }, [isOperatingArea, pageType, selectedSite?.electronicAreaId]); |
| | | |
| | | // 获取用户位置信息 |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | updateMapData(res.latitude, res.longitude).then(({ isInside }) => { |
| | | setOutOfServiceRange(getOutOfServiceRange(isInside, selectedSite)); |
| | | }); |
| | | }) |
| | | } |
| | | |
| | | const fetchUserLocation = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setUserLatitude(res.latitude); |
| | | setUserLongitude(res.longitude); |
| | | }) |
| | | } |
| | | |
| | | // 返回当前位置 |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation(); |
| | | } |
| | | |
| | | // 地图区域变化时触发,判断是否吸附到最近的站点停靠点 |
| | | const handleRegionChange = (e: any) => { |
| | | const type = e?.type || e?.detail?.type; |
| | | // 获取触发源: gesture (手势), update (接口调用) |
| | | const causedBy = e?.causedBy || e?.detail?.causedBy; |
| | | |
| | | if (type === 'begin') { |
| | | setShowToolTip(false); |
| | | } else if (type === 'end') { |
| | | setBounce(true); |
| | | |
| | | // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环 |
| | | if (causedBy === 'gesture' || causedBy === 'drag') { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.getCenterLocation({ |
| | | success: async (res) => { |
| | | const { latitude: lat, longitude: lng } = res; |
| | | |
| | | const { newMarkers, isInside } = await updateMapData(lat, lng); |
| | | |
| | | const { targetMarker, minDistance } = findNearestMarker(lat, lng, newMarkers); |
| | | |
| | | // 如果最近的点在一定范围内(例如 200米),则吸附 |
| | | if (targetMarker && minDistance < distanceConfig.adsorptionDistance) { |
| | | // 使用 moveToLocation 实现吸附效果 |
| | | mapCtx.moveToLocation({ |
| | | latitude: targetMarker.latitude, |
| | | longitude: targetMarker.longitude, |
| | | }); |
| | | setSelectedSite(targetMarker); |
| | | setOutOfServiceRange(getOutOfServiceRange(isInside, targetMarker)); |
| | | setLatitude(targetMarker.latitude); |
| | | setLongitude(targetMarker.longitude); |
| | | setShowToolTip(true); |
| | | } else { |
| | | setSelectedSite({}); |
| | | setOutOfServiceRange(getOutOfServiceRange(isInside, {})); |
| | | setLatitude(lat); |
| | | setLongitude(lng); |
| | | setShowToolTip(true); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | setTimeout(() => { |
| | | setBounce(false); |
| | | setShowToolTip(true); |
| | | }, 600); |
| | | } |
| | | } |
| | | |
| | | const handleCloseCustomerService = () => { |
| | | setShowCustomerService(false); |
| | | }; |
| | | |
| | | const openService = () => { |
| | | const phoneNumber = Taro.getStorageSync('customerServicePhone'); |
| | | Taro.makePhoneCall({ |
| | | phoneNumber, |
| | | }) |
| | | } |
| | | |
| | | |
| | | const toBack = () => { |
| | | Taro.navigateBack({ |
| | | delta: 1, |
| | | }) |
| | | } |
| | | |
| | | const confirmSite = async () => { |
| | | if (!selectedSite.name) { |
| | | Taro.showToast({ |
| | | title: '请选择站点', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | if (pageType === 'start') { |
| | | const endSiteCache = Taro.getStorageSync('endSite'); |
| | | if (endSiteCache?.electronicAreaId && selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== selectedSite.electronicAreaId) { |
| | | Taro.showToast({ |
| | | title: '起终点需在同一运营区域', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | |
| | | // 校验当前位置与选择的上车点距离 |
| | | if (userLatitude && userLongitude) { |
| | | const distance = getDistance(userLatitude, userLongitude, selectedSite.latitude, selectedSite.longitude); |
| | | if (distance > distanceConfig.maxDistance) { |
| | | setTipType('2'); |
| | | setShowTipPopup(true); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | Taro.setStorageSync('startSite', selectedSite) |
| | | if (router.params.skipSource) { |
| | | Taro.navigateBack() |
| | | return |
| | | } |
| | | Taro.navigateBack({ |
| | | delta: 2, |
| | | }) |
| | | } |
| | | if (pageType === 'end') { |
| | | const otp = await checkOtp(false); |
| | | if (!otp) { |
| | | jumpTo({ url: '/pages/user/login?hideBack=1' }); |
| | | return |
| | | } |
| | | const startSiteCache = Taro.getStorageSync('startSite'); |
| | | if (startSiteCache?.electronicAreaId && selectedSite?.electronicAreaId && startSiteCache.electronicAreaId != selectedSite.electronicAreaId) { |
| | | Taro.showToast({ |
| | | title: '起终点需在同一运营区域', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | // 校验上车点与目的地的距离 |
| | | if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) { |
| | | const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, selectedSite.latitude, selectedSite.longitude); |
| | | if (distance < distanceConfig.minDistance) { |
| | | setTipType('1'); |
| | | setShowTipPopup(true); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | Taro.setStorageSync('endSite', selectedSite) |
| | | Taro.reLaunch({ |
| | | url: Taro.getStorageSync('orderId') ? `/robotaxi/pages/begin-route/index?skipPageFlag=1` : `/robotaxi/pages/begin-route/index`, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // 关闭提示弹窗 |
| | | const handleCloseTipPopup = () => { |
| | | setShowTipPopup(false); |
| | | setTimeout(() => { |
| | | setTipType(''); |
| | | }, 600); |
| | | } |
| | | |
| | | // 切换子站点 |
| | | const changeChildrenSite = (site: any) => { |
| | | setSelectedSite(site); |
| | | setLatitude(site.latitude); |
| | | setLongitude(site.longitude); |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation({ |
| | | latitude: site.latitude, |
| | | longitude: site.longitude, |
| | | }); |
| | | } |
| | | |
| | | const reducePoints = (points: any[], maxCount: number) => { |
| | | if (!Array.isArray(points) || points.length <= maxCount) { |
| | | return points; |
| | | } |
| | | const step = Math.ceil(points.length / maxCount); |
| | | const reduced = []; |
| | | for (let i = 0; i < points.length; i += step) { |
| | | reduced.push(points[i]); |
| | | } |
| | | const last = points[points.length - 1]; |
| | | if (reduced[reduced.length - 1] !== last) { |
| | | reduced.push(last); |
| | | } |
| | | return reduced; |
| | | } |
| | | |
| | | useEffect(() => { |
| | | if (pageType !== 'start') { |
| | | setPolyline([]); |
| | | setWalkingDistance(null); |
| | | setWalkingDuration(null); |
| | | return; |
| | | } |
| | | const originLat = userLatitude ?? latitude; |
| | | const originLng = userLongitude ?? longitude; |
| | | if (!originLat || !originLng || !selectedSite?.latitude || !selectedSite?.longitude) { |
| | | setPolyline([]); |
| | | setWalkingDistance(null); |
| | | setWalkingDuration(null); |
| | | return; |
| | | } |
| | | const origin = `${originLng},${originLat}`; |
| | | const destination = `${selectedSite.longitude},${selectedSite.latitude}`; |
| | | getWalkingRoute(origin, destination).then(({ points, distance, duration }) => { |
| | | setWalkingDistance(distance); |
| | | setWalkingDuration(duration); |
| | | if (!Array.isArray(points) || points.length === 0) { |
| | | setPolyline([]); |
| | | return; |
| | | } |
| | | const safePoints = points.filter((item: any) => Number.isFinite(item?.latitude) && Number.isFinite(item?.longitude)); |
| | | const displayPoints = isAndroid ? reducePoints(safePoints, 120) : safePoints; |
| | | if (displayPoints.length === 0) { |
| | | setPolyline([]); |
| | | return; |
| | | } |
| | | const line = [{ |
| | | points: displayPoints, |
| | | color: '#6D7FD9', |
| | | width: 6, |
| | | dottedLine: true, |
| | | arrowLine: false, |
| | | }]; |
| | | setPolyline(line); |
| | | }).catch(() => { |
| | | setPolyline([]); |
| | | }); |
| | | }, [pageType, userLatitude, userLongitude, latitude, longitude, selectedSite?.latitude, selectedSite?.longitude, isAndroid]); |
| | | |
| | | const toCoupon = async () => { |
| | | const otp = await checkOtp(false); |
| | | if (!otp) { |
| | | jumpTo({ url: '/pages/user/login?hideBack=1' }); |
| | | return |
| | | } |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/coupon/index', |
| | | }) |
| | | } |
| | | |
| | | const toOrder = async () => { |
| | | const otp = await checkOtp(false); |
| | | if (!otp) { |
| | | jumpTo({ url: '/pages/user/login?hideBack=1' }); |
| | | return |
| | | } |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/order/index', |
| | | }) |
| | | } |
| | | |
| | | // 处理点击地图 marker |
| | | const handleMarkerTap = (e: any) => { |
| | | const markerId = e?.detail?.markerId; |
| | | if (markerId === undefined) return; |
| | | |
| | | const targetMarker = markers.find(m => m.id === markerId); |
| | | if (targetMarker) { |
| | | // 先隐藏tooltip,并触发指针跳动动画 |
| | | setShowToolTip(false); |
| | | setBounce(true); |
| | | |
| | | // 直接更新经纬度实现跳转 |
| | | setLatitude(targetMarker.latitude); |
| | | setLongitude(targetMarker.longitude); |
| | | |
| | | // 等待指针动画(600ms)结束后,再展示tooltip和吸附状态 |
| | | setTimeout(() => { |
| | | setBounce(false); |
| | | setSelectedSite(targetMarker); |
| | | setOutOfServiceRange(getOutOfServiceRange(isOperatingArea, targetMarker)); |
| | | setShowToolTip(true); |
| | | }, 600); |
| | | } |
| | | }; |
| | | |
| | | const handleCollect = async (e: any) => { |
| | | e.stopPropagation(); |
| | | try { |
| | | const type = selectedSite.isCollect === 1 ? 2 : 1; // 1=收藏 2=取消收藏 |
| | | const payload: any = { |
| | | itemType: 1, // 默认为站点处理 |
| | | latitude: selectedSite.latitude, |
| | | longitude: selectedSite.longitude, |
| | | type: type |
| | | }; |
| | | |
| | | // 处理可能不同的 id 字段名 (id / pointId / stationId) |
| | | if (selectedSite.stationId) { |
| | | payload.stationId = selectedSite.stationId; |
| | | payload.itemType = 2; // 如果是场站 |
| | | } else if (selectedSite.pointId) { |
| | | payload.pointId = selectedSite.pointId; |
| | | } else if (selectedSite.id) { |
| | | payload.pointId = selectedSite.id; |
| | | } |
| | | |
| | | await collectPoint(payload); |
| | | |
| | | setSelectedSite({ ...selectedSite, isCollect: type }); |
| | | if (pageType === 'start') { |
| | | Taro.setStorageSync('startSite', { ...selectedSite, isCollect: type }); |
| | | } |
| | | if (pageType === 'end') { |
| | | Taro.setStorageSync('endSite', { ...selectedSite, isCollect: type }); |
| | | } |
| | | Taro.showToast({ |
| | | title: type === 2 ? '已取消收藏' : '收藏成功', |
| | | icon: 'none' |
| | | }); |
| | | } catch (error) { |
| | | console.log('收藏操作失败', error); |
| | | } |
| | | }; |
| | | const selectMapSite = useSelectMapSite(); |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | | id="map" |
| | | longitude={longitude} |
| | | latitude={latitude} |
| | | scale={mapScale} |
| | | markers={markers.map(m => ({ ...m, title: '', name: '' }))} |
| | | polyline={polyline} |
| | | polygons={polygons} |
| | | showLocation={!!longitude} |
| | | onRegionChange={handleRegionChange} |
| | | onMarkerTap={handleMarkerTap} |
| | | style={{ width: '100%', height: '100%' }} |
| | | > |
| | | {/* 返回 */} |
| | | <View onClick={toBack} className="select-city"> |
| | | <Image className="select-city-icon" src={backIcon} /> |
| | | </View> |
| | | {/* 地图浮层入口 */} |
| | | <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> |
| | | {nearbyLoading && isAndroid && ( |
| | | <Image className="waiting-radar-img" src={waitingRadarSrc} /> |
| | | )} |
| | | {/* 指针 */} |
| | | <View className="pointer-box"> |
| | | {nearbyLoading && !isAndroid && ( |
| | | <Image className="waiting-radar" src={waitingRadarSrc} /> |
| | | )} |
| | | {((showToolTip && selectedSite.name) || outOfServiceRange) && ( |
| | | <View className="toolTip"> |
| | | {(outOfServiceRange || selectedSite.time) && ( |
| | | <View className="toolTip-bg" style={{ backgroundImage: `url(${tooltipBgSrc})` }}> |
| | | <Text className="toolTip-bg-text">{outOfServiceRange ? `不在运营区` : `限停${selectedSite.time}分钟`}</Text> |
| | | </View> |
| | | )} |
| | | <View className="toolTip-txt"> |
| | | <View className="toolTip-txt-content">{outOfServiceRange ? `请拖动至${pageType == 'end' ? '下车' : '上车'}点` : selectedSite.name}</View> |
| | | </View> |
| | | </View> |
| | | )} |
| | | <Image |
| | | className={`pointer-icon ${bounce ? 'bounce' : ''}`} |
| | | src={pointer} |
| | | /> |
| | | </View> |
| | | <View className="container"> |
| | | <View className="container-inner"> |
| | | {/* 复位按钮 */} |
| | | <View className="origin-box"> |
| | | <Image onClick={toOrigin} className="origin-icon" src={originIcon} /> |
| | | </View> |
| | | <View catchMove onClick={(e) => e.stopPropagation()}> |
| | | {/* 确认站点 */}<View className="content-box"> |
| | | {/* <View className="content-limit-bg"></View> |
| | | <View className="limit-tip">目的地附近道路有<Text className="emphasis-txt">通行限制</Text>,为您推荐适合的下车位置</View> */} |
| | | { |
| | | true ? |
| | | <View className="content-title">{pageType === 'start' ? '确认上车站点' : '确认目的地'}</View> |
| | | : |
| | | <View className="content-title"> |
| | | <View>19号大院</View> |
| | | <Image className="collect-icon" src={activeStar} /> |
| | | </View> |
| | | } |
| | | { |
| | | pageType === 'start' && walkingDuration !== null && walkingDistance !== null && |
| | | <View className="limit-tip">从当前位置出发步行{formatTripDurationFromSeconds(walkingDuration)}({formatTripDistance(walkingDistance)})上车</View> |
| | | } |
| | | { |
| | | (nearbySites.length > 0 && pageType === 'end') && |
| | | <View className="limit-tip">点击其他选项或紫色原点,可更改{pageType === 'start' ? '上车' : '下车'}点</View> |
| | | } |
| | | <ScrollView className="site-list" scrollY> |
| | | {/* { |
| | | nearbySites.map(item => ( |
| | | <View onClick={() => changeChildrenSite(item)} key={item.id} className={`site-name ${selectedSite.id == item.id && 'site-active'}`}>{item.name}</View> |
| | | )) |
| | | } */} |
| | | { |
| | | selectedSite.name ? |
| | | true ? |
| | | <View className='site-item site-active'> |
| | | <View>{selectedSite.name}</View> |
| | | {/* <Image className="collect-icon" src={activeStar} /> */} |
| | | <Image onClick={handleCollect} className='collect-icon' src={selectedSite.isCollect === 1 ? activeStar : noStar} /> |
| | | </View> |
| | | : |
| | | [1, 2].map((item) => ( |
| | | <View key={item} className={`site-item ${item == 1 && 'site-active'}`}> |
| | | <View>{selectedSite.name}</View> |
| | | <Image className="collect-icon" src={activeStar} /> |
| | | </View> |
| | | )) |
| | | : |
| | | <View className="no-service">超出服务范围,请在运营区内选择</View> |
| | | } |
| | | </ScrollView> |
| | | <View onClick={confirmSite} className={`site-confirm ${selectedSite.name ? '' : 'no-confirm'}`} >{pageType === 'start' ? '确认上车站点' : '确认目的地站点'}</View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </Map> |
| | | {/* 客服弹窗 */} |
| | | <CustomerServicePopup |
| | | show={showCustomerService} |
| | | onClose={handleCloseCustomerService} |
| | | /> |
| | | {/* 提示弹窗 */} |
| | | <TipPopup |
| | | show={showTipPopup} |
| | | onClose={handleCloseTipPopup} |
| | | tipType={tipType} |
| | | /> |
| | | </View> |
| | | ) |
| | | <SelectMapSiteView |
| | | longitude={selectMapSite.longitude} |
| | | latitude={selectMapSite.latitude} |
| | | mapScale={selectMapSite.mapScale} |
| | | markers={selectMapSite.markers} |
| | | mapPolylines={selectMapSite.mapPolylines} |
| | | polygons={selectMapSite.polygons} |
| | | handleRegionChange={selectMapSite.handleRegionChange} |
| | | handleMarkerTap={selectMapSite.handleMarkerTap} |
| | | toBack={selectMapSite.toBack} |
| | | backIcon={backIcon} |
| | | openService={selectMapSite.openService} |
| | | toCoupon={selectMapSite.toCoupon} |
| | | toOrder={selectMapSite.toOrder} |
| | | personalCenter={personalCenter} |
| | | coupon={coupon} |
| | | suggest={suggest} |
| | | nearbyLoading={selectMapSite.nearbyLoading} |
| | | isAndroid={selectMapSite.isAndroid} |
| | | waitingRadarSrc={selectMapSite.waitingRadarSrc} |
| | | showToolTip={selectMapSite.showToolTip} |
| | | selectedSite={selectMapSite.selectedSite} |
| | | selectionMode={selectMapSite.selectionMode} |
| | | selectedStation={selectMapSite.selectedStation} |
| | | stationSiteList={selectMapSite.stationSiteList} |
| | | outOfServiceRange={selectMapSite.outOfServiceRange} |
| | | tooltipBgSrc={selectMapSite.tooltipBgSrc} |
| | | pageType={selectMapSite.pageType} |
| | | bounce={selectMapSite.bounce} |
| | | pointer={pointer} |
| | | toOrigin={selectMapSite.toOrigin} |
| | | originIcon={originIcon} |
| | | walkingDuration={selectMapSite.walkingDuration} |
| | | walkingDistance={selectMapSite.walkingDistance} |
| | | nearbySites={selectMapSite.nearbySites} |
| | | handleCollect={selectMapSite.handleCollect} |
| | | changeStationSite={selectMapSite.changeStationSite} |
| | | activeStar={activeStar} |
| | | noStar={noStar} |
| | | confirmSite={selectMapSite.confirmSite} |
| | | showCustomerService={selectMapSite.showCustomerService} |
| | | handleCloseCustomerService={selectMapSite.handleCloseCustomerService} |
| | | showTipPopup={selectMapSite.showTipPopup} |
| | | handleCloseTipPopup={selectMapSite.handleCloseTipPopup} |
| | | tipType={selectMapSite.tipType} |
| | | /> |
| | | ); |
| | | } |
| | |
| | | |
| | | const handleDelete = async (item: any) => { |
| | | try { |
| | | await collectPoint({ |
| | | type: 2, |
| | | pointId: item.pointId, |
| | | itemType: item.itemType, |
| | | latitude: item.latitude, |
| | | longitude: item.longitude, |
| | | }); |
| | | // 重新拉取数据或从列表中移除 |
| | | setList(prev => prev.filter(f => (f.id || f.pointId) !== (item.id || item.pointId))); |
| | | const currentItemType = Number(item?.itemType); // 删除项类型:保持原始 itemType(1=站点,2=场站) |
| | | const payload: any = { |
| | | type: 2, // 收藏接口动作:2=取消收藏 |
| | | itemType: currentItemType, // 传删除项 itemType |
| | | }; |
| | | |
| | | if (currentItemType === 2) { |
| | | payload.stationId = item?.stationId; // 场站删除传 stationId |
| | | } else { |
| | | payload.pointId = item?.pointId || item?.id; // 站点删除传 pointId |
| | | payload.latitude = item?.latitude; // 站点删除传纬度 |
| | | payload.longitude = item?.longitude; // 站点删除传经度 |
| | | } |
| | | |
| | | await collectPoint(payload); |
| | | setList(prev => prev.filter(f => (f.id || f.pointId || f.stationId) !== (item.id || item.pointId || item.stationId))); |
| | | Taro.showToast({ title: '已取消收藏', icon: 'none' }); |
| | | } catch (e) { |
| | | console.log('取消收藏失败', e); |
| | | // 兼容接口失败或格式不对时也能移除(仅供开发时预览效果) |
| | | setList(prev => prev.filter(f => (f.id || f.pointId) !== (item.id || item.pointId))); |
| | | Taro.showToast({ title: '取消收藏失败', icon: 'none' }); |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 点击收藏项时根据类型分流: |
| | | * itemType=2(场站) 取 children 第一个站点;children 为空则提示无有效站点。 |
| | | */ |
| | | const handleSelectItem = (item: any) => { |
| | | const itemType = Number(item?.itemType); |
| | | if (itemType === 2) { |
| | | const children = Array.isArray(item?.children) ? item.children : []; |
| | | if (children.length === 0) { |
| | | Taro.showToast({ |
| | | title: '此场站无有效站点', |
| | | icon: 'none', |
| | | }); |
| | | return; |
| | | } |
| | | const firstSite = children[0] || {}; |
| | | onSelect({ |
| | | ...firstSite, |
| | | stationId: item?.stationId || firstSite?.stationId, |
| | | }); |
| | | return; |
| | | } |
| | | onSelect(item); |
| | | }; |
| | | |
| | | const onScroll = (e) => { |
| | |
| | | > |
| | | <View className='favorite-list-inner'> |
| | | {list.map((item, index) => ( |
| | | <View key={item.id || index} className='favorite-item' onClick={() => onSelect(item)}> |
| | | <View key={item.id || index} className='favorite-item' onClick={() => handleSelectItem(item)}> |
| | | <View className='favorite-info'> |
| | | <View className='favorite-name'>{item.pointName}</View> |
| | | <View className='favorite-address'>{item.pointAddress}</View> |
| | |
| | | import { View, Image } from '@tarojs/components'; |
| | | import { useState, useEffect } from 'react'; |
| | | import { collectPoint, getCollectPoints } from '../../../api/site-search'; |
| | | import tUpIcon from '../../../assets/T-up.png'; |
| | | |
| | | type Props = { |
| | | // 站点数据 |
| | |
| | | onSelect: (item: any) => void; |
| | | // 高亮文本渲染器 |
| | | renderHighlightedText: (text: string, className: string) => any; |
| | | // 是否展示场站子站点信息 |
| | | showStationChildren?: boolean; |
| | | }; |
| | | |
| | | // 站点列表单项 |
| | |
| | | noStarSrc, |
| | | showDistance = true, |
| | | onSelect, |
| | | renderHighlightedText |
| | | renderHighlightedText, |
| | | showStationChildren = false |
| | | }: Props) { |
| | | const MAX_COLLECT_COUNT = 50; |
| | | const MAX_VISIBLE_STATION_CHILDREN = 4; // 场站模式默认最多展示子站点数量 |
| | | const [isCollected, setIsCollected] = useState(!!item.isCollected); |
| | | const [expandStationChildren, setExpandStationChildren] = useState(false); // 是否展开全部场站子站点 |
| | | |
| | | useEffect(() => { |
| | | setIsCollected(item.isCollect === 1); |
| | | }, [item.isCollect]); |
| | | |
| | | useEffect(() => { |
| | | setExpandStationChildren(false); |
| | | }, [item?.id, item?.pointId, item?.stationId]); |
| | | |
| | | const isStationItem = Number(item?.itemType) === 2; // 当前条目是否场站 |
| | | const stationChildren = Array.isArray(item?.children) ? item.children : []; // 场站子站点列表 |
| | | const shouldShowStationChildren = showStationChildren && isStationItem && stationChildren.length > 0; // 是否渲染场站子站点盒子 |
| | | const visibleStationChildren = expandStationChildren |
| | | ? stationChildren |
| | | : stationChildren.slice(0, MAX_VISIBLE_STATION_CHILDREN); |
| | | |
| | | /** |
| | | * 获取子站点展示名称。 |
| | | */ |
| | | const getStationChildName = (child: any) => { |
| | | return child?.name || child?.pointName || '--'; |
| | | }; |
| | | |
| | | /** |
| | | * 组装场站子站点选择结果,补齐 stationId。 |
| | | */ |
| | | const getSelectedStationChild = (child: any) => { |
| | | return { |
| | | ...child, |
| | | stationId: item?.stationId || child?.stationId |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 点击主列表项:场站默认选择首个子站点,普通站点按原逻辑选择。 |
| | | */ |
| | | const handleSelectMainItem = () => { |
| | | if (!isStationItem) { |
| | | onSelect(item); |
| | | return; |
| | | } |
| | | const firstSite = stationChildren[0]; |
| | | if (!firstSite) { |
| | | Taro.showToast({ |
| | | title: '此场站无有效站点', |
| | | icon: 'none' |
| | | }); |
| | | return; |
| | | } |
| | | onSelect(getSelectedStationChild(firstSite)); |
| | | }; |
| | | |
| | | /** |
| | | * 点击场站子站点:按子站点直接选择。 |
| | | */ |
| | | const handleSelectStationChild = (child: any) => { |
| | | onSelect(getSelectedStationChild(child)); |
| | | }; |
| | | |
| | | const checkCollectLimit = async () => { |
| | | // 只取总数即可,避免为校验拉完整列表 |
| | |
| | | } |
| | | } |
| | | |
| | | const currentItemType = Number(item?.itemType); // 当前条目类型(1=站点,2=场站) |
| | | const payload: any = { |
| | | itemType: 1, // 默认为站点处理 |
| | | latitude: item.latitude, |
| | | longitude: item.longitude, |
| | | itemType: currentItemType === 2 ? 2 : 1, // 收藏类型参数 |
| | | type: type |
| | | }; |
| | | |
| | | // 处理可能不同的 id 字段名 (id / pointId / stationId) |
| | | if (item.stationId) { |
| | | // 场站收藏:不传经纬度,仅传 stationId |
| | | if (currentItemType === 2) { |
| | | payload.stationId = item.stationId; |
| | | payload.itemType = 2; // 如果是场站 |
| | | } else if (item.pointId) { |
| | | // 站点收藏:传 pointId + 经纬度 |
| | | payload.pointId = item.pointId; |
| | | payload.latitude = item.latitude; |
| | | payload.longitude = item.longitude; |
| | | } else if (item.id) { |
| | | // 兼容 id 字段作为 pointId 的场景 |
| | | payload.pointId = item.id; |
| | | payload.latitude = item.latitude; |
| | | payload.longitude = item.longitude; |
| | | } |
| | | |
| | | await collectPoint(payload); |
| | |
| | | }; |
| | | |
| | | return ( |
| | | <View> |
| | | <View hoverClass='common-hover-class' onClick={() => onSelect(item)} className='history-item' key={itemKey}> |
| | | <View className='site-list-item'> |
| | | <View hoverClass='common-hover-class' onClick={handleSelectMainItem} className='history-item' key={itemKey}> |
| | | <Image className='history-icon' src={iconSrc}></Image> |
| | | <View className='history-info'> |
| | | {renderHighlightedText(item.name || '_', 'history-name')} |
| | |
| | | </View> |
| | | : null} |
| | | </View> |
| | | <View className='site-gate'> |
| | | <View>东门</View> |
| | | <View>北门</View> |
| | | <View>西门</View> |
| | | <View>南门</View> |
| | | </View> |
| | | {shouldShowStationChildren && ( |
| | | <View className='site-gate'> |
| | | {visibleStationChildren.map((child: any, index: number) => { |
| | | const isMoreSlot = !expandStationChildren |
| | | && stationChildren.length > MAX_VISIBLE_STATION_CHILDREN |
| | | && index === MAX_VISIBLE_STATION_CHILDREN - 1; |
| | | if (isMoreSlot) { |
| | | return ( |
| | | <View |
| | | hoverClass='common-hover-class' |
| | | key='station-more' |
| | | className='site-gate-item-more' |
| | | onClick={() => setExpandStationChildren(true)} |
| | | > |
| | | <View>更多</View> |
| | | <Image className='site-gate-more-icon' src={tUpIcon} /> |
| | | </View> |
| | | ); |
| | | } |
| | | return ( |
| | | <View |
| | | hoverClass='common-hover-class' |
| | | key={child?.id || child?.pointId || `${child?.latitude}_${child?.longitude}_${index}`} |
| | | onClick={() => handleSelectStationChild(child)} |
| | | > |
| | | {getStationChildName(child)} |
| | | </View> |
| | | ); |
| | | })} |
| | | </View> |
| | | )} |
| | | </View> |
| | | ); |
| | | } |
| | |
| | | margin-bottom: 32px; |
| | | padding: 0 24px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0px; |
| | | } |
| | | |
| | | .history-icon { |
| | | flex-shrink: 0; |
| | | width: 32px; |
| | |
| | | } |
| | | } |
| | | |
| | | .site-list-item:last-child { |
| | | .history-item { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .site-gate { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .site-gate { |
| | | margin-left: 72px; |
| | | margin-right: 24px; |
| | |
| | | text-align: center; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | word-break: break-word; |
| | | padding: 0 12px; |
| | | } |
| | | |
| | | .site-gate-item-more { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | column-gap: 8px; |
| | | } |
| | | |
| | | .site-gate-more-icon { |
| | | width: 16px; |
| | | height: 16px; |
| | | } |
| | | } |
| | | |
| | |
| | | width: 480px; |
| | | height: 400px; |
| | | } |
| | | } |
| | | } |
| | |
| | | ); |
| | | } |
| | | |
| | | const renderSiteItems = (list: any[], onSelect: (item: any) => void, showDistance = true) => { // 复用列表项渲染 |
| | | const renderSiteItems = ( |
| | | list: any[], |
| | | onSelect: (item: any) => void, |
| | | showDistance = true, |
| | | showStationChildren = false |
| | | ) => { // 复用列表项渲染 |
| | | return list.map((item, index) => ( |
| | | <SiteListItem |
| | | key={item.id || index} |
| | |
| | | showDistance={showDistance} |
| | | onSelect={onSelect} |
| | | renderHighlightedText={renderHighlightedText} |
| | | showStationChildren={showStationChildren} |
| | | /> |
| | | )); |
| | | }; |
| | |
| | | ? Taro.getStorageSync('endSite') |
| | | : null; |
| | | const filteredNearbyList = filterNearbyListByCurrentSite(nearbySiteList, currentSiteCache); |
| | | const fallbackNearbyList = filteredNearbyList.slice(0, MAX_EXPANDED_COUNT); // 兜底推荐列表(最多展示 MAX_EXPANDED_COUNT 条) |
| | | const recommendList = searchValue ? sortSitesByNameMatchAndDistance(siteList, searchValue) : filteredNearbyList; |
| | | const visibleHistory = showAllHistory ? historySite.slice(0, MAX_EXPANDED_COUNT) : historySite.slice(0, MAX_SHOW_COUNT); |
| | | const visibleRecommend = showAllRecommend ? recommendList.slice(0, MAX_EXPANDED_COUNT) : recommendList.slice(0, MAX_SHOW_COUNT); |
| | |
| | | <View className='card-header'>历史记录</View> |
| | | <View className='card-content list-content' id='history-card-content'> |
| | | { |
| | | renderSiteItems(visibleHistory, (item) => onChangeSelectSite(item), true) |
| | | renderSiteItems(visibleHistory, (item) => onChangeSelectSite(item), true, true) |
| | | } |
| | | </View> |
| | | {historySite.length > MAX_SHOW_COUNT && ( |
| | |
| | | <View className='card-header'>{searchValue ? '推荐站点' : '附近站点'}</View> |
| | | <View className='card-content list-content' id='recommend-card-content'> |
| | | { |
| | | renderSiteItems(visibleRecommend, (item) => onChangeSelectSite(item), true) |
| | | renderSiteItems(visibleRecommend, (item) => onChangeSelectSite(item), true, true) |
| | | } |
| | | </View> |
| | | {recommendList.length > MAX_SHOW_COUNT && ( |
| | |
| | | </View> |
| | | } |
| | | { |
| | | isInitLoaded && canShowSearchFallback && (!siteList.length && (!searchValue ? !nearbySiteList.length : true) && !nonOperationalSite.length && (!searchValue ? !historySite.length : true)) && |
| | | isInitLoaded && canShowSearchFallback && fallbackNearbyList.length > 0 && (!siteList.length && (!searchValue ? !nearbySiteList.length : true) && !nonOperationalSite.length && (!searchValue ? !historySite.length : true)) && |
| | | <View className='history-card'> |
| | | <View className='card-header'>暂无搜索结果,为您推荐附近站点</View> |
| | | <View className='card-content list-content'> |
| | | { |
| | | renderSiteItems(filteredNearbyList.slice(0, MAX_EXPANDED_COUNT), (item) => onChangeSelectSite(item), true) |
| | | renderSiteItems(fallbackNearbyList, (item) => onChangeSelectSite(item), true) |
| | | } |
| | | </View> |
| | | </View> |
| | |
| | | import { getFenceCheck, getNearestSite } from '../robotaxi/api/home'; |
| | | import { getOpenCity } from '../robotaxi/api/select-city'; |
| | | import { getStorage, getOrCreateLocalId } from './tools'; |
| | | import { GEOFENCE_TYPE_OPERATING } from '../robotaxi/constants/geofence'; |
| | | |
| | | /** |
| | | * 安全的 JSON 解析函数 |
| | |
| | | }); |
| | | }; |
| | | |
| | | /** |
| | | * 过滤可渲染的围栏数据,仅保留 type=1 的运营区围栏。 |
| | | */ |
| | | export const filterRenderableGeofence = (geofenceList: any[]) => { |
| | | if (!Array.isArray(geofenceList)) return []; |
| | | return geofenceList.filter((item) => Number(item?.type) === GEOFENCE_TYPE_OPERATING); |
| | | }; |
| | | |
| | | // 格式化电子围栏数据 |
| | | export const formatGeofencePolygons = (geofenceList: any[]) => { |
| | | if (!geofenceList || !Array.isArray(geofenceList)) { |
| | |
| | |
|
| | | import Taro from "@tarojs/taro";
|
| | | import { checkOtp } from "./tools";
|
| | | import { saveUserSubscribeMessage } from "../robotaxi/api/subscribe";
|
| | | import {
|
| | | OPERATION_COUPON_SUBSCRIBE_CONFIG,
|
| | | ORDER_SUCCESS_SUBSCRIBE_CONFIG,
|
| | |
| | | return `${sceneKey}_${bizId}`;
|
| | | }
|
| | |
|
| | | /**
|
| | | * 从订阅结果中提取用户同意(accept)的模板 ID。
|
| | | */
|
| | | function getAcceptedTemplateIds(tmplIds: string[], result: Record<string, string>) {
|
| | | return tmplIds.filter((id) => result?.[id] === 'accept');
|
| | | }
|
| | |
|
| | | /**
|
| | | * 用户同意订阅后上报后端(失败不影响主流程)。
|
| | | */
|
| | | async function saveAcceptedSubscribeTemplates(templateIds: string[]) {
|
| | | if (!Array.isArray(templateIds) || templateIds.length === 0) return;
|
| | | try {
|
| | | const loginRes = await Taro.login();
|
| | | const jscode = loginRes?.code;
|
| | | if (!jscode) return;
|
| | | await saveUserSubscribeMessage({
|
| | | templateIds,
|
| | | jscode
|
| | | });
|
| | | } catch (error) {
|
| | | }
|
| | | }
|
| | |
|
| | | // 执行通用订阅流程(失败/拒绝不影响主流程)
|
| | | export async function runSubscribeScene(config: SubscribeSceneConfig, bizId?: string | number) {
|
| | | try {
|
| | |
| | | tmplIds: config.tmplIds,
|
| | | success: (result) => {
|
| | | checkSubscribeStatus(type, config.tmplIds, result, config.limit);
|
| | | const acceptedTemplateIds = getAcceptedTemplateIds(config.tmplIds, result as Record<string, string>);
|
| | | void saveAcceptedSubscribeTemplates(acceptedTemplateIds);
|
| | | resolve();
|
| | | },
|
| | | fail: () => {
|