feat(begin-route): 新增行程开始页面及订单状态卡片
- 新增行程开始页面 `/pages/begin-route/index`,包含发布行程、呼叫车辆、行程中、结束行程及支付评价全流程
- 在首页添加订单状态卡片,实时显示车辆呼叫、到达、行驶中等状态
- 新增多个弹窗组件:取消行程、结束行程确认、乘客须知、公告、评价等
- 优化站点搜索页面,支持通过路由参数设置页面类型(出发地/目的地)
- 修复地图选点页面关闭按钮点击事件
1个文件已删除
11 文件已重命名
10个文件已添加
7个文件已修改
| | |
| | | 'pages/order/index', |
| | | 'pages/order/detail/index', |
| | | 'pages/select-city/index', |
| | | 'pages/select-map-site/index' |
| | | 'pages/select-map-site/index', |
| | | 'pages/begin-route/index' |
| | | ], |
| | | } |
| | | ]; |
| | |
| | | } |
| | | } |
| | | |
| | | @keyframes radiate { |
| | | 0% { |
| | | transform: translate(-50%, -50%) scale(0.5); |
| | | opacity: 1; |
| | | } |
| | | |
| | | 100% { |
| | | transform: translate(-50%, -50%) scale(1.2); |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | .order-status { |
| | | position: relative; |
| | | margin-top: 24px; |
| | | margin-bottom: 6px; |
| | | border-radius: 24px; |
| | | background: #FEFEFE; |
| | | box-sizing: border-box; |
| | | border: 0px solid #FFFFFF; |
| | | height: 104px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding-right: 24px; |
| | | |
| | | &-left { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | &-item { |
| | | margin-right: 12px; |
| | | position: relative; |
| | | margin-left: 14px; |
| | | display: flex; |
| | | justify-content: center; |
| | | width: 78px; |
| | | height: 78px; |
| | | |
| | | view { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | border-radius: 50%; |
| | | width: 78px; |
| | | height: 78px; |
| | | background: rgba(64, 124, 255, 0.2); |
| | | animation: radiate 2s infinite ease-out; |
| | | } |
| | | |
| | | view:last-child { |
| | | animation-delay: 1s; |
| | | } |
| | | |
| | | &-icon { |
| | | position: relative; |
| | | z-index: 9; |
| | | width: 26px; |
| | | height: 38px; |
| | | } |
| | | } |
| | | |
| | | &-text { |
| | | font-family: PingFang SC; |
| | | font-size: 28px; |
| | | font-weight: 500; |
| | | line-height: 48px; |
| | | letter-spacing: normal; |
| | | color: #3D3D3D; |
| | | } |
| | | |
| | | &-icon { |
| | | width: 156px; |
| | | height: 73.54px; |
| | | margin-left: 18px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | &-detail { |
| | | margin-left: 16px; |
| | | |
| | | view:first-child { |
| | | font-size: 28px; |
| | | font-weight: 600; |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | view:last-child { |
| | | font-size: 20px; |
| | | line-height: 20px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | } |
| | | } |
| | | } |
| | | |
| | | &-btn { |
| | | position: relative; |
| | | z-index: 1; |
| | | line-height: 56px; |
| | | padding: 0 28px; |
| | | font-size: 24px; |
| | | letter-spacing: normal; |
| | | color: #FFFFFF; |
| | | border-radius: 200px; |
| | | background: linear-gradient(111deg, #8475FF 0%, #7DD6FF 98%); |
| | | } |
| | | |
| | | &-bg { |
| | | position: absolute; |
| | | top: 4px; |
| | | right: 4px; |
| | | width: 266px; |
| | | height: 96px; |
| | | } |
| | | } |
| | | |
| | | .activity { |
| | | margin-top: 24px; |
| | | |
| | |
| | | const [markers, setMarkers] = useState<any[]>([]); // 标记点列表 |
| | | const [isMaintain, setIsMaintain] = useState(false); // 是否维护中 |
| | | const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内 |
| | | const [tripStatus, setTripStatus] = useState<number>(0); // 行程状态 |
| | | |
| | | const [boxHeight, setBoxHeight] = useState<number | string>('auto'); // 活动区域高度 |
| | | const startY = useRef(0); // 拖动开始时的Y坐标 |
| | |
| | | |
| | | const [selectedCity, setSelectedCity] = useState(''); // 选中的城市 |
| | | |
| | | const ORDER_STATUS = { |
| | | 4: '车辆正在赶来', |
| | | 5: '正在前往目的地', |
| | | 6: '车辆已到达上车点,请尽快前往', |
| | | 7: '车辆已到达目的地,请尽快结束行程', |
| | | } |
| | | |
| | | useDidShow(() => { |
| | | // 从本地存储中获取选中的城市 |
| | | const selectedCity = Taro.getStorageSync('selectedCity'); |
| | | if (selectedCity) { |
| | | setSelectedCity(selectedCity); |
| | | } |
| | | // 从本地存储中获取行程状态 |
| | | const tripStatus = Taro.getStorageSync('tripStatus'); |
| | | if (tripStatus) { |
| | | setTripStatus(Number(tripStatus)); |
| | | } |
| | | }); |
| | | |
| | |
| | | mapCtx.getCenterLocation({ |
| | | success: (res) => { |
| | | const { latitude: lat, longitude: lng } = res; |
| | | |
| | | const { targetMarker, minDistance } = findNearestMarker(lat, lng, markers); |
| | | |
| | | // 如果最近的点在一定范围内(例如 200米),则吸附 |
| | | const { targetMarker, minDistance } = findNearestMarker(lat, lng, markers); |
| | | |
| | | // 如果最近的点在一定范围内(例如 200米),则吸附 |
| | | if (targetMarker && minDistance < 200) { |
| | | // 使用 moveToLocation 实现吸附效果 |
| | | mapCtx.moveToLocation({ |
| | |
| | | |
| | | const toSeleteSitePage = (type: string) => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/site-search/index?type=' + type |
| | | url: '/robotaxi/pages/site-search/index?pageType=' + type |
| | | }) |
| | | } |
| | | |
| | | const toSeleteCityPage = () => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/select-city/index', |
| | | }) |
| | | } |
| | | |
| | | const toOrderDetail = () => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/begin-route/index', |
| | | }) |
| | | } |
| | | |
| | |
| | | </View> |
| | | </View> |
| | | </View> |
| | | {/* 订单状态卡片 */} |
| | | { |
| | | [2, 3, 4, 5, 6, 7].includes(tripStatus) && |
| | | <View className="order-status"> |
| | | { |
| | | [2, 3].includes(tripStatus) && |
| | | <View className="order-status-left"> |
| | | <View className="order-status-left-item"> |
| | | <Image className="order-status-left-item-icon" src={require('../../../robotaxi/assets/pointer.png')} /> |
| | | <View></View> |
| | | <View></View> |
| | | </View> |
| | | <View className="order-status-left-text">正在为您呼叫...</View> |
| | | </View> |
| | | } |
| | | { |
| | | [4, 5, 6, 7].includes(tripStatus) && |
| | | <View className="order-status-left"> |
| | | <Image className="order-status-left-icon" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e76e8fd6-1f52-482f-b9c0-6a026f4d2797.png' /> |
| | | <View className="order-status-left-detail"> |
| | | <View>粤PXV7029</View> |
| | | <View>{ORDER_STATUS[tripStatus]}</View> |
| | | </View> |
| | | </View> |
| | | } |
| | | <View onClick={toOrderDetail} className="order-status-btn">查看</View> |
| | | <Image className="order-status-bg" src={require('../../../robotaxi/assets/order-card-bg.png')} /> |
| | | </View> |
| | | } |
| | | {/* 运营活动 */} |
| | | <View className="activity"> |
| | | { |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './css-folder/cancel-popup.less' |
| | | |
| | | type Props = { |
| | | show?: boolean; |
| | | cancelType?: number; |
| | | onClose?: () => void; |
| | | onConfirm?: () => void; |
| | | } |
| | | |
| | | export default function CancelPopup({ show = false, cancelType = 1, onClose, onConfirm }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | |
| | | const STATUS_MAP = { |
| | | 1: { |
| | | title: '2分钟内预计有车,确定取消吗?', |
| | | tip: '本单限时优惠16.2元,重新叫车可能等待更久', |
| | | }, |
| | | 2: { |
| | | title: '正在为您重新派车中...', |
| | | tip: '当前车辆无法继续服务', |
| | | }, |
| | | 3: { |
| | | title: '附近没有空余车辆,已为您取消订单', |
| | | tip: '当前运力紧张,建议您稍后重新发起订单', |
| | | }, |
| | | 4: { |
| | | title: '订单已取消', |
| | | tip: '已为您取消订单,期待您下次体验', |
| | | }, |
| | | } |
| | | |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-out' |
| | | }); |
| | | animation.translateY(0).step(); |
| | | setAnimationData(animation.export()); |
| | | } else if (!show && visible) { |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-in' |
| | | }); |
| | | animation.translateY(500).step(); |
| | | setAnimationData(animation.export()); |
| | | |
| | | setTimeout(() => { |
| | | setVisible(false); |
| | | }, 300); |
| | | } |
| | | }, [show]); |
| | | |
| | | if (!visible) return null; |
| | | |
| | | return ( |
| | | <View className='cancel-popup' catchMove> |
| | | <View className='cancel-mask'></View> |
| | | <View className='cancel-content' animation={animationData}> |
| | | <View className='cancel-box'> |
| | | <Image onClick={onClose} className='cancel-icon' src={require('../../../assets/close-icon.png')} /> |
| | | </View> |
| | | <View className='cancel-tip-box'> |
| | | <View className='cancel-tip-box-title'>{STATUS_MAP[cancelType].title}</View> |
| | | <View className={`cancel-tip-box-desc ${cancelType === 4 && 'color1'}`}>{STATUS_MAP[cancelType].tip}</View> |
| | | </View> |
| | | { |
| | | [3, 4].includes(cancelType) ? |
| | | <View> |
| | | <View className='cancel-site-box'> |
| | | <View className='cancel-site-box-icon'> |
| | | <View className='cancel-site-box-icon-start'></View> |
| | | <Image src={require('../../../assets/dotted-line.png')} /> |
| | | <View className='cancel-site-box-icon-end'></View> |
| | | </View> |
| | | <View className='cancel-site-box-content'> |
| | | <View>广州岭南职业技术学院-南1门(公交站)</View> |
| | | <View>小鹏科技园2号楼</View> |
| | | </View> |
| | | </View> |
| | | <View className='cancel-btn-box'> |
| | | <View className='cancel-btn-again' onClick={onConfirm}>再来一单</View> |
| | | </View> |
| | | </View> |
| | | : |
| | | <View className='cancel-btn-box'> |
| | | <View className='cancel-btn-cancel' onClick={onConfirm}>确认取消</View> |
| | | <View className='cancel-btn-again' onClick={onClose}>再等等</View> |
| | | </View> |
| | | } |
| | | </View> |
| | | </View> |
| | | ) |
| | | } |
| File was renamed from src/robotaxi/pages/select-map-site/components/css-folder/cancel-popup.less |
| | |
| | | letter-spacing: normal; |
| | | color: #F58900; |
| | | } |
| | | |
| | | .color1 { |
| | | color: rgba(0, 0, 0, 0.4) |
| | | } |
| | | } |
| | | |
| | | .cancel-site-box { |
| | | margin-top: 32px; |
| | | border-radius: 20px; |
| | | background: #F5F6FA; |
| | | padding: 32px; |
| | | display: flex; |
| | | gap: 16px; |
| | | |
| | | &-icon { |
| | | padding: 17px 0; |
| | | flex-shrink: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | |
| | | &-start { |
| | | width: 10px; |
| | | height: 10px; |
| | | background: #768AFF; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | &-end { |
| | | width: 10px; |
| | | height: 10px; |
| | | background: #8BB654; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | image { |
| | | flex: 1; |
| | | width: 2px; |
| | | margin: 7px 0; |
| | | } |
| | | } |
| | | |
| | | &-content { |
| | | flex: 1; |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | |
| | | view:first-child { |
| | | margin-bottom: 40px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .cancel-btn-box { |
| New file |
| | |
| | | .passenger-popup { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 99999; |
| | | |
| | | .passenger-mask { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | } |
| | | |
| | | .passenger-content { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | background: #ffffff; |
| | | border-radius: 16px 16px 0 0; |
| | | padding: 48px 45px 88px 45px; |
| | | |
| | | &-title { |
| | | font-size: 32px; |
| | | font-weight: 500; |
| | | line-height: 48px; |
| | | text-align: center; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | &-desc { |
| | | margin-top: 34px; |
| | | font-size: 28px; |
| | | line-height: 48px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | &-checkbox { |
| | | margin-top: 32px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | font-size: 24px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.6); |
| | | width: fit-content; |
| | | |
| | | image { |
| | | width: 36px; |
| | | height: 36px; |
| | | } |
| | | } |
| | | |
| | | &-btn { |
| | | margin-top: 30px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(96deg, #8475FF -1%, #7DD6FF 103%); |
| | | line-height: 96px; |
| | | text-align: center; |
| | | font-size: 30px; |
| | | letter-spacing: normal; |
| | | color: #FFFFFF; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './css-folder/evaluate-popup.less' |
| | | |
| | | type Props = { |
| | | show?: boolean; |
| | | onClose?: () => void; |
| | | onConfirm?: () => void; |
| | | } |
| | | |
| | | export default function EvaluatePopup({ show = false, onClose, onConfirm }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-out' |
| | | }); |
| | | animation.translateY(0).step(); |
| | | setAnimationData(animation.export()); |
| | | } else if (!show && visible) { |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-in' |
| | | }); |
| | | animation.translateY(500).step(); |
| | | setAnimationData(animation.export()); |
| | | |
| | | setTimeout(() => { |
| | | setVisible(false); |
| | | }, 300); |
| | | } |
| | | }, [show]); |
| | | |
| | | if (!visible) return null; |
| | | |
| | | return ( |
| | | <View className='cancel-popup' catchMove> |
| | | <View className='cancel-mask'></View> |
| | | <View className='cancel-content' animation={animationData}> |
| | | </View> |
| | | </View> |
| | | ) |
| | | } |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './css-folder/passenger-notice.less' |
| | | |
| | | type Props = { |
| | | show?: boolean; |
| | | onClose?: () => void; |
| | | } |
| | | |
| | | export default function PassengerNotice({ show = false, onClose }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [checked, setChecked] = useState(false); |
| | | |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-out' |
| | | }); |
| | | animation.translateY(0).step(); |
| | | setAnimationData(animation.export()); |
| | | } else if (!show && visible) { |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-in' |
| | | }); |
| | | animation.translateY(500).step(); |
| | | setAnimationData(animation.export()); |
| | | |
| | | setTimeout(() => { |
| | | setVisible(false); |
| | | }, 300); |
| | | } |
| | | }, [show]); |
| | | |
| | | if (!visible) return null; |
| | | |
| | | return ( |
| | | <View className='passenger-popup' catchMove> |
| | | <View className='passenger-mask'></View> |
| | | <View className='passenger-content' animation={animationData}> |
| | | <View className='passenger-content-title'>乘车需知</View> |
| | | <View className='passenger-content-desc'>此处限停,请在倒计时结束前到达上车点此处限停,请在倒计时结束前到达上车点此处限停,请在倒计时结束前到达上车点此处限停,请在倒计时结束前到达上车点此处限停,请在倒计时结束前到达上车点此处限停,请在倒计时结束前到达上车点此请在倒计时结束前到达上车点此请在倒计时结束前到达上车点此</View> |
| | | <View onClick={() => setChecked(!checked)} className='passenger-content-checkbox'> |
| | | { |
| | | checked ? |
| | | <Image src={require('../../../assets/check.png')} /> |
| | | : |
| | | <Image src={require('../../../assets/uncheck.png')} /> |
| | | } |
| | | <View>3个月内不再显示</View> |
| | | </View> |
| | | <View onClick={() => { |
| | | if (checked) { |
| | | Taro.setStorageSync('passengerNoticeCheckedTime', Date.now()); |
| | | } |
| | | onClose?.(); |
| | | }} className='passenger-content-btn'>我知道了</View> |
| | | </View> |
| | | </View> |
| | | ) |
| | | } |
| New file |
| | |
| | | export default { |
| | | navigationStyle: "custom", |
| | | disableScroll: true |
| | | }; |
| New file |
| | |
| | | .select-city { |
| | | position: fixed; |
| | | top: 110px; |
| | | left: 31px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 72px; |
| | | height: 72px; |
| | | border-radius: 200px; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | backdrop-filter: blur(40px); |
| | | box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1); |
| | | |
| | | .select-city-icon { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | } |
| | | |
| | | .map-entry { |
| | | position: fixed; |
| | | top: 236px; |
| | | right: 32px; |
| | | display: flex; |
| | | align-items: center; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | border-radius: 138px; |
| | | background: rgba(255, 255, 255, 1); |
| | | overflow: hidden; |
| | | padding: 8px 12px; |
| | | |
| | | .map-entry-icon { |
| | | width: 64px; |
| | | height: 64px; |
| | | } |
| | | |
| | | .split-line { |
| | | width: 44px; |
| | | height: 1px; |
| | | background-color: rgba(0, 0, 0, 0.1); |
| | | } |
| | | } |
| | | |
| | | .home-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 100vh; |
| | | overflow: hidden; |
| | | position: relative; |
| | | } |
| | | |
| | | .container { |
| | | width: 100%; |
| | | position: absolute; |
| | | bottom: 0; |
| | | z-index: 10; |
| | | overflow: hidden; |
| | | |
| | | .origin-box { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-bottom: 32px; |
| | | padding-right: 32px; |
| | | |
| | | .origin-icon { |
| | | width: 88px; |
| | | height: 88px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .custom-callout { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .custom-callout-content { |
| | | display: flex; |
| | | align-items: center; |
| | | border-radius: 16px; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | padding: 12px 12px 12px 20px; |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | line-height: 48px; |
| | | box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2); |
| | | } |
| | | |
| | | .car-time { |
| | | font-size: 24px; |
| | | letter-spacing: normal; |
| | | color: #3D3D3D; |
| | | } |
| | | |
| | | .car-time-num { |
| | | font-size: 36px; |
| | | font-weight: 600; |
| | | letter-spacing: normal; |
| | | color: #6D7FD9; |
| | | } |
| | | |
| | | .custom-callout-icon { |
| | | margin-left: 8px; |
| | | width: 32px; |
| | | height: 32px; |
| | | flex-shrink: 0; |
| | | display: block; |
| | | } |
| | | |
| | | .waiting-callout { |
| | | position: relative; |
| | | z-index: 10; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | justify-content: center; |
| | | overflow: visible; // 确保辐射动画不被裁剪 |
| | | } |
| | | |
| | | .waiting-radiation-box { |
| | | position: absolute; |
| | | top: 100%; // 移到气泡下方 |
| | | left: 50%; |
| | | margin-top: 30px; // 向下偏移以对齐Marker锚点(预估值) |
| | | width: 0; |
| | | height: 0; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | pointer-events: none; |
| | | overflow: visible; |
| | | } |
| | | |
| | | @keyframes ripple { |
| | | 0% { |
| | | opacity: 0.6; |
| | | transform: translate(-50%, -50%) scale(0.2); |
| | | } |
| | | |
| | | 100% { |
| | | opacity: 0; |
| | | transform: translate(-50%, -50%) scale(2.5); |
| | | } |
| | | } |
| | | |
| | | .waiting-radiation-ring { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 200px; |
| | | height: 200px; |
| | | border-radius: 50%; |
| | | background: rgba(26, 79, 255, 0.05); |
| | | border: 1px solid rgba(26, 79, 255, 0.2); |
| | | opacity: 0; // 初始不可见,由动画控制 |
| | | animation: ripple 3.5s infinite linear; |
| | | |
| | | &:nth-child(1) { |
| | | animation-delay: 0s; |
| | | } |
| | | |
| | | &:nth-child(2) { |
| | | animation-delay: 1s; |
| | | } |
| | | } |
| | | |
| | | |
| | | .waiting-callout-bg { |
| | | position: relative; |
| | | height: 70px; |
| | | min-width: 155px; |
| | | text-align: center; |
| | | width: fit-content; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .waiting-callout-bg-img { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .waiting-callout-bg-text { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | z-index: 100; |
| | | font-size: 22px; |
| | | font-weight: 500; |
| | | letter-spacing: normal; |
| | | color: #FFFFFF; |
| | | line-height: 48px; |
| | | padding: 0 20px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .waiting-callout-txt { |
| | | position: relative; |
| | | z-index: 200; |
| | | margin-top: -26px; |
| | | display: flex; |
| | | align-items: center; |
| | | border-radius: 16px; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | padding: 12px 12px 12px 20px; |
| | | min-width: 155px; |
| | | } |
| | | |
| | | .waiting-callout-txt-content { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | line-height: 48px; |
| | | } |
| New file |
| | |
| | | import Taro from "@tarojs/taro" |
| | | import { useEffect, useState } from 'react'; |
| | | import { View, Map, Text, Image, CoverView, CoverImage } from '@tarojs/components'; |
| | | import './index.less'; |
| | | |
| | | import CustomerServicePopup from '../../components/customer-service'; |
| | | import ReleaseRoute from './components/release-route'; |
| | | import ReleaseBeBeing from './components/release-be-being'; |
| | | import CancelPopup from './components/cancel-popup'; |
| | | import CallingCar from './components/calling-car'; |
| | | import AnnouncementsPopup from './components/announcements-popup'; |
| | | import OverRoute from './components/over-route'; |
| | | import PassengerNotice from './components/passenger-notice'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png' |
| | | import contactService from '../../../robotaxi/assets/contact-service.png' |
| | | import coupon from '../../../robotaxi/assets/coupon.png' |
| | | import suggest from '../../../robotaxi/assets/suggest.png' |
| | | import originIcon from '../../../robotaxi/assets/origin-icon.png' |
| | | import { parseRouteToPoints } from '../../../utils/Robotaxi-public-method'; |
| | | import amapFile from '../../../libs/amap-wx.130'; |
| | | import { AMAP_KEY } from '../../../constants/api'; |
| | | |
| | | export default function SelectMapSite() { |
| | | |
| | | const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示 |
| | | const [longitude, setLongitude] = useState<number | null>(null); // 经度 |
| | | const [latitude, setLatitude] = useState<number | null>(null); // 纬度 |
| | | const [markers, setMarkers] = useState<any[]>([]); // 标记点列表 |
| | | const [polyline, setPolyline] = useState<any[]>([]); // 路线 |
| | | const [startSite, setStartSite] = useState<any>(null); // 上车点 |
| | | const [endSite, setEndSite] = useState<any>({}); // 下车点 |
| | | const [tripStatus, setTripStatus] = useState<number>(Number(Taro.getStorageSync('tripStatus')) || 1); // 行程状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功 |
| | | const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数 |
| | | const [showCancelPopup, setShowCancelPopup] = useState(true); // 取消弹窗是否显示 |
| | | const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false); // 安全注意事项动画弹窗是否显示 |
| | | const [showOverRoutePopup, setShowOverRoutePopup] = useState(false); // 结束行程弹窗是否显示 |
| | | const [showPassengerNotice, setShowPassengerNotice] = useState(false); // 乘车需知弹窗是否显示 |
| | | const [cancelType, setCancelType] = useState(1); // 取消类型 |
| | | |
| | | const router = Taro.useRouter(); |
| | | |
| | | |
| | | // 监听行程状态,处理等待计时 |
| | | useEffect(() => { |
| | | if (tripStatus === 4) { |
| | | if (Taro.getStorageSync('passengerNoticeShowed') !== 'true') { |
| | | setShowPassengerNotice(true); |
| | | Taro.setStorageSync('passengerNoticeShowed', 'true'); |
| | | } |
| | | if (Taro.getStorageSync('passengerNoticeCheckedTime')) { |
| | | const checkedTime = Number(Taro.getStorageSync('passengerNoticeCheckedTime')); |
| | | if (Date.now() - checkedTime > 3 * 30 * 24 * 60 * 60 * 1000) { |
| | | setShowPassengerNotice(true); |
| | | } |
| | | } |
| | | } |
| | | Taro.setStorageSync('tripStatus', tripStatus); |
| | | let timer: any = null; |
| | | if (tripStatus === 2 || tripStatus === 3) { |
| | | // 状态为2或3时,将地图中心定位到上车点 |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if (s && s.latitude && s.longitude) { |
| | | setLatitude(s.latitude); |
| | | setLongitude(s.longitude); |
| | | |
| | | // 增加延迟,确保地图上下文就绪 |
| | | setTimeout(() => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation({ |
| | | latitude: s.latitude, |
| | | longitude: s.longitude |
| | | }); |
| | | }, 500); |
| | | } |
| | | |
| | | setElapsedSeconds(0); |
| | | timer = setInterval(() => { |
| | | setElapsedSeconds(prev => prev + 1); |
| | | }, 1000); |
| | | } else { |
| | | setElapsedSeconds(0); |
| | | } |
| | | return () => { |
| | | if (timer) clearInterval(timer); |
| | | }; |
| | | }, [tripStatus]); |
| | | |
| | | // 页面初始化 |
| | | useEffect(() => { |
| | | const s = Taro.getStorageSync('startSite'); |
| | | const e = Taro.getStorageSync('endSite'); |
| | | if (s) { |
| | | setStartSite(s); |
| | | } |
| | | if (e) { |
| | | setEndSite(e); |
| | | } |
| | | |
| | | // 初始化默认标记点 (成都坐标) |
| | | const defaultMarkers = [ |
| | | { |
| | | id: 1, |
| | | latitude: 30.572269, |
| | | longitude: 104.066541, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png', |
| | | width: 15, |
| | | height: 15, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '中国南方电网·1号门' |
| | | }, |
| | | { |
| | | id: 2, |
| | | latitude: 30.573200, |
| | | longitude: 104.068100, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png', |
| | | width: 15, |
| | | height: 15, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '中国南方电网·2号门' |
| | | }, |
| | | { |
| | | id: 3, |
| | | latitude: 30.571100, |
| | | longitude: 104.065200, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png', |
| | | width: 15, |
| | | height: 15, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '中国南方电网·3号门' |
| | | }, |
| | | { |
| | | id: 4, |
| | | latitude: 30.574100, |
| | | longitude: 104.066100, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png', |
| | | width: 15, |
| | | height: 15, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '中国南方电网·4号门' |
| | | }, |
| | | { |
| | | id: 5, |
| | | latitude: 30.570100, |
| | | longitude: 104.067100, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png', |
| | | width: 15, |
| | | height: 15, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '中国南方电网·5号门' |
| | | } |
| | | ]; |
| | | setMarkers(defaultMarkers); |
| | | |
| | | getLocationInfo(); |
| | | }, []); |
| | | |
| | | // 规划路线 |
| | | useEffect(() => { |
| | | const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY }); |
| | | myAmapFun.getDrivingRoute({ |
| | | origin: `${longitude},${latitude}`, |
| | | destination: `${endSite.longitude},${endSite.latitude}`, |
| | | success: function (data) { |
| | | const points = parseRouteToPoints(data); |
| | | |
| | | setPolyline([{ |
| | | points: points, |
| | | color: '#6D7FD9', |
| | | width: 10, |
| | | arrowLine: true, |
| | | }]) |
| | | |
| | | // 设置起点和终点标记 |
| | | if (points.length > 0) { |
| | | const startPoint = points[0]; |
| | | const endPoint = points[points.length - 1]; |
| | | |
| | | // 检查 storage 中的 startSite 是否存在 |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if ([1, 2, 3, 4, 7, 8].includes(tripStatus)) { |
| | | const isCar = [4, 7].includes(tripStatus); |
| | | |
| | | const startMarker: any = { |
| | | id: 1, |
| | | latitude: startPoint.latitude, |
| | | longitude: startPoint.longitude, |
| | | iconPath: isCar |
| | | ? 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e51e92e6-700d-4e83-b3e0-14e6f18e0b9a.png' |
| | | : 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png', |
| | | width: isCar ? 40 : 22, |
| | | height: isCar ? 20 : 40, |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 2, |
| | | }; |
| | | |
| | | if (tripStatus === 4) { |
| | | startMarker.name = '车辆位置'; |
| | | startMarker.customCallout = { anchorY: 0, anchorX: 0, display: 'ALWAYS' }; |
| | | } else if (tripStatus !== 7) { |
| | | startMarker.name = s?.name || '当前位置'; |
| | | startMarker.customCallout = { anchorY: 8, anchorX: 0, display: 'ALWAYS' }; |
| | | } |
| | | |
| | | setMarkers([ |
| | | // 起点指针 |
| | | startMarker, |
| | | // 终点指针 |
| | | { |
| | | id: 2, |
| | | latitude: endPoint.latitude, |
| | | longitude: endPoint.longitude, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png', |
| | | width: 22, |
| | | height: 40, |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 2, |
| | | name: tripStatus === 7 ? '已到达' : endSite.name, |
| | | customCallout: { |
| | | anchorY: 8, |
| | | anchorX: 0, |
| | | display: 'ALWAYS' |
| | | } |
| | | }, |
| | | ]) |
| | | } |
| | | |
| | | // 调整视角以包含所有路线点 |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.includePoints({ |
| | | points: points, |
| | | padding: [50, 50, 300, 50] |
| | | }) |
| | | } |
| | | }, |
| | | fail: function (info) { |
| | | console.log('规划失败', info) |
| | | } |
| | | }) |
| | | }, [tripStatus, endSite, longitude, latitude]) |
| | | |
| | | const formatTime = (sec: number) => { |
| | | const m = Math.floor(sec / 60); |
| | | const s = sec % 60; |
| | | return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; |
| | | } |
| | | |
| | | // 获取用户位置信息 |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | }) |
| | | } |
| | | |
| | | // 返回当前位置 |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation(); |
| | | } |
| | | |
| | | const handleCloseCustomerService = () => { |
| | | setShowCustomerService(false); |
| | | }; |
| | | |
| | | const openService = () => { |
| | | setShowCustomerService(true); |
| | | } |
| | | |
| | | |
| | | const toBack = () => { |
| | | if (tripStatus == 1) { |
| | | |
| | | } |
| | | Taro.switchTab({ |
| | | url: '/pages/robotaxi/home/index' |
| | | }) |
| | | } |
| | | |
| | | // 立即呼叫 |
| | | const callImmediately = () => { |
| | | if (!endSite.name) { |
| | | Taro.showToast({ |
| | | title: '请选择站点', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | setTripStatus(2) |
| | | // 校验上车点是否存在 |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if (s && s.latitude && s.longitude) { |
| | | // 移动地图中心到上车点 |
| | | setLatitude(s.latitude); |
| | | setLongitude(s.longitude); |
| | | } |
| | | } |
| | | |
| | | // 确认取消 |
| | | const handleConfirmCancel = () => { |
| | | setShowCancelPopup(false); |
| | | setTripStatus(1); |
| | | } |
| | | |
| | | // 关闭取消弹窗 |
| | | const handleCloseCancelPopup = () => { |
| | | setShowCancelPopup(false); |
| | | } |
| | | |
| | | // 打开取消弹窗 |
| | | const handleOpenCancelPopup = () => { |
| | | setShowCancelPopup(true); |
| | | } |
| | | |
| | | // 关闭安全注意事项弹窗 |
| | | const handleCloseAnnouncementsPopup = () => { |
| | | setShowAnnouncementsPopup(false); |
| | | } |
| | | |
| | | // 打开结束行程弹窗 |
| | | const handleOpenOverRoutePopup = () => { |
| | | setShowOverRoutePopup(true); |
| | | } |
| | | |
| | | // 关闭结束行程弹窗 |
| | | const handleCloseOverRoutePopup = () => { |
| | | setShowOverRoutePopup(false); |
| | | } |
| | | |
| | | // 确认结束行程 |
| | | const handleConfirmOverRoute = () => { |
| | | setShowOverRoutePopup(false); |
| | | setTripStatus(8); |
| | | } |
| | | |
| | | // 确认支付 |
| | | const handlePay = () => { |
| | | setTripStatus(9); |
| | | } |
| | | |
| | | // 关闭乘车需知弹窗 |
| | | const handleClosePassengerNotice = () => { |
| | | setShowPassengerNotice(false); |
| | | } |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | | id="map" |
| | | longitude={longitude} |
| | | latitude={latitude} |
| | | scale={14} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | showLocation={!!longitude} |
| | | style={{ width: '100%', height: '100%' }} |
| | | > |
| | | {/* 自定义callout */} |
| | | { |
| | | ![2, 3].includes(tripStatus) && markers.map((item) => { |
| | | return ( |
| | | <CoverView slot="callout" marker-id={item.id} key={item.id} className="custom-callout"> |
| | | { |
| | | (!['车辆位置', '已到达'].includes(item.name)) && |
| | | <CoverView className="custom-callout-content"> |
| | | {item.name} <CoverImage className="custom-callout-icon" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f9e285fc-4c09-404d-90f8-4bac178565d0.png' /> |
| | | </CoverView> |
| | | } |
| | | { |
| | | item.name === '车辆位置' && |
| | | <CoverView className="custom-callout-content"> |
| | | <Text className="car-time"><Text className="car-time-num">3</Text>公里<Text className="car-time-nuum">2</Text>分钟</Text> |
| | | </CoverView> |
| | | } |
| | | { |
| | | item.name === '已到达' && |
| | | <CoverView className="custom-callout-content"> |
| | | 已到达 |
| | | </CoverView> |
| | | } |
| | | </CoverView> |
| | | ) |
| | | }) |
| | | } |
| | | { |
| | | (tripStatus === 2 || tripStatus === 3) && markers.map((item) => { |
| | | if (item.id === 1) { |
| | | return ( |
| | | <CoverView slot="callout" marker-id={item.id} key={item.id} className="waiting-callout"> |
| | | {tripStatus === 3 && ( |
| | | <CoverView className="waiting-radiation-box"> |
| | | <CoverView className="waiting-radiation-ring"></CoverView> |
| | | <CoverView className="waiting-radiation-ring"></CoverView> |
| | | </CoverView> |
| | | )} |
| | | <CoverView className="waiting-callout-bg"> |
| | | <CoverImage className="waiting-callout-bg-img" src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f55ce277-68de-4615-9c7c-807f588afaac.png" /> |
| | | <CoverView className="waiting-callout-bg-text">{formatTime(elapsedSeconds)}</CoverView> |
| | | </CoverView> |
| | | <CoverView className="waiting-callout-txt"> |
| | | <CoverView className="waiting-callout-txt-content">{item.name}</CoverView> |
| | | </CoverView> |
| | | </CoverView> |
| | | ) |
| | | } |
| | | return null |
| | | }) |
| | | } |
| | | {/* 选择城市 */} |
| | | <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={contactService} /> |
| | | <View className="split-line"></View> |
| | | <Image className="map-entry-icon" src={coupon} /> |
| | | <View className="split-line"></View> |
| | | <Image className="map-entry-icon" src={suggest} /> |
| | | </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()}> |
| | | {/* 发布路线 */} |
| | | { |
| | | tripStatus == 1 && <ReleaseRoute callImmediately={callImmediately} /> |
| | | } |
| | | {/* 发布中弹窗 */} |
| | | { |
| | | tripStatus == 2 && <ReleaseBeBeing handleOpenCancelPopup={handleOpenCancelPopup} onFinish={() => setTripStatus(3)} /> |
| | | } |
| | | {/* 呼叫车辆弹窗 */} |
| | | { |
| | | tripStatus >= 3 && |
| | | <CallingCar |
| | | key={tripStatus} |
| | | tripStatus={tripStatus} |
| | | handleOpenCancelPopup={handleOpenCancelPopup} |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | | handlePay={handlePay} |
| | | updateStatus={() => setTripStatus(prev => prev + 1)} |
| | | /> |
| | | } |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </Map> |
| | | {/* 客服弹窗 */} |
| | | <CustomerServicePopup |
| | | show={showCustomerService} |
| | | onClose={handleCloseCustomerService} |
| | | /> |
| | | {/* 取消弹窗 */} |
| | | <CancelPopup |
| | | show={showCancelPopup} |
| | | cancelType={cancelType} |
| | | onClose={handleCloseCancelPopup} |
| | | onConfirm={handleConfirmCancel} |
| | | /> |
| | | {/* 安全注意事项弹窗 */} |
| | | <AnnouncementsPopup |
| | | show={showAnnouncementsPopup} |
| | | onClose={handleCloseAnnouncementsPopup} |
| | | /> |
| | | {/* 结束行程弹窗 */} |
| | | <OverRoute |
| | | show={showOverRoutePopup} |
| | | onClose={handleCloseOverRoutePopup} |
| | | onConfirm={handleConfirmOverRoute} |
| | | /> |
| | | {/* 乘车需知弹窗 */} |
| | | <PassengerNotice |
| | | show={showPassengerNotice} |
| | | onClose={handleClosePassengerNotice} |
| | | /> |
| | | </View> |
| | | ) |
| | | } |
| | |
| | | <View className='tip-popup' catchMove> |
| | | <View className='tip-mask'></View> |
| | | <View className='tip-content' animation={animationData}> |
| | | <Image className='close-icon' src={require('../../../assets/close-icon.png')} /> |
| | | <Image onClick={onClose} className='close-icon' src={require('../../../assets/close-icon.png')} /> |
| | | <View className='tip-title'>{tipType === '1' ? '上车点与目的地距离较近,建议步行前往' : '当前位置距上车点较远,暂不支持叫车,请重新选择上车点'}</View> |
| | | <View onClick={onClose} className='reselect-btn'>重新选点</View> |
| | | </View> |
| | |
| | | } |
| | | } |
| | | |
| | | @keyframes radiate { |
| | | 0% { |
| | | transform: translate(-50%, -50%) scale(1); |
| | | opacity: 1; |
| | | } |
| | | |
| | | 100% { |
| | | transform: translate(-50%, -50%) scale(25); |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | @keyframes pointer-bounce { |
| | | 0% { |
| | | transform: translateY(0); |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .custom-callout { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .custom-callout-content { |
| | | display: flex; |
| | | align-items: center; |
| | | border-radius: 16px; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | padding: 12px 12px 12px 20px; |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | line-height: 48px; |
| | | box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2); |
| | | } |
| | | |
| | | .custom-callout-icon { |
| | | margin-left: 8px; |
| | | width: 32px; |
| | | height: 32px; |
| | | flex-shrink: 0; |
| | | display: block; |
| | | } |
| | | |
| | | .waiting-callout { |
| | | position: relative; |
| | | z-index: 10; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | justify-content: center; |
| | | overflow: visible; // 确保辐射动画不被裁剪 |
| | | } |
| | | |
| | | .waiting-radiation-box { |
| | | position: absolute; |
| | | top: 100%; // 移到气泡下方 |
| | | left: 50%; |
| | | margin-top: 30px; // 向下偏移以对齐Marker锚点(预估值) |
| | | width: 0; |
| | | height: 0; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | pointer-events: none; |
| | | overflow: visible; |
| | | } |
| | | |
| | | @keyframes ripple { |
| | | 0% { |
| | | opacity: 0.6; |
| | | transform: translate(-50%, -50%) scale(0.2); |
| | | } |
| | | |
| | | 100% { |
| | | opacity: 0; |
| | | transform: translate(-50%, -50%) scale(2.5); |
| | | } |
| | | } |
| | | |
| | | .waiting-radiation-ring { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 200px; |
| | | height: 200px; |
| | | border-radius: 50%; |
| | | background: rgba(26, 79, 255, 0.05); |
| | | border: 1px solid rgba(26, 79, 255, 0.2); |
| | | opacity: 0; // 初始不可见,由动画控制 |
| | | animation: ripple 3.5s infinite linear; |
| | | |
| | | &:nth-child(1) { |
| | | animation-delay: 0s; |
| | | } |
| | | |
| | | &:nth-child(2) { |
| | | animation-delay: 1s; |
| | | } |
| | | } |
| | | |
| | | |
| | | .waiting-callout-bg { |
| | | position: relative; |
| | | height: 70px; |
| | | min-width: 155px; |
| | | text-align: center; |
| | | width: fit-content; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .waiting-callout-bg-img { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .waiting-callout-bg-text { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | z-index: 100; |
| | | font-size: 22px; |
| | | font-weight: 500; |
| | | letter-spacing: normal; |
| | | color: #FFFFFF; |
| | | line-height: 48px; |
| | | padding: 0 20px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .waiting-callout-txt { |
| | | position: relative; |
| | | z-index: 200; |
| | | margin-top: -26px; |
| | | display: flex; |
| | | align-items: center; |
| | | border-radius: 16px; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | padding: 12px 12px 12px 20px; |
| | | min-width: 155px; |
| | | } |
| | | |
| | | .waiting-callout-txt-content { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | line-height: 48px; |
| | | } |
| | |
| | | import Taro from "@tarojs/taro" |
| | | import { useEffect, useState } from 'react'; |
| | | import { View, Map, Text, Image, CoverView, CoverImage } from '@tarojs/components'; |
| | | import { View, Map, Text, Image } from '@tarojs/components'; |
| | | import './index.less'; |
| | | |
| | | import CustomerServicePopup from '../../components/customer-service'; |
| | | import ReleaseRoute from './components/release-route'; |
| | | import TipPopup from './components/tip-popup'; |
| | | import ReleaseBeBeing from './components/release-be-being'; |
| | | import CancelPopup from './components/cancel-popup'; |
| | | import CallingCar from './components/calling-car'; |
| | | import AnnouncementsPopup from './components/announcements-popup'; |
| | | import OverRoute from './components/over-route'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png' |
| | | import contactService from '../../../robotaxi/assets/contact-service.png' |
| | |
| | | import originIcon from '../../../robotaxi/assets/origin-icon.png' |
| | | import pointer from '../../../robotaxi/assets/pointer.png' |
| | | import rightIcon from '../../../robotaxi/assets/right-icon.png' |
| | | import { findNearestMarker, parseRouteToPoints, getDistance } from '../../../utils/Robotaxi-public-method'; |
| | | import amapFile from '../../../libs/amap-wx.130'; |
| | | import { AMAP_KEY } from '../../../constants/api'; |
| | | import { findNearestMarker, getDistance } from '../../../utils/Robotaxi-public-method'; |
| | | |
| | | export default function SelectMapSite() { |
| | | |
| | |
| | | const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示 |
| | | const [bounce, setBounce] = useState(false); // 是否弹跳 |
| | | const [markers, setMarkers] = useState<any[]>([]); // 标记点列表 |
| | | const [polyline, setPolyline] = useState<any[]>([]); // 路线 |
| | | const [pageType, setPageType] = useState('end'); // 起点start 终点end |
| | | const [selectedSite, setSelectedSite] = useState<any>({}); // 选择的站点 |
| | | const [tripStatus, setTripStatus] = useState(0); // 行程状态 0:默认状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功 |
| | | const [showTipPopup, setShowTipPopup] = useState(false); // 提示弹窗是否显示 |
| | | const [tipType, setTipType] = useState(''); // 提示类型 1:上下车点近 2:距上车点远 |
| | | const [startSite, setStartSite] = useState<any>(null); // 上车点 |
| | | const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数 |
| | | const [showCancelPopup, setShowCancelPopup] = useState(false); // 取消弹窗是否显示 |
| | | const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false); // 安全注意事项动画弹窗是否显示 |
| | | const [showOverRoutePopup, setShowOverRoutePopup] = useState(false); // 结束行程弹窗是否显示 |
| | | |
| | | const router = Taro.useRouter(); |
| | | |
| | | useEffect(() => { |
| | | Taro.setStorageSync('startSite', startSite); |
| | | }, [tripStatus]); |
| | | |
| | | // 校验上车点是否存在 |
| | | useEffect(() => { |
| | |
| | | setStartSite(s); |
| | | } |
| | | }, []); |
| | | |
| | | // 监听行程状态,处理等待计时 |
| | | useEffect(() => { |
| | | let timer: any = null; |
| | | if (tripStatus === 2 || tripStatus === 3) { |
| | | // 状态为2或3时,将地图中心定位到上车点 |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if (s && s.latitude && s.longitude) { |
| | | setLatitude(s.latitude); |
| | | setLongitude(s.longitude); |
| | | |
| | | // 增加延迟,确保地图上下文就绪 |
| | | setTimeout(() => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation({ |
| | | latitude: s.latitude, |
| | | longitude: s.longitude |
| | | }); |
| | | }, 500); |
| | | } |
| | | |
| | | setElapsedSeconds(0); |
| | | timer = setInterval(() => { |
| | | setElapsedSeconds(prev => prev + 1); |
| | | }, 1000); |
| | | } else { |
| | | setElapsedSeconds(0); |
| | | } |
| | | return () => { |
| | | if (timer) clearInterval(timer); |
| | | }; |
| | | }, [tripStatus]); |
| | | |
| | | // 页面初始化 |
| | | useEffect(() => { |
| | |
| | | height: 15, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '中国南方电网·5号门' |
| | | }, |
| | | { |
| | | id: 6, |
| | | latitude: 30.584992, |
| | | longitude: 104.054063, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png', |
| | | width: 15, |
| | | height: 15, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '冯校长老火锅' |
| | | } |
| | | ]; |
| | | setMarkers(defaultMarkers); |
| | | |
| | | getLocationInfo(); |
| | | }, []); |
| | | |
| | | // 规划路线 |
| | | useEffect(() => { |
| | | if (tripStatus === 1 && pageType === 'end' && selectedSite.longitude && longitude) { |
| | | const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY }); |
| | | myAmapFun.getDrivingRoute({ |
| | | origin: `${longitude},${latitude}`, |
| | | destination: `${selectedSite.longitude},${selectedSite.latitude}`, |
| | | success: function (data) { |
| | | const points = parseRouteToPoints(data); |
| | | |
| | | setPolyline([{ |
| | | points: points, |
| | | color: '#6D7FD9', |
| | | width: 10, |
| | | arrowLine: true, |
| | | }]) |
| | | |
| | | // 设置起点和终点标记 |
| | | if (points.length > 0) { |
| | | const startPoint = points[0]; |
| | | const endPoint = points[points.length - 1]; |
| | | |
| | | // 检查 storage 中的 startSite 是否存在 |
| | | const s = Taro.getStorageSync('startSite'); |
| | | |
| | | setMarkers([ |
| | | // 起点指针 |
| | | { |
| | | id: 1, |
| | | latitude: startPoint.latitude, |
| | | longitude: startPoint.longitude, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png', |
| | | width: 22, |
| | | height: 40, |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 2, |
| | | name: s?.name || '当前位置', |
| | | customCallout: { |
| | | anchorY: 8, |
| | | anchorX: 0, |
| | | display: 'ALWAYS' |
| | | } |
| | | }, |
| | | // 终点指针 |
| | | { |
| | | id: 2, |
| | | latitude: endPoint.latitude, |
| | | longitude: endPoint.longitude, |
| | | iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png', |
| | | width: 22, |
| | | height: 40, |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 2, |
| | | name: selectedSite.name, |
| | | customCallout: { |
| | | anchorY: 8, |
| | | anchorX: 0, |
| | | display: 'ALWAYS' |
| | | } |
| | | }, |
| | | ]) |
| | | |
| | | // 调整视角以包含所有路线点 |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.includePoints({ |
| | | points: points, |
| | | padding: [50, 50, 300, 50] |
| | | }) |
| | | } |
| | | }, |
| | | fail: function (info) { |
| | | console.log('规划失败', info) |
| | | } |
| | | }) |
| | | } else if (tripStatus !== 2 && tripStatus !== 3) { |
| | | setPolyline([]) |
| | | } |
| | | }, [tripStatus, pageType, selectedSite, longitude, latitude]) |
| | | |
| | | const formatTime = (sec: number) => { |
| | | const m = Math.floor(sec / 60); |
| | | const s = sec % 60; |
| | | return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; |
| | | } |
| | | |
| | | // 获取用户位置信息 |
| | | const getLocationInfo = () => { |
| | |
| | | |
| | | // 地图区域变化时触发,判断是否吸附到最近的站点停靠点 |
| | | const handleRegionChange = (e: any) => { |
| | | if (tripStatus === 1 || tripStatus === 2) return |
| | | const type = e?.type || e?.detail?.type; |
| | | // 获取触发源: gesture (手势), update (接口调用) |
| | | const causedBy = e?.causedBy || e?.detail?.causedBy; |
| | |
| | | |
| | | |
| | | const toBack = () => { |
| | | if (tripStatus == 0) { |
| | | Taro.navigateBack({ |
| | | delta: 1, |
| | | }) |
| | | } else { |
| | | Taro.switchTab({ |
| | | url: '/pages/robotaxi/home/index' |
| | | }) |
| | | } |
| | | Taro.navigateBack({ |
| | | delta: 1, |
| | | }) |
| | | } |
| | | |
| | | const confirmSite = () => { |
| | |
| | | return; |
| | | } |
| | | } |
| | | setTripStatus(1) |
| | | } |
| | | } |
| | | |
| | | // 立即呼叫 |
| | | const callImmediately = () => { |
| | | if (!selectedSite.name) { |
| | | Taro.showToast({ |
| | | title: '请选择站点', |
| | | icon: 'none', |
| | | Taro.setStorageSync('endSite', { |
| | | latitude: selectedSite.latitude, |
| | | longitude: selectedSite.longitude, |
| | | name: selectedSite.name, |
| | | }) |
| | | return |
| | | } |
| | | setTripStatus(2) |
| | | // 校验上车点是否存在 |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if (s && s.latitude && s.longitude) { |
| | | setStartSite(s); |
| | | |
| | | // 移动地图中心到上车点 |
| | | setLatitude(s.latitude); |
| | | setLongitude(s.longitude); |
| | | |
| | | // 确保显示 Tooltip |
| | | setShowToolTip(true); |
| | | Taro.reLaunch({ |
| | | url: '/robotaxi/pages/begin-route/index', |
| | | }) |
| | | } |
| | | } |
| | | |
| | |
| | | setSelectedSite({ name: item }) |
| | | } |
| | | |
| | | // 确认取消 |
| | | const handleConfirmCancel = () => { |
| | | setShowCancelPopup(false); |
| | | setTripStatus(0); |
| | | } |
| | | |
| | | // 关闭取消弹窗 |
| | | const handleCloseCancelPopup = () => { |
| | | setShowCancelPopup(false); |
| | | } |
| | | |
| | | // 打开取消弹窗 |
| | | const handleOpenCancelPopup = () => { |
| | | setShowCancelPopup(true); |
| | | } |
| | | |
| | | // 关闭安全注意事项弹窗 |
| | | const handleCloseAnnouncementsPopup = () => { |
| | | setShowAnnouncementsPopup(false); |
| | | } |
| | | |
| | | // 打开结束行程弹窗 |
| | | const handleOpenOverRoutePopup = () => { |
| | | setShowOverRoutePopup(true); |
| | | } |
| | | |
| | | // 关闭结束行程弹窗 |
| | | const handleCloseOverRoutePopup = () => { |
| | | setShowOverRoutePopup(false); |
| | | } |
| | | |
| | | // 确认结束行程 |
| | | const handleConfirmOverRoute = () => { |
| | | setShowOverRoutePopup(false); |
| | | setTripStatus(8); |
| | | } |
| | | |
| | | // 确认支付 |
| | | const handlePay = () => { |
| | | setTripStatus(9); |
| | | } |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | |
| | | latitude={latitude} |
| | | scale={14} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | showLocation={!!longitude} |
| | | onRegionChange={handleRegionChange} |
| | | style={{ width: '100%', height: '100%' }} |
| | | > |
| | | {/* 自定义callout */} |
| | | { |
| | | tripStatus === 1 && markers.map((item) => { |
| | | return ( |
| | | <CoverView slot="callout" marker-id={item.id} key={item.id} className="custom-callout"> |
| | | <CoverView className="custom-callout-content"> |
| | | {item.name} <CoverImage className="custom-callout-icon" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f9e285fc-4c09-404d-90f8-4bac178565d0.png' /> |
| | | </CoverView> |
| | | </CoverView> |
| | | ) |
| | | }) |
| | | } |
| | | { |
| | | (tripStatus === 2 || tripStatus === 3) && markers.map((item) => { |
| | | if (item.id === 1) { |
| | | return ( |
| | | <CoverView slot="callout" marker-id={item.id} key={item.id} className="waiting-callout"> |
| | | {tripStatus === 3 && ( |
| | | <CoverView className="waiting-radiation-box"> |
| | | <CoverView className="waiting-radiation-ring"></CoverView> |
| | | <CoverView className="waiting-radiation-ring"></CoverView> |
| | | </CoverView> |
| | | )} |
| | | <CoverView className="waiting-callout-bg"> |
| | | <CoverImage className="waiting-callout-bg-img" src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f55ce277-68de-4615-9c7c-807f588afaac.png" /> |
| | | <CoverView className="waiting-callout-bg-text">{formatTime(elapsedSeconds)}</CoverView> |
| | | </CoverView> |
| | | <CoverView className="waiting-callout-txt"> |
| | | <CoverView className="waiting-callout-txt-content">{item.name}</CoverView> |
| | | </CoverView> |
| | | </CoverView> |
| | | ) |
| | | } |
| | | return null |
| | | }) |
| | | } |
| | | {/* 选择城市 */} |
| | | <View onClick={toBack} className="select-city"> |
| | | <Image className="select-city-icon" src={backIcon} /> |
| | |
| | | <Image className="map-entry-icon" src={suggest} /> |
| | | </View> |
| | | {/* 指针 */} |
| | | { |
| | | tripStatus === 0 && <View className="pointer-box"> |
| | | {(showToolTip && selectedSite.name) && ( |
| | | <View className="toolTip"> |
| | | <View className="toolTip-bg"> |
| | | <Text className="toolTip-bg-text">'限停3分钟'</Text> |
| | | </View> |
| | | <View className="toolTip-txt"> |
| | | <View className="toolTip-txt-content">{selectedSite.name}</View> |
| | | <Image className="toolTip-icon" src={rightIcon} /> |
| | | </View> |
| | | <View className="pointer-box"> |
| | | {(showToolTip && selectedSite.name) && ( |
| | | <View className="toolTip"> |
| | | <View className="toolTip-bg"> |
| | | <Text className="toolTip-bg-text">'限停3分钟'</Text> |
| | | </View> |
| | | )} |
| | | <Image |
| | | className={`pointer-icon ${bounce ? 'bounce' : ''}`} |
| | | src={pointer} |
| | | /> |
| | | </View> |
| | | } |
| | | <View className="toolTip-txt"> |
| | | <View className="toolTip-txt-content">{selectedSite.name}</View> |
| | | <Image className="toolTip-icon" src={rightIcon} /> |
| | | </View> |
| | | </View> |
| | | )} |
| | | <Image |
| | | className={`pointer-icon ${bounce ? 'bounce' : ''}`} |
| | | src={pointer} |
| | | /> |
| | | </View> |
| | | <View className="container"> |
| | | <View className="container-inner"> |
| | | {/* 复位按钮 */} |
| | |
| | | <View catchMove onClick={(e) => e.stopPropagation()}> |
| | | {/* 确认站点 */} |
| | | { |
| | | (tripStatus == 0 && selectedSite.name) && <View className="content-box"> |
| | | selectedSite.name && <View className="content-box"> |
| | | <View className="content-limit-bg"></View> |
| | | <View className="content-title">{pageType === 'start' ? '确认起点' : '确认目的地'}</View> |
| | | <View className="limit-tip">目的地附近道路有<Text className="emphasis-txt">通行限制</Text>,为您推荐适合的下车位置</View> |
| | |
| | | </View> |
| | | <View onClick={confirmSite} className="site-confirm">{pageType === 'start' ? '确认起点站点' : '确认目的地站点'}</View> |
| | | </View> |
| | | } |
| | | {/* 发布路线 */} |
| | | { |
| | | tripStatus == 1 && <ReleaseRoute callImmediately={callImmediately} /> |
| | | } |
| | | {/* 发布中弹窗 */} |
| | | { |
| | | tripStatus == 2 && <ReleaseBeBeing handleOpenCancelPopup={handleOpenCancelPopup} onFinish={() => setTripStatus(3)} /> |
| | | } |
| | | {/* 呼叫车辆弹窗 */} |
| | | { |
| | | tripStatus >= 3 && |
| | | <CallingCar |
| | | key={tripStatus} |
| | | tripStatus={tripStatus} |
| | | handleOpenCancelPopup={handleOpenCancelPopup} |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | | handlePay={handlePay} |
| | | updateStatus={() => setTripStatus(prev => prev + 1)} |
| | | /> |
| | | } |
| | | </View> |
| | | </View> |
| | |
| | | show={showTipPopup} |
| | | onClose={handleCloseTipPopup} |
| | | tipType={tipType} |
| | | /> |
| | | {/* 取消弹窗 */} |
| | | <CancelPopup |
| | | show={showCancelPopup} |
| | | onClose={handleCloseCancelPopup} |
| | | onConfirm={handleConfirmCancel} |
| | | /> |
| | | {/* 安全注意事项弹窗 */} |
| | | <AnnouncementsPopup |
| | | show={showAnnouncementsPopup} |
| | | onClose={handleCloseAnnouncementsPopup} |
| | | /> |
| | | {/* 结束行程弹窗 */} |
| | | <OverRoute |
| | | show={showOverRoutePopup} |
| | | onClose={handleCloseOverRoutePopup} |
| | | onConfirm={handleConfirmOverRoute} |
| | | /> |
| | | </View> |
| | | ) |
| | |
| | | } |
| | | ]) // 非运营区域站点 |
| | | const [searchValue, setSearchValue] = useState('') //搜索值 |
| | | const [pageType, setPageType] = useState('end') //出发地 start 目的地 end |
| | | const [pageType, setPageType] = useState('') //出发地 start 目的地 end |
| | | const [showAllHistory, setShowAllHistory] = useState(false) // 是否展示全部历史记录 |
| | | const [showAllRecommend, setShowAllRecommend] = useState(false) // 是否展示全部推荐站点 |
| | | |
| | | const MAX_SHOW_COUNT = 5 // 默认最多显示5条 |
| | | |
| | | const router = Taro.useRouter(); |
| | | |
| | | useEffect(() => { |
| | | console.log(router.params.pageType); |
| | | |
| | | if (router.params.pageType) { |
| | | setPageType(router.params.pageType); |
| | | } |
| | | }, []) |
| | | |
| | | const searchSite = () => { |
| | | console.log(searchValue) |
| | |
| | | } |
| | | |
| | | |
| | | const toSelectSite = () => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/select-map-site/index?pageType=' + pageType, |
| | | }) |
| | | } |
| | | const toSelectSite = () => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/select-map-site/index?pageType=' + pageType, |
| | | }) |
| | | } |
| | | |
| | | |
| | | return ( |