fix: 修复多个页面样式、逻辑及API调用问题
- 将查询订单轨迹的API方法从POST改为GET
- 修复选择城市页面的搜索结果布局和滚动问题
- 为多个页面的富文本图片添加自适应样式
- 修复订单详情页评价弹窗关闭时地图状态异常
- 修复站点搜索页的距离格式化及缓存逻辑
- 优化首页系统维护状态判断逻辑
- 修复行程页面地图缩放、复位按钮及气泡显示问题
- 优化叫车组件倒计时缓存和费用明细显示
| | |
| | | if (!s) return NaN; |
| | | return new Date(s.replace(/-/g, '/')).getTime(); |
| | | }; |
| | | const formatEnd = (s: string) => { |
| | | if (!s) return ''; |
| | | const d = new Date(s.replace(/-/g, '/')); |
| | | if (isNaN(d.getTime())) return ''; |
| | | const hh = String(d.getHours()).padStart(2, '0'); |
| | | const mm = String(d.getMinutes()).padStart(2, '0'); |
| | | return `${hh}:${mm}`; |
| | | }; |
| | | const check = () => { |
| | | getHotFixInfo().then((res: any) => { |
| | | const status = Number(res?.hotfixStatus ?? res?.data?.hotfixStatus ?? 0); |
| | | const start = parseTime(res?.hotfixStartTime ?? res?.data?.hotfixStartTime ?? ''); |
| | | const end = parseTime(res?.hotfixEndTime ?? res?.data?.hotfixEndTime ?? ''); |
| | | const endRaw = res?.hotfixEndTime ?? res?.data?.hotfixEndTime ?? ''; |
| | | const now = Date.now(); |
| | | if (status === 1 && Number.isFinite(start) && Number.isFinite(end) && now >= start && now <= end) { |
| | | if (status === 1 ) { |
| | | setIsMaintain(true); |
| | | setHotfixEndText(formatEnd(endRaw)); |
| | | setHotfixEndText(res?.hotfixEndTime); |
| | | } else { |
| | | setIsMaintain(false); |
| | | setHotfixEndText(''); |
| | |
| | | } |
| | | {/* 系统维护 */} |
| | | { |
| | | isMaintain && <View className={`is-maintain ml-32 ${maintainBounce ? 'maintain-bounce' : ''}`}>系统维护中,预计今日{hotfixEndText || '-:-'}恢复服务</View> |
| | | isMaintain && <View className={`is-maintain ml-32 ${maintainBounce ? 'maintain-bounce' : ''}`}>系统维护中,预计{hotfixEndText || '-:-'}恢复服务</View> |
| | | } |
| | | {/* 查看运营区域 */} |
| | | { |
| | |
| | | export const queryOrderTrajectory = async (payload) => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderTrajectory`, |
| | | method: 'POST', |
| | | method: 'GET', |
| | | payload |
| | | }); |
| | | } |
| | |
| | | import activeGood from '../../../assets/active-good.png' |
| | | import activeBad from '../../../assets/active-bad.png' |
| | | import activeNormal from '../../../assets/active-normal.png' |
| | | import { formatCountdown, getRemainingSeconds, parseFlexibleEndTimeMs, formatDurationDisplay, formatNumber, getTypeLabel, formatMoney, formatTripDistance, formatTripDurationFromSeconds } from '../../../../utils/Robotaxi-public-method'; |
| | | import { |
| | | formatCountdown, |
| | | getRemainingSeconds, |
| | | parseFlexibleEndTimeMs, |
| | | formatDurationDisplay, |
| | | formatNumber, |
| | | getTypeLabel, |
| | | formatMoney, |
| | | formatTripDistance, |
| | | formatTripDurationFromSeconds |
| | | } from '../../../../utils/Robotaxi-public-method'; |
| | | import { |
| | | getWaitTime, |
| | | setTemperature, |
| | |
| | | const lastFareOrderIdRef = useRef<string | null>(null); |
| | | const hasShownAnnouncementsRef = useRef(false); // 是否已展示过公告弹窗 |
| | | const PICKUP_END_TIME_KEY = 'pickupEndTime'; // 上车点限停结束时间缓存键 |
| | | const FINISH_END_TIME_KEY = 'finishEndTime'; // 到达目的地倒计时结束时间缓存键 |
| | | |
| | | const TEMP_MIN = 22; |
| | | const TEMP_MAX = 28; |
| | |
| | | } |
| | | finishEndTimeRef.current = null; |
| | | setFinishCountdownSeconds(null); |
| | | Taro.removeStorageSync(FINISH_END_TIME_KEY); |
| | | return; |
| | | } |
| | | queryOrderFinishTime().then((res: any) => { |
| | | const endTimeMs = parseFlexibleEndTimeMs(res); |
| | | const cachedEndTime = Taro.getStorageSync(FINISH_END_TIME_KEY); |
| | | const hasValidCached = typeof cachedEndTime === 'number' && cachedEndTime > Date.now(); |
| | | const initFinishCountdown = (endTimeMs: number | null) => { |
| | | finishEndTimeRef.current = endTimeMs; |
| | | if (typeof endTimeMs === 'number' && endTimeMs > Date.now()) { |
| | | Taro.setStorageSync(FINISH_END_TIME_KEY, endTimeMs); |
| | | } else { |
| | | Taro.removeStorageSync(FINISH_END_TIME_KEY); |
| | | } |
| | | const remainingSeconds = getRemainingSeconds(endTimeMs); |
| | | if (remainingSeconds === null) { |
| | | setFinishCountdownSeconds(null); |
| | |
| | | clearTimeout(finishCountdownTimerRef.current); |
| | | } |
| | | setFinishCountdownSeconds(0); |
| | | Taro.removeStorageSync(FINISH_END_TIME_KEY); |
| | | handleFinishTimeout?.(); |
| | | return; |
| | | } |
| | |
| | | finishCountdownTimerRef.current = setTimeout(scheduleFinishTick, delay); |
| | | }; |
| | | scheduleFinishTick(); |
| | | }; |
| | | if (hasValidCached) { |
| | | initFinishCountdown(cachedEndTime); |
| | | return; |
| | | } |
| | | queryOrderFinishTime().then((res: any) => { |
| | | const endTimeMs = parseFlexibleEndTimeMs(res); |
| | | initFinishCountdown(endTimeMs); |
| | | }).catch(() => { |
| | | setFinishCountdownSeconds(null); |
| | | finishEndTimeRef.current = null; |
| | | Taro.removeStorageSync(FINISH_END_TIME_KEY); |
| | | }); |
| | | } else if (tripStatus === 8 || tripStatus === 9) { |
| | | Taro.removeStorageSync(FINISH_END_TIME_KEY); |
| | | const targetOrderId = orderId || Taro.getStorageSync('orderId'); |
| | | if (targetOrderId && lastFareOrderIdRef.current !== targetOrderId) { |
| | | lastFareOrderIdRef.current = targetOrderId; |
| | |
| | | } |
| | | return () => { |
| | | isActive = false; |
| | | if (tripStatus === 7 && finishCountdownEnabled && typeof finishEndTimeRef.current === 'number' && finishEndTimeRef.current > Date.now()) { |
| | | Taro.setStorageSync(FINISH_END_TIME_KEY, finishEndTimeRef.current); |
| | | } |
| | | if (countdownTimerRef.current) { |
| | | clearTimeout(countdownTimerRef.current); |
| | | } |
| | |
| | | </View> |
| | | {fareBreakdown.mileage.filter((item: any) => item?.money || item?.mileage).map((item: any, index: number) => ( |
| | | <View key={`mileage-${index}`} className="particulars-detail-item-content"> |
| | | <View>{getTypeLabel(item?.type)}({formatNumber(item?.mileage)}公里)</View> |
| | | <View>{getTypeLabel(item?.type)}({formatTripDistance(item?.mileage)})</View> |
| | | <View>{formatMoney(item?.money)}元</View> |
| | | </View> |
| | | ))} |
| | |
| | | </View> |
| | | {fareBreakdown.longDistance.filter((item: any) => item?.money || item?.mileage).map((item: any, index: number) => ( |
| | | <View key={`long-${index}`} className="particulars-detail-item-content"> |
| | | <View>{getTypeLabel(item?.type)}({formatNumber(item?.mileage)}公里)</View> |
| | | <View>{getTypeLabel(item?.type)}({formatTripDistance(item?.mileage)})</View> |
| | | <View>{formatMoney(item?.money)}元</View> |
| | | </View> |
| | | ))} |
| | |
| | | return; |
| | | } |
| | | getPrivacyContent(5).then((res: any) => { |
| | | if (res.content) { |
| | | res.content = res.content.replace(/<img/gi, |
| | | '<img style="max-width:100%;height:auto;margin:0 auto;display:block"') |
| | | } |
| | | setNoticeContent(res.content); |
| | | }).catch(() => { }); |
| | | }, [show]); |
| | |
| | | const [carCalloutStyle, setCarCalloutStyle] = useState<any>({}); |
| | | const [carCalloutVisible, setCarCalloutVisible] = useState(false); |
| | | const [distanceConfig, setDistanceConfig] = useState<any>({}); |
| | | const [mapScale, setMapScale] = useState(14); |
| | | |
| | | const markersRef = useRef<any[]>([]); |
| | | const tripStatusRef = useRef(tripStatus); // 行程状态引用 |
| | |
| | | points, |
| | | padding |
| | | }); |
| | | if (viewportType === 'dropoff') { |
| | | setTimeout(() => { |
| | | setMapScale(13); |
| | | }, 50); |
| | | } |
| | | }); |
| | | if (tripStatus === 6 && viewportType === 'route') { |
| | | tripStatus6ViewportLockedRef.current = true; |
| | |
| | | }; |
| | | // 打开取消弹窗 |
| | | const handleOpenCancelPopupByTripStatus = (status: number, durationDisplay?: any) => { |
| | | console.log(status); |
| | | |
| | | if (durationDisplay) { |
| | | setCancelType(2); |
| | | setShowCancelPopup(true); |
| | |
| | | id="map" |
| | | longitude={mapLongitude} |
| | | latitude={mapLatitude} |
| | | scale={14} |
| | | scale={mapScale} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | showLocation={!!longitude} |
| | |
| | | { |
| | | (!['车辆位置', '已到达'].includes(item.name)) && |
| | | <CoverView className={`custom-callout-content`}> |
| | | {item.name}<CoverView style={{ width: '2px' }}></CoverView> |
| | | {item.name} |
| | | { |
| | | !isAndroid && |
| | | <CoverView style={{ width: '2px' }}></CoverView> |
| | | } |
| | | { |
| | | tripStatus == 1 && <CoverImage className="custom-callout-icon" src={customCalloutIconSrc} /> |
| | | } |
| | |
| | | <View className="container-inner"> |
| | | {/* 复位按钮 */} |
| | | { |
| | | [7, 8, 9].includes(tripStatus) && |
| | | ![7, 8, 9].includes(tripStatus) && |
| | | <View className="origin-box" style={{ opacity: originBoxHidden ? 0 : originOpacity, transition: 'opacity 0.2s ease-out', pointerEvents: originBoxHidden ? 'none' : 'auto' }}> |
| | | <Image onClick={toOrigin} className="origin-icon" src={originIcon} /> |
| | | </View> |
| | |
| | | try { |
| | | Taro.showLoading({ title: '加载中...' }); |
| | | const detail = await getCouponDetail(item.couponId); |
| | | if (detail.couponDesc) { |
| | | detail.couponDesc = detail.couponDesc.replace(/<img/gi, |
| | | '<img style="max-width:100%;height:auto;margin:0 auto;display:block"') |
| | | } |
| | | setCurrentCoupon(detail); |
| | | Taro.hideLoading(); |
| | | |
| | |
| | | </View> |
| | | <View className='remark-card'> |
| | | <Text className='remark-label'>使用说明</Text> |
| | | <Text className='remark-value'><RichText nodes={currentCoupon.decodeURIComponent} /></Text> |
| | | <Text className='remark-value'><RichText nodes={currentCoupon.couponDesc} /></Text> |
| | | </View> |
| | | |
| | | </View> |
| | |
| | | import Taro, { useRouter, useDidShow, useDidHide } from '@tarojs/taro'; |
| | | import { useState, useEffect } from 'react'; |
| | | import Taro, { useRouter, useDidHide } from '@tarojs/taro'; |
| | | import { useState, useEffect, useRef } from 'react'; |
| | | import { View, Text, Image, Map } from '@tarojs/components'; |
| | | import './index.less'; |
| | | import good from '../../../assets/good.png' |
| | |
| | | const [activeIndex, setActiveIndex] = useState<number | null>(null) // 默认选中"非常满意" |
| | | const [orderData, setOrderData] = useState<OrderDetail | null>(null); |
| | | const [showEvaluatePopup, setShowEvaluatePopup] = useState(false); |
| | | const showEvaluatePopupRef = useRef(false); |
| | | |
| | | // 地图相关状态 |
| | | const [longitude, setLongitude] = useState(116.397128) // 地图中心点经度 |
| | |
| | | } |
| | | }, [orderId]); |
| | | |
| | | useEffect(() => { |
| | | showEvaluatePopupRef.current = showEvaluatePopup; |
| | | }, [showEvaluatePopup]); |
| | | |
| | | useDidHide(() => { |
| | | if (showEvaluatePopupRef.current) { |
| | | return; |
| | | } |
| | | setMarkers([]); |
| | | setPolyline([]); |
| | | setLongitude(116.397128); |
| | |
| | | }; |
| | | setTitle(titleMap[type] || '乘车须知'); |
| | | getPrivacyContent(type).then((res: any) => { |
| | | if (res.content) { |
| | | res.content = res.content.replace(/<img/gi, |
| | | '<img style="max-width:100%;height:auto;margin:0 auto;display:block"') |
| | | } |
| | | setContent(res?.content); |
| | | }).catch(() => { }); |
| | | }, []); |
| | |
| | | try { |
| | | setLoading(true); |
| | | const res: any = await getPricingRules(cityId); |
| | | setPricingHtml(res); |
| | | let pricingContent = res; |
| | | if (pricingContent) { |
| | | pricingContent = pricingContent.replace(/<img/gi, |
| | | '<img style="max-width:100%;height:auto;margin:0 auto;display:block"') |
| | | } |
| | | setPricingHtml(pricingContent); |
| | | } catch (error) { |
| | | console.error('获取定价规则失败', error); |
| | | Taro.showToast({ |
| | |
| | | } |
| | | |
| | | .search-result-box { |
| | | margin: 32px; |
| | | padding: 32px; |
| | | box-sizing: border-box; |
| | | |
| | | .search-result-item { |
| | | display: flex; |
| | |
| | | } |
| | | } |
| | | |
| | | .search-result-scroll { |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .no-data-box { |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | {/* 搜索后 */} |
| | | { |
| | | searchValue && |
| | | <View> |
| | | <View className='city-content'> |
| | | { |
| | | searchResultList.length > 0 ? |
| | | <View className='search-result-box'> |
| | | { |
| | | searchResultList.map((item, index) => { |
| | | const isOpen = openCityList.some(open => open.name === item.name); |
| | | return ( |
| | | <View key={index} className='search-result-item' onClick={() => handleSelectCity(item, isOpen)}> |
| | | <View className='search-result-city'>{item.name}</View> |
| | | <View className={`search-result-zone ${!isOpen ? 'search-result-zone-disabled' : ''}`}>{isOpen ? '运营区' : '待开放'}</View> |
| | | </View> |
| | | ) |
| | | }) |
| | | } |
| | | </View> |
| | | <ScrollView className='search-result-scroll' scrollY> |
| | | <View className='search-result-box'> |
| | | { |
| | | searchResultList.map((item, index) => { |
| | | const isOpen = openCityList.some(open => open.name === item.name); |
| | | return ( |
| | | <View key={index} className='search-result-item' onClick={() => handleSelectCity(item, isOpen)}> |
| | | <View className='search-result-city'>{item.name}</View> |
| | | <View className={`search-result-zone ${!isOpen ? 'search-result-zone-disabled' : ''}`}>{isOpen ? '运营区' : '待开放'}</View> |
| | | </View> |
| | | ) |
| | | }) |
| | | } |
| | | </View> |
| | | </ScrollView> |
| | | : |
| | | <View className='no-data-box'> |
| | | <Image className='no-data-image' src={require('../../assets/no-data-city.png')} /> |
| | |
| | | if (Array.isArray(availablePoints)) { |
| | | const list = availablePoints.map(item => ({ |
| | | ...item, |
| | | distance: item.distance |
| | | distance: formatDistance(item.distance) |
| | | })); |
| | | setSiteList(list.slice(0, MAX_EXPANDED_COUNT)); |
| | | } else { |
| | |
| | | } |
| | | } |
| | | |
| | | const startSiteCache = pageType === 'end' || pageType === 'start' ? Taro.getStorageSync('startSite') : null; |
| | | const filteredNearbyList = startSiteCache |
| | | const currentSiteCache = pageType === 'start' |
| | | ? Taro.getStorageSync('startSite') |
| | | : pageType === 'end' |
| | | ? Taro.getStorageSync('endSite') |
| | | : null; |
| | | const filteredNearbyList = currentSiteCache |
| | | ? nearbySiteList.filter((item: any) => { |
| | | const startId = startSiteCache?.id; |
| | | const startId = currentSiteCache?.id; |
| | | const itemId = item?.id; |
| | | if (startId && itemId) { |
| | | return String(startId) !== String(itemId); |
| | | } |
| | | const startLng = Number(startSiteCache?.longitude); |
| | | const startLat = Number(startSiteCache?.latitude); |
| | | const startLng = Number(currentSiteCache?.longitude); |
| | | const startLat = Number(currentSiteCache?.latitude); |
| | | const itemLng = Number(item?.longitude); |
| | | const itemLat = Number(item?.latitude); |
| | | if (!Number.isNaN(startLng) && !Number.isNaN(startLat) && !Number.isNaN(itemLng) && !Number.isNaN(itemLat)) { |