feat(robotaxi): 完善行程发布与评价功能,优化地图交互
- 新增上传图标资源用于评价弹窗
- 修复城市选择页面的标题错别字
- 重构地图选点页面,合并初始化逻辑并移除冗余图标
- 在行程发布组件中动态显示起终点站点名称
- 扩展车辆呼叫组件,支持打开评价弹窗并默认选中“很满意”
- 实现完整的评价弹窗,包含问题类型选择、描述输入和图片上传
- 优化城市选择页面的数据过滤和选中状态管理
- 重构行程开始页面,改进地图标记、路线规划和弹窗管理逻辑
- 增强主页地图交互,实现站点吸附、位置反编码和行程状态跳转
| | |
| | | const [isMaintain, setIsMaintain] = useState(false); // 是否维护中 |
| | | const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内 |
| | | const [tripStatus, setTripStatus] = useState<number>(0); // 行程状态 |
| | | const [currentSite, setCurrentSite] = useState<any>({}); // 当前站点 |
| | | const [currentAddress, setCurrentAddress] = useState(''); // 当前位置地址 |
| | | const [isAdsorbed, setIsAdsorbed] = useState(true); // 是否吸附到站点 |
| | | |
| | | const [boxHeight, setBoxHeight] = useState<number | string>('auto'); // 活动区域高度 |
| | | const startY = useRef(0); // 拖动开始时的Y坐标 |
| | |
| | | const fullHeight = useRef(0); // 活动区域最大高度 |
| | | const minHeight = useRef(0); // 活动区域最小高度 |
| | | const isDragging = useRef(false); // 是否正在拖动 |
| | | const shouldIgnoreRegionChange = useRef(false); // 是否忽略地图区域变化 |
| | | |
| | | const [selectedCity, setSelectedCity] = useState(''); // 选中的城市 |
| | | |
| | |
| | | if (tripStatus) { |
| | | setTripStatus(Number(tripStatus)); |
| | | } |
| | | |
| | | Taro.getSetting({ |
| | | success: (res) => { |
| | | const auth = res.authSetting['scope.userLocation']; |
| | | if (auth === false) { |
| | | setIsLocationAuthDenied(true); |
| | | } else { |
| | | // 检查缓存中是否有 startSite |
| | | const startSite = Taro.getStorageSync('startSite'); |
| | | if (startSite && startSite.latitude && startSite.longitude) { |
| | | setLatitude(startSite.latitude); |
| | | setLongitude(startSite.longitude); |
| | | setCurrentSite(startSite); |
| | | setIsAdsorbed(true); |
| | | shouldIgnoreRegionChange.current = true; |
| | | } else { |
| | | getLocationInfo(true); |
| | | } |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('获取位置授权信息失败', err); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | useEffect(() => { |
| | |
| | | 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 } |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '中国南方电网·2号门' |
| | | }, |
| | | { |
| | | id: 2, |
| | |
| | | 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 } |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '天河体育中心' |
| | | }, |
| | | { |
| | | id: 3, |
| | |
| | | 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 } |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '太古汇' |
| | | }, |
| | | { |
| | | id: 4, |
| | |
| | | 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 } |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '广州东站' |
| | | }, |
| | | { |
| | | id: 5, |
| | |
| | | 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 } |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | name: '广州南站' |
| | | } |
| | | ]; |
| | | setMarkers(defaultMarkers); |
| | |
| | | } |
| | | }).exec(); |
| | | }, 500); |
| | | |
| | | Taro.getSetting({ |
| | | success: (res) => { |
| | | const auth = res.authSetting['scope.userLocation']; |
| | | if (auth === false) { |
| | | setIsLocationAuthDenied(true); |
| | | } else { |
| | | getLocationInfo(); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('获取位置授权信息失败', err); |
| | | } |
| | | }); |
| | | }, []); |
| | | |
| | | // 获取用户位置信息 |
| | | const getLocationInfo = () => { |
| | | const getLocationInfo = (updateCenter = true) => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | if (updateCenter) { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | } |
| | | const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY }); |
| | | |
| | | // 获取当前位置的逆地理编码 |
| | | myAmapFun.getRegeo({ |
| | | location: `${res.longitude},${res.latitude}`, |
| | | success: (data) => { |
| | | if (data && data[0]) { |
| | | const address = data[0].name || data[0].desc; |
| | | setCurrentAddress(address); |
| | | setCurrentSite({ |
| | | name: address, |
| | | latitude: res.latitude, |
| | | longitude: res.longitude |
| | | }); |
| | | setIsAdsorbed(false); |
| | | } |
| | | }, |
| | | fail: (info) => { |
| | | console.error('getRegeo fail', info); |
| | | setCurrentAddress('未知位置'); |
| | | setCurrentSite({ name: '未知位置' }); |
| | | } |
| | | }); |
| | | |
| | | myAmapFun.getPoiAround({ |
| | | location: `${res.latitude},${res.longitude}`, |
| | | location: `${res.longitude},${res.latitude}`, |
| | | success: () => { }, |
| | | fail: () => { } |
| | | }); |
| | |
| | | |
| | | // 返回当前位置 |
| | | const toOrigin = () => { |
| | | shouldIgnoreRegionChange.current = false; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation(); |
| | | } |
| | |
| | | // 获取触发源: gesture (手势), update (接口调用) |
| | | const causedBy = e?.causedBy || e?.detail?.causedBy; |
| | | |
| | | // 如果是用户手势拖动,则必须触发,且重置忽略标记 |
| | | if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') { |
| | | shouldIgnoreRegionChange.current = false; |
| | | } else { |
| | | shouldIgnoreRegionChange.current = true; |
| | | return |
| | | } |
| | | |
| | | 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: (res) => { |
| | | const { latitude: lat, longitude: lng } = res; |
| | | |
| | | const { targetMarker, minDistance } = findNearestMarker(lat, lng, markers); |
| | | |
| | | // 如果最近的点在一定范围内(例如 200米),则吸附 |
| | | if (targetMarker && minDistance < 200) { |
| | | // 使用 moveToLocation 实现吸附效果 |
| | | mapCtx.moveToLocation({ |
| | | latitude: targetMarker.latitude, |
| | | longitude: targetMarker.longitude, |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | // 如果标记为忽略且非手势触发,则直接返回 |
| | | if (shouldIgnoreRegionChange.current) { |
| | | shouldIgnoreRegionChange.current = false; |
| | | return; |
| | | } |
| | | |
| | | setTimeout(() => { |
| | | setBounce(false); |
| | | setShowToolTip(true); |
| | | }, 600); |
| | | setBounce(true); |
| | | |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.getCenterLocation({ |
| | | success: (res) => { |
| | | const { latitude: lat, longitude: lng } = res; |
| | | |
| | | const { targetMarker, minDistance } = findNearestMarker(lat, lng, markers); |
| | | |
| | | // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环 |
| | | if ((causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') && targetMarker && minDistance < 200) { |
| | | // 使用 moveToLocation 实现吸附效果 |
| | | mapCtx.moveToLocation({ |
| | | latitude: targetMarker.latitude, |
| | | longitude: targetMarker.longitude, |
| | | }); |
| | | } |
| | | |
| | | // 根据距离决定是否显示 ToolTip |
| | | setTimeout(() => { |
| | | setBounce(false); |
| | | if (minDistance < 200) { |
| | | setIsAdsorbed(true); |
| | | if (targetMarker) { |
| | | setCurrentSite(targetMarker); |
| | | } |
| | | setShowToolTip(true); |
| | | } else { |
| | | setIsAdsorbed(false); |
| | | setCurrentSite({ name: '正在获取位置...' }); |
| | | // 获取当前位置的逆地理编码 |
| | | const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY }); |
| | | myAmapFun.getRegeo({ |
| | | location: `${lng},${lat}`, |
| | | success: (data) => { |
| | | if (data && data[0]) { |
| | | const address = data[0].name || data[0].desc; |
| | | setCurrentAddress(address); |
| | | setCurrentSite({ name: address, latitude: lat, longitude: lng }); |
| | | } |
| | | }, |
| | | fail: (info) => { |
| | | console.error('getRegeo fail', info); |
| | | setCurrentAddress('未知位置'); |
| | | setCurrentSite({ name: '未知位置' }); |
| | | } |
| | | }); |
| | | setShowToolTip(true); |
| | | } |
| | | }, 600); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | |
| | | }) |
| | | } |
| | | |
| | | const toBeginRoute = () => { |
| | | if (currentSite && currentSite.name) { |
| | | Taro.setStorageSync('startSite', currentSite); |
| | | } |
| | | Taro.setStorageSync('endSite', { |
| | | latitude: 30.571100, |
| | | longitude: 104.065200, |
| | | name: '中国南方电网·3号门' |
| | | }); |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/begin-route/index', |
| | | }) |
| | | } |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | |
| | | </View> |
| | | {/* 指针 */} |
| | | <View className="pointer-box"> |
| | | {showToolTip && ( |
| | | <View className="toolTip"> |
| | | <View className="toolTip-bg"> |
| | | <Text className="toolTip-bg-text">限停3分钟</Text> |
| | | { |
| | | showToolTip && ( |
| | | <View className="toolTip"> |
| | | { |
| | | isAdsorbed && ( |
| | | <View className="toolTip-bg"> |
| | | <Text className="toolTip-bg-text">限停3分钟</Text> |
| | | </View> |
| | | ) |
| | | } |
| | | <View className="toolTip-txt"> |
| | | <View className="toolTip-txt-content">{isAdsorbed ? currentSite.name : currentAddress}</View> |
| | | {isAdsorbed && <Image className="toolTip-icon" src={rightIcon} />} |
| | | </View> |
| | | </View> |
| | | <View className="toolTip-txt"> |
| | | <View className="toolTip-txt-content">中国南方电网·2号门</View> |
| | | <Image className="toolTip-icon" src={rightIcon} /> |
| | | </View> |
| | | </View> |
| | | )} |
| | | ) |
| | | } |
| | | <Image className={`pointer-icon ${bounce ? 'bounce' : ''}`} src={pointer} /> |
| | | </View> |
| | | {/* 复位按钮 */} |
| | |
| | | <View onClick={() => toSeleteSitePage('start')} className="now-site"> |
| | | <View className="now-site-dot"></View> |
| | | <View className="now-site-address"> |
| | | <View className="now-site-address-content"> |
| | | 从<Text className="now-site-address-text">中国南方电网·2号门</Text>上车 |
| | | </View> |
| | | { |
| | | currentSite.name ? |
| | | <View className="now-site-address-content"> |
| | | 从<Text className="now-site-address-text">{currentSite.name}</Text>出发 |
| | | </View> |
| | | : |
| | | <View className="now-site-address-content"> |
| | | <Text className="now-site-address-text">请选择上车点</Text> |
| | | </View> |
| | | } |
| | | <Image className="now-site-icon" src={rightIcon} /> |
| | | </View> |
| | | </View> |
| | |
| | | <View className="to-site-dot"></View> |
| | | <View className="to-site-address"> |
| | | <View className="to-site-text">你想去哪?</View> |
| | | <View className="to-site-fast">去:体育西·珠江新城</View> |
| | | <View onClick={toBeginRoute} className="to-site-fast">去:中国南方电网·3号门</View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | |
| | | { |
| | | [1, 2].map(item => ( |
| | | <View key={item} className="activity-item"> |
| | | <Image className="activity-img" src={'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/436c0306-d22c-4b60-bc1c-4cf01fe20f4b.png'} /> |
| | | <Image mode="aspectFill" className="activity-img" src={'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/436c0306-d22c-4b60-bc1c-4cf01fe20f4b.png'} /> |
| | | </View> |
| | | )) |
| | | } |
| | |
| | | handleOpenOverRoutePopup?: () => void; |
| | | handlePay?: () => void; |
| | | updateStatus?: () => void; |
| | | handleOpenEvaluatePopup?: () => void; |
| | | } |
| | | |
| | | export default function CallingCar({ tripStatus = 3, handleOpenCancelPopup, handleOpenOverRoutePopup, handlePay, updateStatus }: Props) { |
| | | export default function CallingCar({ tripStatus = 3, handleOpenCancelPopup, handleOpenOverRoutePopup, handlePay, updateStatus, handleOpenEvaluatePopup }: Props) { |
| | | |
| | | const [trunkStatus, setTrunkStatus] = useState(false); |
| | | const [lockStatus, setLockStatus] = useState(false); |
| | | const [tempStatus, setTempStatus] = useState(19); |
| | | const [showDetail, setShowDetail] = useState(false); |
| | | const [activeIndex, setActiveIndex] = useState(3) // 默认选中"很满意" |
| | | const [activeIndex, setActiveIndex] = useState(0) // 默认选中"很满意" |
| | | |
| | | // 使用 useRef 保存最新的 updateStatus 回调,避免因父组件重绘导致定时器重置 |
| | | const updateStatusRef = useRef(updateStatus); |
| | |
| | | 6: { text: '行程已开始,请全程系好安全带', className: 'calling-car-content-desc' }, |
| | | 7: { text: '倒计时结束时车辆将自动驶离,请带好物品安全下车', className: 'calling-car-content-desc' }, |
| | | 8: { text: '订单待支付,请核对后完成支付', className: 'calling-car-content-desc' } |
| | | } |
| | | |
| | | // 切换评价选项 |
| | | const changeEvaluate = (index: number) => { |
| | | setActiveIndex(index); |
| | | if (index == 1) { |
| | | handleOpenEvaluatePopup?.() |
| | | } |
| | | } |
| | | |
| | | return ( |
| | |
| | | <View className='car-card-evaluate'> |
| | | <View className='car-card-evaluate-title'>您对本次行程满意吗?</View> |
| | | <View className='car-card-evaluate-content'> |
| | | <View className='car-card-evaluate-content-item' onClick={() => { setActiveIndex(1) }}> |
| | | <View className='car-card-evaluate-content-item' onClick={() => changeEvaluate(1)}> |
| | | <Image src={activeIndex == 1 ? activeBad : bad} className='car-card-evaluate-content-item-img' /> |
| | | <Text className={`car-card-evaluate-content-item-text ${activeIndex == 1 ? 'active-text' : ''}`}>很不满意</Text> |
| | | </View> |
| | | <View className='car-card-evaluate-content-item' onClick={() => { setActiveIndex(2) }}> |
| | | <View className='car-card-evaluate-content-item' onClick={() => changeEvaluate(2)}> |
| | | <Image src={activeIndex == 2 ? activeNormal : normal} className='car-card-evaluate-content-item-img' /> |
| | | <Text className={`car-card-evaluate-content-item-text ${activeIndex == 2 ? 'active-text' : ''}`}>一般般</Text> |
| | | </View> |
| | | <View className='car-card-evaluate-content-item' onClick={() => { setActiveIndex(3) }}> |
| | | <View className='car-card-evaluate-content-item' onClick={() => changeEvaluate(3)}> |
| | | <Image src={activeIndex == 3 ? activeGood : good} className='car-card-evaluate-content-item-img' /> |
| | | <Text className={`car-card-evaluate-content-item-text ${activeIndex == 3 ? 'active-text' : ''}`}>很满意</Text> |
| | | </View> |
| | |
| | | .evaluate-popup { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 99999; |
| | | |
| | | .evaluate-mask { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | } |
| | | |
| | | .evaluate-content { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | background: #ffffff; |
| | | border-radius: 16px 16px 0 0; |
| | | padding: 42px 0 88px 0; |
| | | |
| | | &-close { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-right: 34px; |
| | | |
| | | image { |
| | | width: 36px; |
| | | height: 36px; |
| | | } |
| | | } |
| | | |
| | | &-title { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 24px; |
| | | font-size: 32px; |
| | | font-weight: 600; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | |
| | | image { |
| | | width: 82px; |
| | | height: 80px; |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | | |
| | | .content-box { |
| | | margin-top: 54px; |
| | | padding: 0 49px; |
| | | |
| | | .evaluate-problem-type { |
| | | |
| | | &-title { |
| | | font-size: 24px; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | } |
| | | |
| | | &-content { |
| | | margin-top: 24px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 24px; |
| | | |
| | | &-item { |
| | | padding: 0 40px; |
| | | height: 60px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | border-radius: 8px; |
| | | background: #FFFFFF; |
| | | box-sizing: border-box; |
| | | border: 2px solid rgba(0, 0, 0, 0.2); |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.6); |
| | | } |
| | | |
| | | .active-tag { |
| | | border-radius: 12px; |
| | | box-sizing: border-box; |
| | | border: 2px solid #000000; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .evaluate-problem-desc-box { |
| | | position: relative; |
| | | |
| | | .evaluate-problem-desc { |
| | | width: calc(100% - 48px); |
| | | margin-top: 48px; |
| | | border-radius: 16px; |
| | | background: rgba(0, 0, 0, 0.03); |
| | | padding: 24px; |
| | | padding-bottom: 16px; |
| | | font-size: 28px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | height: 196px; |
| | | } |
| | | |
| | | .placeholder-class { |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | color: rgba(0, 0, 0, 0.25); |
| | | } |
| | | |
| | | .evaluate-problem-desc-count { |
| | | position: absolute; |
| | | right: 24px; |
| | | bottom: 16px; |
| | | font-size: 20px; |
| | | line-height: 20px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.25); |
| | | } |
| | | } |
| | | |
| | | .evaluate-problem-pic-box { |
| | | margin-top: 24px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | |
| | | &-img { |
| | | flex-shrink: 0; |
| | | width: 208px; |
| | | height: 208px; |
| | | } |
| | | |
| | | &-upload { |
| | | flex-shrink: 0; |
| | | width: 208px; |
| | | height: 208px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | border-radius: 16px; |
| | | background: rgba(0, 0, 0, 0.03); |
| | | |
| | | &-img { |
| | | width: 48px; |
| | | height: 48px; |
| | | } |
| | | |
| | | &-count { |
| | | font-size: 24px; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.25); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | &-confirm { |
| | | margin: 0 94px; |
| | | margin-top: 40px; |
| | | font-size: 30px; |
| | | letter-spacing: normal; |
| | | color: #FFFFFF; |
| | | text-align: center; |
| | | line-height: 96px; |
| | | border-radius: 16px; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | } |
| | | |
| | | .ok-btn { |
| | | border-radius: 16px; |
| | | background: linear-gradient(97deg, #8475FF 0%, #7DD6FF 103%); |
| | | } |
| | | } |
| | | } |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { View, Text, Image, Textarea } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './css-folder/evaluate-popup.less' |
| | | |
| | |
| | | export default function EvaluatePopup({ show = false, onClose, onConfirm }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [selectedTag, setSelectedTag] = useState(''); |
| | | const tags = ['乘车体验', '服务态度', '司机行为', '其他'] |
| | | |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | |
| | | } |
| | | }, [show]); |
| | | |
| | | const submit = () => { |
| | | if (!selectedTag) return; |
| | | onConfirm?.(); |
| | | } |
| | | |
| | | if (!visible) return null; |
| | | |
| | | return ( |
| | | <View className='cancel-popup' catchMove> |
| | | <View className='cancel-mask'></View> |
| | | <View className='cancel-content' animation={animationData}> |
| | | <View className='evaluate-popup' catchMove onClick={(e) => e.stopPropagation()}> |
| | | <View className='evaluate-mask'></View> |
| | | <View className='evaluate-content' animation={animationData}> |
| | | <View className='evaluate-content-close'> |
| | | <Image onClick={onClose} src={require('../../../assets/close-icon.png')} /> |
| | | </View> |
| | | <View className='evaluate-content-title'> |
| | | <Image src={require('../../../assets/active-bad.png')} /> |
| | | <View>“不满意,我要吐槽”</View> |
| | | </View> |
| | | <View className='content-box'> |
| | | <View className='evaluate-problem-type'> |
| | | <View className='evaluate-problem-type-title'>遇到的问题类型:</View> |
| | | <View className='evaluate-problem-type-content'> |
| | | {tags.map((item) => ( |
| | | <View key={item} className={`evaluate-problem-type-content-item ${selectedTag === item && 'active-tag'}`} onClick={() => setSelectedTag(item)}>{item}</View> |
| | | ))} |
| | | </View> |
| | | </View> |
| | | <View className='evaluate-problem-desc-box'> |
| | | <Textarea className='evaluate-problem-desc' maxlength={500} placeholderClass='placeholder-class' placeholder='请描述您的建议或问题(可添加相关截图)' /> |
| | | <View className='evaluate-problem-desc-count'>0/500</View> |
| | | </View> |
| | | <View className='evaluate-problem-pic-box'> |
| | | {/* <Image className='evaluate-problem-pic-box-img /> */} |
| | | <View className='evaluate-problem-pic-box-upload'> |
| | | <Image className='evaluate-problem-pic-box-upload-img' src={require('../../../assets/upload-icon.png')} /> |
| | | <View className='evaluate-problem-pic-box-upload-count'>0/9</View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | <View className='evaluate-content-confirm'> |
| | | <View className={`evaluate-content-confirm-btn ${selectedTag && 'ok-btn'}`} onClick={submit}>提交反馈</View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | ) |
| | |
| | | import tipBlueIcon from '../../../assets/tip-blue-icon.png' |
| | | import tipDefaultIcon from '../../../assets/tip-default-icon.png' |
| | | |
| | | export default function ReleaseRoute({ callImmediately }: { callImmediately: () => void }) { |
| | | type Props = { |
| | | startSite: Site; |
| | | endSite: Site; |
| | | callImmediately: () => void; |
| | | } |
| | | |
| | | export default function ReleaseRoute({ startSite, endSite, callImmediately }: Props) { |
| | | |
| | | const toSeleteSitePage = (type: string) => { |
| | | Taro.navigateTo({ |
| | |
| | | <View className="now-site-dot"></View> |
| | | <View className="now-site-address"> |
| | | <View className="now-site-address-content"> |
| | | 从<Text className="now-site-address-text">中国南方电网·2号门</Text>上车 |
| | | 从<Text className="now-site-address-text">{startSite.name}</Text>上车 |
| | | </View> |
| | | <Image className="now-site-icon" src={rightIcon} /> |
| | | </View> |
| | |
| | | <View onClick={() => toSeleteSitePage('end')} className="to-site"> |
| | | <View className="to-site-dot"></View> |
| | | <View className="to-site-address"> |
| | | <View className="to-site-text">你想去哪?</View> |
| | | <View className="to-site-text">{endSite.name}</View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | |
| | | import Taro from "@tarojs/taro" |
| | | import Taro, { useDidShow } from "@tarojs/taro" |
| | | import { useEffect, useState } from 'react'; |
| | | import { View, Map, Text, Image, CoverView, CoverImage } from '@tarojs/components'; |
| | | import './index.less'; |
| | |
| | | import AnnouncementsPopup from './components/announcements-popup'; |
| | | import OverRoute from './components/over-route'; |
| | | import PassengerNotice from './components/passenger-notice'; |
| | | import EvaluatePopup from './components/evaluate-popup'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png' |
| | | import contactService from '../../../robotaxi/assets/contact-service.png' |
| | |
| | | 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 [showCancelPopup, setShowCancelPopup] = useState(false); // 取消弹窗是否显示 |
| | | const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false); // 安全注意事项动画弹窗是否显示 |
| | | const [showOverRoutePopup, setShowOverRoutePopup] = useState(false); // 结束行程弹窗是否显示 |
| | | const [showPassengerNotice, setShowPassengerNotice] = useState(false); // 乘车需知弹窗是否显示 |
| | | const [cancelType, setCancelType] = useState(1); // 取消类型 |
| | | const [showEvaluatePopup, setShowEvaluatePopup] = useState(false); // 评价弹窗是否显示 |
| | | |
| | | const router = Taro.useRouter(); |
| | | |
| | |
| | | }, [tripStatus]); |
| | | |
| | | // 页面初始化 |
| | | useEffect(() => { |
| | | useDidShow(() => { |
| | | const s = Taro.getStorageSync('startSite'); |
| | | const e = Taro.getStorageSync('endSite'); |
| | | if (s) { |
| | |
| | | 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(); |
| | | }, []); |
| | | if (s && s.latitude && s.longitude) { |
| | | setLatitude(s.latitude); |
| | | setLongitude(s.longitude); |
| | | } else { |
| | | getLocationInfo(); |
| | | } |
| | | }); |
| | | |
| | | // 规划路线 |
| | | useEffect(() => { |
| | | const originLng = startSite?.longitude || longitude; |
| | | const originLat = startSite?.latitude || latitude; |
| | | |
| | | if (!originLng || !originLat || !endSite.longitude || !endSite.latitude) return; |
| | | |
| | | const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY }); |
| | | myAmapFun.getDrivingRoute({ |
| | | origin: `${longitude},${latitude}`, |
| | | origin: `${originLng},${originLat}`, |
| | | destination: `${endSite.longitude},${endSite.latitude}`, |
| | | success: function (data) { |
| | | const points = parseRouteToPoints(data); |
| | |
| | | console.log('规划失败', info) |
| | | } |
| | | }) |
| | | }, [tripStatus, endSite, longitude, latitude]) |
| | | }, [tripStatus, endSite, startSite, longitude, latitude]) |
| | | |
| | | const formatTime = (sec: number) => { |
| | | const m = Math.floor(sec / 60); |
| | |
| | | setShowPassengerNotice(false); |
| | | } |
| | | |
| | | // 打开评价弹窗 |
| | | const handleOpenEvaluatePopup = () => { |
| | | setShowEvaluatePopup(true); |
| | | } |
| | | |
| | | // 关闭评价弹窗 |
| | | const handleCloseEvaluatePopup = () => { |
| | | setShowEvaluatePopup(false); |
| | | } |
| | | |
| | | // 确认评价 |
| | | const handleConfirmEvaluate = () => { |
| | | setShowEvaluatePopup(false); |
| | | } |
| | | |
| | | const toSelectSite = (id: number) => { |
| | | if (id === 1) { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/site-search/index?pageType=start' |
| | | }) |
| | | } else { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/site-search/index?pageType=end' |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // 点击气泡 |
| | | const handleCalloutTap = (e) => { |
| | | const markerId = e.detail.markerId; |
| | | toSelectSite(markerId); |
| | | } |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | |
| | | markers={markers} |
| | | polyline={polyline} |
| | | showLocation={!!longitude} |
| | | onCalloutTap={handleCalloutTap} |
| | | style={{ width: '100%', height: '100%' }} |
| | | > |
| | | {/* 自定义callout */} |
| | |
| | | <View catchMove onClick={(e) => e.stopPropagation()}> |
| | | {/* 发布路线 */} |
| | | { |
| | | tripStatus == 1 && <ReleaseRoute callImmediately={callImmediately} /> |
| | | (tripStatus == 1 && startSite && endSite) && |
| | | <ReleaseRoute |
| | | startSite={startSite} |
| | | endSite={endSite} |
| | | callImmediately={callImmediately} |
| | | /> |
| | | } |
| | | {/* 发布中弹窗 */} |
| | | { |
| | |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | | handlePay={handlePay} |
| | | updateStatus={() => setTripStatus(prev => prev + 1)} |
| | | handleOpenEvaluatePopup={handleOpenEvaluatePopup} |
| | | /> |
| | | } |
| | | </View> |
| | |
| | | show={showPassengerNotice} |
| | | onClose={handleClosePassengerNotice} |
| | | /> |
| | | {/* 评价弹窗 */} |
| | | <EvaluatePopup |
| | | show={showEvaluatePopup} |
| | | onClose={handleCloseEvaluatePopup} |
| | | onConfirm={handleConfirmEvaluate} |
| | | /> |
| | | </View> |
| | | ) |
| | | } |
| | |
| | | export default { |
| | | navigationBarTitleText: '国内(含广澳门台)', |
| | | navigationBarTitleText: '国内(含港澳台)', |
| | | }; |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useState } from 'react'; |
| | | import { useEffect, useState } from 'react'; |
| | | import { View, Text, Input, ScrollView } from '@tarojs/components'; |
| | | import './index.less'; |
| | | |
| | | const pendingCityList = [ |
| | | { title: 'A', items: ['安庆市', '安顺市', '安阳市', '鞍山市'] }, |
| | | { title: 'B', items: ['北京市', '保定市', '包头市', '北海市'] }, |
| | | { title: 'C', items: ['重庆市', '成都市', '长沙市', '长春市'] }, |
| | | { title: 'D', items: ['大连市', '东莞市', '大同市', '达州市'] }, |
| | | { title: 'F', items: ['福州市', '佛山市', '抚顺市', '阜阳市'] }, |
| | | { title: 'G', items: ['广州市', '贵阳市', '桂林市', '赣州市'] }, |
| | | { title: 'H', items: ['杭州市', '哈尔滨市', '合肥市', '海口市'] }, |
| | | { title: 'J', items: ['济南市', '嘉兴市', '金华市', '吉林市'] }, |
| | | { title: 'K', items: ['昆明市', '开封市'] }, |
| | | { title: 'L', items: ['兰州市', '拉萨市', '洛阳市', '临沂市'] }, |
| | | { title: 'N', items: ['南京市', '南宁市', '宁波市', '南昌市'] }, |
| | | { title: 'Q', items: ['青岛市', '秦皇岛市', '泉州市', '曲靖市'] }, |
| | | { title: 'S', items: ['上海市', '深圳市', '石家庄市', '沈阳市', '苏州市', '三亚市'] }, |
| | | { title: 'T', items: ['天津市', '太原市', '唐山市', '台州市'] }, |
| | | { title: 'W', items: ['武汉市', '无锡市', '温州市', '乌鲁木齐市'] }, |
| | | { title: 'X', items: ['西安市', '厦门市', '徐州市', '西宁市'] }, |
| | | { title: 'Y', items: ['银川市', '扬州市', '烟台市', '宜昌市'] }, |
| | | { title: 'Z', items: ['郑州市', '珠海市', '中山市', '张家口市'] } |
| | | { title: 'A', items: ['安庆', '安顺', '安阳', '鞍山'] }, |
| | | { title: 'B', items: ['北京', '保定', '包头', '北海'] }, |
| | | { title: 'C', items: ['重庆', '成都', '长沙', '长春'] }, |
| | | { title: 'D', items: ['大连', '东莞', '大同', '达州'] }, |
| | | { title: 'F', items: ['福州', '佛山', '抚顺', '阜阳'] }, |
| | | { title: 'G', items: ['广州', '贵阳', '桂林', '赣州'] }, |
| | | { title: 'H', items: ['杭州', '哈尔滨', '合肥', '海口'] }, |
| | | { title: 'J', items: ['济南', '嘉兴', '金华', '吉林'] }, |
| | | { title: 'K', items: ['昆明', '开封'] }, |
| | | { title: 'L', items: ['兰州', '拉萨', '洛阳', '临沂'] }, |
| | | { title: 'N', items: ['南京', '南宁', '宁波', '南昌'] }, |
| | | { title: 'Q', items: ['青岛', '秦皇岛', '泉州', '曲靖'] }, |
| | | { title: 'S', items: ['上海', '深圳', '石家庄', '沈阳', '苏州', '三亚'] }, |
| | | { title: 'T', items: ['天津', '太原', '唐山', '台州'] }, |
| | | { title: 'W', items: ['武汉', '无锡', '温州', '乌鲁木齐'] }, |
| | | { title: 'X', items: ['西安', '厦门', '徐州', '西宁'] }, |
| | | { title: 'Y', items: ['银川', '扬州', '烟台', '宜昌'] }, |
| | | { title: 'Z', items: ['郑州', '珠海', '中山', '张家口'] } |
| | | ]; |
| | | |
| | | export default function SelectCityPage() { |
| | | const [scrollIntoView, setScrollIntoView] = useState(''); |
| | | const [searchValue, setSearchValue] = useState(''); |
| | | const [selectedCity, setSelectedCity] = useState('北京'); |
| | | const [filteredPendingList, setFilteredPendingList] = useState<any[]>([]); |
| | | |
| | | // 开放城市 |
| | | const openCityList = ['北京', '广州', '深圳', '珠海', '佛山']; |
| | | |
| | | useEffect(() => { |
| | | const city = Taro.getStorageSync('selectedCity'); |
| | | if (city) { |
| | | setSelectedCity(city); |
| | | } |
| | | |
| | | // 初始化待开放城市,过滤掉已开放的城市 |
| | | const list = pendingCityList.map(group => ({ |
| | | ...group, |
| | | items: group.items.filter(item => !openCityList.includes(item)) |
| | | })).filter(group => group.items.length > 0); |
| | | |
| | | setFilteredPendingList(list); |
| | | }, []) |
| | | |
| | | const handleScrollTo = (letter) => { |
| | | setScrollIntoView(`city-${letter}`); |
| | |
| | | <View className='service-city-title'>运营城市</View> |
| | | <View className='service-city-list'> |
| | | { |
| | | ['广州市', '深圳市', '珠海市', '佛山市'].map((item, index) => ( |
| | | openCityList.map((item, index) => ( |
| | | <View |
| | | key={index} |
| | | className={`service-city-item ${index === 0 ? 'active-city' : ''}`} |
| | | className={`service-city-item ${selectedCity === item && 'active-city'}`} |
| | | onClick={() => handleSelectCity(item)} |
| | | > |
| | | {item} |
| | |
| | | scrollIntoView={scrollIntoView} |
| | | > |
| | | { |
| | | pendingCityList.map((group) => ( |
| | | filteredPendingList.map((group) => ( |
| | | <View key={group.title} id={`city-${group.title}`} className='city-group'> |
| | | <View className='city-group-title'>{group.title}</View> |
| | | { |
| | |
| | | </View> |
| | | <View className='shortcut'> |
| | | { |
| | | pendingCityList.map((group) => ( |
| | | filteredPendingList.map((group) => ( |
| | | <View |
| | | key={group.title} |
| | | className='shortcut-item' |
| | |
| | | <View> |
| | | <View className='search-result-box'> |
| | | { |
| | | ['成都市', '广州市', '深圳市', '珠海市', '佛山市'].map((item, index) => ( |
| | | ['成都', '广州', '深圳', '珠海', '佛山'].map((item, index) => ( |
| | | <View key={index} className='search-result-item'> |
| | | <View onClick={() => handleSelectCity(item)} className='search-result-city'>{item}</View> |
| | | <View className={`search-result-zone ${index != 0 ? 'search-result-zone-disabled' : ''}`}>运营区</View> |
| | |
| | | |
| | | const router = Taro.useRouter(); |
| | | |
| | | // 校验上车点是否存在 |
| | | // 页面初始化 |
| | | useEffect(() => { |
| | | const s = Taro.getStorageSync('startSite'); |
| | | if (s) { |
| | | setStartSite(s); |
| | | } |
| | | }, []); |
| | | |
| | | // 页面初始化 |
| | | useEffect(() => { |
| | | if (router.params.pageType) { |
| | | setPageType(router.params.pageType); |
| | | } |
| | |
| | | {(showToolTip && selectedSite.name) && ( |
| | | <View className="toolTip"> |
| | | <View className="toolTip-bg"> |
| | | <Text className="toolTip-bg-text">'限停3分钟'</Text> |
| | | <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> |
| | | )} |