feat(robotaxi): 新增站点选择与隐私协议功能
- 新增站点选择页面,支持地图选点与城市切换
- 新增隐私协议弹窗组件,包含用户协议与隐私政策
- 新增客服弹窗组件,提供电话客服功能
- 新增站点搜索页面的地图选点入口
- 新增工具函数:计算两点距离与查找最近标记点
- 更新应用配置,添加新页面路由
5 文件已重命名
11个文件已添加
3个文件已修改
| | |
| | | 'pages/coupon/index', |
| | | 'pages/site-search/index', |
| | | 'pages/order/index', |
| | | 'pages/order/detail/index' |
| | | 'pages/order/detail/index', |
| | | 'pages/select-city/index', |
| | | 'pages/select-map-site/index' |
| | | ], |
| | | } |
| | | ]; |
| File was renamed from src/pages/robotaxi/components/privacy-popup/index.tsx |
| | |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './index.less'; |
| | | import check from '../../../../robotaxi/assets/check.png' |
| | | import uncheck from '../../../../robotaxi/assets/uncheck.png' |
| | | import check from '../../../../../robotaxi/assets/check.png' |
| | | import uncheck from '../../../../../robotaxi/assets/uncheck.png' |
| | | import { privicyShowManager } from './utils' |
| | | |
| | | type Props = { |
| | |
| | | import Taro from "@tarojs/taro" |
| | | import Taro, { useDidShow } from "@tarojs/taro" |
| | | import { useEffect, useState, useRef } from 'react'; |
| | | import { View, Map, Text, Image } from '@tarojs/components'; |
| | | import './index.less'; |
| | | import PrivacyPopup from '../components/privacy-popup'; |
| | | import CustomerServicePopup from '../components/customer-service'; |
| | | import PrivacyPopup from './components/privacy-popup'; |
| | | import CustomerServicePopup from '../../../robotaxi/components/customer-service'; |
| | | import { AMAP_KEY } from '../../../constants/api'; |
| | | import selectCityIcon from '../../../robotaxi/assets/select-city.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 siteTopBg from '../../../robotaxi/assets/site-top-bg.png' |
| | | import amapFile from '../../../libs/amap-wx.130' |
| | | import { findNearestMarker } from '../../../utils/Robotaxi-public-method'; |
| | | |
| | | export default function Home() { |
| | | |
| | | const [showPrivacy, setShowPrivacy] = useState(false); // |
| | | const [showCustomerService, setShowCustomerService] = useState(false); |
| | | const [longitude, setLongitude] = useState<number | null>(null); |
| | | const [latitude, setLatitude] = useState<number | null>(null); |
| | | const [showToolTip, setShowToolTip] = useState(true); |
| | | const [bounce, setBounce] = useState(false); |
| | | const [isLocationAuthDenied, setIsLocationAuthDenied] = useState(false); |
| | | const [isMaintain, setIsMaintain] = useState(false); |
| | | const [isOperatingArea, setIsOperatingArea] = useState(false); |
| | | const [showPrivacy, setShowPrivacy] = useState(false); // 隐私政策弹窗是否显示 |
| | | const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示 |
| | | const [longitude, setLongitude] = useState<number | null>(null); // 经度 |
| | | const [latitude, setLatitude] = useState<number | null>(null); // 纬度 |
| | | const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示 |
| | | const [bounce, setBounce] = useState(false); // 是否弹跳 |
| | | const [isLocationAuthDenied, setIsLocationAuthDenied] = useState(false); // 定位权限是否被拒绝 |
| | | const [markers, setMarkers] = useState<any[]>([]); // 标记点列表 |
| | | const [isMaintain, setIsMaintain] = useState(false); // 是否维护中 |
| | | const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内 |
| | | |
| | | const [boxHeight, setBoxHeight] = useState<number | string>('auto'); |
| | | const startY = useRef(0); |
| | | const startHeight = useRef(0); |
| | | const fullHeight = useRef(0); |
| | | const minHeight = useRef(0); |
| | | const isDragging = useRef(false); |
| | | const [boxHeight, setBoxHeight] = useState<number | string>('auto'); // 活动区域高度 |
| | | const startY = useRef(0); // 拖动开始时的Y坐标 |
| | | const startHeight = useRef(0); // 拖动开始时的高度 |
| | | const fullHeight = useRef(0); // 活动区域最大高度 |
| | | const minHeight = useRef(0); // 活动区域最小高度 |
| | | const isDragging = useRef(false); // 是否正在拖动 |
| | | |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY }); |
| | | myAmapFun.getPoiAround({ |
| | | location: `${res.latitude},${res.longitude}`, |
| | | success: () => { }, |
| | | fail: () => { } |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | setIsLocationAuthDenied(true); |
| | | }); |
| | | } |
| | | const [selectedCity, setSelectedCity] = useState(''); // 选中的城市 |
| | | |
| | | useDidShow(() => { |
| | | // 从本地存储中获取选中的城市 |
| | | const selectedCity = Taro.getStorageSync('selectedCity'); |
| | | if (selectedCity) { |
| | | setSelectedCity(selectedCity); |
| | | } |
| | | }); |
| | | |
| | | useEffect(() => { |
| | | // 初始化默认标记点 (成都坐标) |
| | | 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 } |
| | | }, |
| | | { |
| | | 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 } |
| | | }, |
| | | { |
| | | 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 } |
| | | }, |
| | | { |
| | | 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 } |
| | | }, |
| | | { |
| | | 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 } |
| | | } |
| | | ]; |
| | | setMarkers(defaultMarkers); |
| | | |
| | | setShowPrivacy(Taro.getStorageSync('has_show_privicy_Robotaxi') != 'true'); |
| | | |
| | | // 计算活动区域高度,用于初始隐藏 |
| | |
| | | }); |
| | | }, []); |
| | | |
| | | // 获取用户位置信息 |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY }); |
| | | myAmapFun.getPoiAround({ |
| | | location: `${res.latitude},${res.longitude}`, |
| | | success: () => { }, |
| | | fail: () => { } |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | setIsLocationAuthDenied(true); |
| | | }); |
| | | } |
| | | |
| | | const handleClosePrivacy = () => { |
| | | setShowPrivacy(false); |
| | | }; |
| | |
| | | setShowPrivacy(false); |
| | | }; |
| | | |
| | | // 返回当前位置 |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation(); |
| | | } |
| | | |
| | | // 地图区域变化时触发,判断是否吸附到最近的站点停靠点 |
| | | const handleRegionChange = (e: any) => { |
| | | const type = e?.type; |
| | | const type = e?.type || e?.detail?.type; |
| | | // 获取触发源: gesture (手势), update (接口调用) |
| | | const causedBy = e?.causedBy || e?.detail?.causedBy; |
| | | |
| | | if (type === 'begin') { |
| | | setShowToolTip(false); |
| | | } else if (type === 'end') { |
| | | setBounce(true); |
| | | |
| | | // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环 |
| | | if (causedBy === 'gesture' || causedBy === 'drag') { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.getCenterLocation({ |
| | | success: (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, |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | setTimeout(() => { |
| | | setBounce(false); |
| | | setShowToolTip(true); |
| | |
| | | }) |
| | | } |
| | | |
| | | const toSeleteSitePage = (type: string) => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/site-search/index?type=' + type |
| | | }) |
| | | } |
| | | |
| | | const toSeleteCityPage = () => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/select-city/index', |
| | | }) |
| | | } |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | | id="map" |
| | | longitude={longitude ?? 113.32452} |
| | | latitude={latitude ?? 23.099994} |
| | | longitude={longitude ?? 104.066541} |
| | | latitude={latitude ?? 30.572269} |
| | | scale={14} |
| | | markers={markers} |
| | | showLocation={!!longitude} |
| | | onRegionChange={handleRegionChange} |
| | | style={{ width: '100%', flex: 1 }} |
| | | > |
| | | {/* 选择城市 */} |
| | | <View className="select-city"> |
| | | <Text>成都</Text> |
| | | <View onClick={toSeleteCityPage} className="select-city"> |
| | | <Text>{selectedCity || '北京'}</Text> |
| | | <Image className="select-city-icon" src={selectCityIcon} /> |
| | | </View> |
| | | {/* 地图浮层入口 */} |
| | |
| | | onTouchEnd={handleTouchEnd} |
| | | > |
| | | <View className="select-site"> |
| | | <Image className="site-top-bg" src={siteTopBg} /> |
| | | <Image className="site-top-bg" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/cab0e5b2-0da0-4d2e-80e6-0fc2da8d2532.png' /> |
| | | <View className="site-boarding-point"> |
| | | {/* 起点 */} |
| | | <View className="now-site"> |
| | | <View onClick={() => toSeleteSitePage('start')} className="now-site"> |
| | | <View className="now-site-dot"></View> |
| | | <View className="now-site-address"> |
| | | <View className="now-site-address-content"> |
| | |
| | | </View> |
| | | </View> |
| | | {/* 终点 */} |
| | | <View className="to-site"> |
| | | <View onClick={() => toSeleteSitePage('end')} className="to-site"> |
| | | <View className="to-site-dot"></View> |
| | | <View className="to-site-address"> |
| | | <View className="to-site-text">你想去哪?</View> |
| New file |
| | |
| | | export default { |
| | | navigationBarTitleText: '国内(含广澳门台)', |
| | | }; |
| New file |
| | |
| | | page { |
| | | background: #fff; |
| | | } |
| | | |
| | | .select-city-page { |
| | | height: 100vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | |
| | | .search-box { |
| | | padding: 16px 32px; |
| | | background: #fff; |
| | | flex-shrink: 0; |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 100; |
| | | |
| | | .search-input { |
| | | border-radius: 24px; |
| | | background: #FFFFFF; |
| | | box-sizing: border-box; |
| | | border: 1.4px solid #F1F1F1; |
| | | box-shadow: 0px 16px 60px 0px rgba(0, 0, 0, 0.05); |
| | | padding: 0 32px; |
| | | height: 96px; |
| | | font-size: 28px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | |
| | | .placeholder-class { |
| | | font-size: 28px; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .service-city-box { |
| | | margin: 60px 32px 64px 32px; |
| | | flex-shrink: 0; |
| | | |
| | | .service-city-title { |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | line-height: 48px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | .service-city-list { |
| | | margin-top: 32px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 20px; |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | |
| | | .service-city-item { |
| | | border-radius: 12px; |
| | | background: #F2F3F7; |
| | | box-sizing: border-box; |
| | | line-height: 72px; |
| | | text-align: center; |
| | | padding: 0 20px; |
| | | border: 2px solid #F2F3F7; |
| | | } |
| | | |
| | | .active-city { |
| | | border: 2px solid #000000; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .non-operational-city-box { |
| | | margin: 0 32px; |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | min-height: 0; |
| | | |
| | | .non-operational-city-title { |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | line-height: 48px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .non-operational-city-list { |
| | | margin-top: 42px; |
| | | flex: 1; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-sizing: border-box; |
| | | padding-bottom: calc(60px + constant(safe-area-inset-bottom)); |
| | | padding-bottom: calc(60px + env(safe-area-inset-bottom)); |
| | | |
| | | .city-group { |
| | | .city-group-title { |
| | | font-size: 24px; |
| | | line-height: 34px; |
| | | color: rgba(0, 0, 0, 0.5); |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .non-operational-city-item { |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | margin-bottom: 24px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .shortcut { |
| | | position: fixed; |
| | | top: 50%; |
| | | right: 12px; |
| | | transform: translateY(-50%); |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | z-index: 100; |
| | | padding: 10px 0; |
| | | background-color: rgba(255, 255, 255, 0.5); |
| | | border-radius: 20px; |
| | | |
| | | .shortcut-item { |
| | | font-size: 22px; |
| | | line-height: 32px; |
| | | color: rgba(0, 0, 0, 0.6); |
| | | padding: 2px 8px; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .search-result-box { |
| | | margin: 32px; |
| | | |
| | | .search-result-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 38px; |
| | | |
| | | .search-result-city { |
| | | font-size: 28px; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | .search-result-zone { |
| | | border-radius: 200px; |
| | | background: linear-gradient(107deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 100%); |
| | | line-height: 48px; |
| | | text-align: center; |
| | | padding: 0 18px; |
| | | font-size: 24px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | .search-result-zone-disabled { |
| | | background: rgba(0, 0, 0, 0.15); |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { 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: ['郑州市', '珠海市', '中山市', '张家口市'] } |
| | | ]; |
| | | |
| | | export default function SelectCityPage() { |
| | | const [scrollIntoView, setScrollIntoView] = useState(''); |
| | | const [searchValue, setSearchValue] = useState(''); |
| | | |
| | | const handleScrollTo = (letter) => { |
| | | setScrollIntoView(`city-${letter}`); |
| | | }; |
| | | |
| | | const handleSelectCity = (city) => { |
| | | Taro.setStorageSync('selectedCity', city); |
| | | Taro.navigateBack(); |
| | | }; |
| | | |
| | | const searchCity = () => { |
| | | } |
| | | |
| | | const changeSearchValue = (e) => { |
| | | setSearchValue(e.detail.value) |
| | | } |
| | | |
| | | return ( |
| | | <View className='select-city-page'> |
| | | <View className='search-box'> |
| | | <Input |
| | | onInput={(e) => changeSearchValue(e)} |
| | | onBlur={() => searchCity()} |
| | | onConfirm={() => searchCity()} |
| | | value={searchValue} |
| | | placeholder='请输入城市名' |
| | | className='search-input' |
| | | placeholderClass='placeholder-class' |
| | | /> |
| | | </View> |
| | | {/* 搜索前 */} |
| | | { |
| | | !searchValue && |
| | | <View> |
| | | <View className='service-city-box'> |
| | | <View className='service-city-title'>运营城市</View> |
| | | <View className='service-city-list'> |
| | | { |
| | | ['广州市', '深圳市', '珠海市', '佛山市'].map((item, index) => ( |
| | | <View |
| | | key={index} |
| | | className={`service-city-item ${index === 0 ? 'active-city' : ''}`} |
| | | onClick={() => handleSelectCity(item)} |
| | | > |
| | | {item} |
| | | </View> |
| | | )) |
| | | } |
| | | </View> |
| | | </View> |
| | | <View className='non-operational-city-box'> |
| | | <View className='non-operational-city-title'>待开放城市</View> |
| | | <ScrollView |
| | | className='non-operational-city-list' |
| | | scrollY |
| | | scrollWithAnimation |
| | | scrollIntoView={scrollIntoView} |
| | | > |
| | | { |
| | | pendingCityList.map((group) => ( |
| | | <View key={group.title} id={`city-${group.title}`} className='city-group'> |
| | | <View className='city-group-title'>{group.title}</View> |
| | | { |
| | | group.items.map((city, index) => ( |
| | | <View |
| | | key={index} |
| | | className='non-operational-city-item' |
| | | |
| | | > |
| | | {city} |
| | | </View> |
| | | )) |
| | | } |
| | | </View> |
| | | )) |
| | | } |
| | | </ScrollView> |
| | | </View> |
| | | <View className='shortcut'> |
| | | { |
| | | pendingCityList.map((group) => ( |
| | | <View |
| | | key={group.title} |
| | | className='shortcut-item' |
| | | onClick={(e) => { |
| | | e.stopPropagation(); |
| | | handleScrollTo(group.title); |
| | | }} |
| | | > |
| | | {group.title} |
| | | </View> |
| | | )) |
| | | } |
| | | </View> |
| | | </View> |
| | | } |
| | | {/* 搜索后 */} |
| | | { |
| | | searchValue && |
| | | <View> |
| | | <View className='search-result-box'> |
| | | { |
| | | ['成都市', '广州市', '深圳市', '珠海市', '佛山市'].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> |
| | | </View> |
| | | )) |
| | | } |
| | | </View> |
| | | </View> |
| | | } |
| | | </View> |
| | | ) |
| | | } |
| New file |
| | |
| | | .release-route { |
| | | .select-site { |
| | | position: relative; |
| | | padding: 32px; |
| | | padding-top: 40px; |
| | | background: #F2F3F7; |
| | | border-top: 2px solid #fff; |
| | | border-radius: 32px 32px 0 0; |
| | | |
| | | .site-top-bg { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 326px; |
| | | } |
| | | |
| | | .placeholder-box { |
| | | width: 100%; |
| | | height: 326px; |
| | | } |
| | | |
| | | .site-boarding-point { |
| | | border-radius: 24px; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | box-sizing: border-box; |
| | | border: 1px solid #FFFFFF; |
| | | backdrop-filter: blur(20px); |
| | | box-shadow: 0px 40px 60px 0px rgba(87, 99, 147, 0.1); |
| | | padding: 24px; |
| | | |
| | | .now-site { |
| | | display: flex; |
| | | align-items: center; |
| | | padding-left: 28px; |
| | | |
| | | .now-site-dot { |
| | | width: 12px; |
| | | height: 12px; |
| | | border-radius: 50%; |
| | | background: #6D7FD9; |
| | | margin-right: 24px; |
| | | } |
| | | |
| | | .now-site-address { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | .now-site-address-content { |
| | | font-size: 28px; |
| | | font-weight: bold; |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | |
| | | .now-site-address-text { |
| | | margin: 0 2px; |
| | | color: #6D7FD9; |
| | | } |
| | | } |
| | | |
| | | .now-site-icon { |
| | | margin-left: 2px; |
| | | width: 32px; |
| | | height: 32px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .now-site-walk { |
| | | font-size: 24px; |
| | | font-weight: normal; |
| | | line-height: normal; |
| | | letter-spacing: normal; |
| | | color: #F58900; |
| | | margin-left: 64px; |
| | | } |
| | | |
| | | .to-site { |
| | | margin-top: 32px; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px 0 20px 28px; |
| | | border-radius: 20px; |
| | | background: #F1F2F6; |
| | | |
| | | .to-site-dot { |
| | | width: 12px; |
| | | height: 12px; |
| | | border-radius: 50%; |
| | | background: #8CB654; |
| | | margin-right: 24px; |
| | | } |
| | | |
| | | .to-site-address { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | .to-site-text { |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | line-height: 48px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | margin-right: 22px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import Taro from "@tarojs/taro" |
| | | import { useEffect, useState } from 'react'; |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import rightIcon from '../../../assets/right-icon.png' |
| | | import './css-folder/release-route.less' |
| | | |
| | | export default function ReleaseRoute() { |
| | | |
| | | const toSeleteSitePage = (type: string) => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/site-search/index?type=' + type |
| | | }) |
| | | } |
| | | |
| | | return ( |
| | | <View className='release-route'> |
| | | <View className="select-site"> |
| | | <Image className="site-top-bg" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/cab0e5b2-0da0-4d2e-80e6-0fc2da8d2532.png' /> |
| | | <View className="site-boarding-point"> |
| | | {/* 起点 */} |
| | | <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> |
| | | <Image className="now-site-icon" src={rightIcon} /> |
| | | </View> |
| | | </View> |
| | | <View className="now-site-walk">步行6分钟</View> |
| | | </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> |
| | | </View> |
| | | </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); |
| | | } |
| | | } |
| | | |
| | | .pointer-box { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -100%); |
| | | display: flex; |
| | | align-items: center; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | |
| | | .toolTip { |
| | | |
| | | .toolTip-bg { |
| | | background-image: url(../../../robotaxi/assets/tooltip-bg.png); |
| | | background-size: cover; |
| | | height: 70px; |
| | | min-width: 155px; |
| | | text-align: center; |
| | | width: fit-content; |
| | | |
| | | .toolTip-bg-text { |
| | | font-size: 22px; |
| | | font-weight: 500; |
| | | letter-spacing: normal; |
| | | color: #FFFFFF; |
| | | line-height: 48px; |
| | | } |
| | | } |
| | | |
| | | .toolTip-txt { |
| | | margin-top: -26px; |
| | | display: flex; |
| | | align-items: center; |
| | | border-radius: 16px; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | backdrop-filter: blur(40px); |
| | | box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2); |
| | | padding: 12px 12px 12px 20px; |
| | | |
| | | .toolTip-txt-content { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | line-height: 48px; |
| | | } |
| | | |
| | | .toolTip-icon { |
| | | margin-left: 8px; |
| | | width: 32px; |
| | | height: 32px; |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | .pointer-icon { |
| | | width: 52px; |
| | | height: 76px; |
| | | margin-top: -54px; |
| | | |
| | | &.bounce { |
| | | animation: pointer-bounce 600ms ease-in-out; |
| | | will-change: transform; |
| | | } |
| | | } |
| | | } |
| | | |
| | | @keyframes pointer-bounce { |
| | | 0% { |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | 30% { |
| | | transform: translateY(-16px); |
| | | } |
| | | |
| | | 60% { |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | 100% { |
| | | transform: translateY(0); |
| | | } |
| | | } |
| | | |
| | | |
| | | .home-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 100vh; |
| | | overflow: hidden; |
| | | position: relative; |
| | | } |
| | | |
| | | .container { |
| | | width: 100%; |
| | | position: absolute; |
| | | bottom: 0; |
| | | transition: height 0.3s ease-in-out; |
| | | |
| | | .origin-box { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-bottom: 32px; |
| | | |
| | | .origin-icon { |
| | | width: 88px; |
| | | height: 88px; |
| | | } |
| | | } |
| | | |
| | | .content-box { |
| | | overflow: hidden; |
| | | border-radius: 32px 32px 0 0; |
| | | padding: 40px; |
| | | padding-bottom: 82px; |
| | | background: #FFFFFF; |
| | | |
| | | .content-title { |
| | | font-size: 36px; |
| | | font-weight: bold; |
| | | line-height: 50px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | .site-name { |
| | | margin-top: 32px; |
| | | font-size: 28px; |
| | | font-weight: 500; |
| | | color: #3D3D3D; |
| | | display: flex; |
| | | align-items: center; |
| | | height: 112px; |
| | | padding-left: 32px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(97deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 103%); |
| | | box-sizing: border-box; |
| | | position: relative; |
| | | |
| | | &::after { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | border-radius: 16px; |
| | | padding: 1px; |
| | | background: linear-gradient(180deg, #AAA8FF -10%, rgba(41, 159, 255, 0.0469) 108%, rgba(0, 0, 0, 0) 108%); |
| | | -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); |
| | | -webkit-mask-composite: xor; |
| | | mask-composite: exclude; |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | |
| | | .site-confirm { |
| | | margin-top: 148px; |
| | | height: 88px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | border-radius: 16px; |
| | | background: linear-gradient(95deg, #8475FF 0%, #7DD6FF 124%); |
| | | font-size: 28px; |
| | | letter-spacing: normal; |
| | | color: #FFFFFF; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import Taro from "@tarojs/taro" |
| | | import { useEffect, useState } from 'react'; |
| | | import { View, Map, Text, Image } from '@tarojs/components'; |
| | | import './index.less'; |
| | | import CustomerServicePopup from '../../components/customer-service'; |
| | | 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 pointer from '../../../robotaxi/assets/pointer.png' |
| | | import rightIcon from '../../../robotaxi/assets/right-icon.png' |
| | | import { findNearestMarker } from '../../../utils/Robotaxi-public-method'; |
| | | import ReleaseRoute from './components/release-route'; |
| | | |
| | | export default function SelectMapSite() { |
| | | |
| | | const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示 |
| | | const [longitude, setLongitude] = useState<number | null>(null); // 经度 |
| | | const [latitude, setLatitude] = useState<number | null>(null); // 纬度 |
| | | const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示 |
| | | const [bounce, setBounce] = useState(false); // 是否弹跳 |
| | | const [markers, setMarkers] = useState<any[]>([]); // 标记点列表 |
| | | const [pageType, setPageType] = useState(''); // 起点start 终点end |
| | | const [selectedSite, setSelectedSite] = useState<any>({}); // 选择的站点 |
| | | const [tripStatus, setTripStatus] = useState(0); // 行程状态 0:默认状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 7:开始教程 8:车辆形式中 9:到达目的地 10:行程已结束 11:查看明细 12:支付成功 |
| | | const router = Taro.useRouter(); |
| | | |
| | | useEffect(() => { |
| | | if (router.params.pageType) { |
| | | setPageType(router.params.pageType); |
| | | } |
| | | |
| | | // 初始化默认标记点 (成都坐标) |
| | | 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(); |
| | | }, []); |
| | | |
| | | // 获取用户位置信息 |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | }) |
| | | } |
| | | |
| | | // 返回当前位置 |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.moveToLocation(); |
| | | } |
| | | |
| | | // 地图区域变化时触发,判断是否吸附到最近的站点停靠点 |
| | | const handleRegionChange = (e: any) => { |
| | | const type = e?.type || e?.detail?.type; |
| | | // 获取触发源: gesture (手势), update (接口调用) |
| | | const causedBy = e?.causedBy || e?.detail?.causedBy; |
| | | |
| | | if (type === 'begin') { |
| | | setShowToolTip(false); |
| | | } else if (type === 'end') { |
| | | setBounce(true); |
| | | |
| | | // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环 |
| | | if (causedBy === 'gesture' || causedBy === 'drag') { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.getCenterLocation({ |
| | | success: (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, |
| | | }); |
| | | setSelectedSite(targetMarker); |
| | | } else { |
| | | setSelectedSite({}); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | setTimeout(() => { |
| | | setBounce(false); |
| | | setShowToolTip(true); |
| | | }, 600); |
| | | } |
| | | } |
| | | |
| | | const handleCloseCustomerService = () => { |
| | | setShowCustomerService(false); |
| | | }; |
| | | |
| | | const openService = () => { |
| | | setShowCustomerService(true); |
| | | } |
| | | |
| | | |
| | | const toBack = () => { |
| | | if (tripStatus == 0) { |
| | | Taro.navigateBack({ |
| | | delta: 1, |
| | | }) |
| | | } else { |
| | | Taro.switchTab({ |
| | | url: '/pages/robotaxi/home/index' |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const confirmSite = () => { |
| | | if (!selectedSite.name) { |
| | | Taro.showToast({ |
| | | title: '请选择站点', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | if (pageType === 'start') { |
| | | Taro.setStorageSync('startSite', { |
| | | latitude: selectedSite.latitude, |
| | | longitude: selectedSite.longitude, |
| | | name: selectedSite.name, |
| | | }) |
| | | Taro.navigateBack({ |
| | | delta: 2, |
| | | }) |
| | | } else { |
| | | setTripStatus(1) |
| | | } |
| | | } |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | | id="map" |
| | | longitude={longitude} |
| | | latitude={latitude} |
| | | scale={14} |
| | | markers={markers} |
| | | showLocation={!!longitude} |
| | | onRegionChange={handleRegionChange} |
| | | style={{ width: '100%', height: '100%' }} |
| | | > |
| | | {/* 选择城市 */} |
| | | <View onClick={toBack} className="select-city"> |
| | | <Image className="select-city-icon" src={backIcon} /> |
| | | </View> |
| | | {/* 地图浮层入口 */} |
| | | <View className="map-entry"> |
| | | <Image onClick={openService} className="map-entry-icon" src={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="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> |
| | | )} |
| | | <Image className={`pointer-icon ${bounce ? 'bounce' : ''}`} src={pointer} /> |
| | | </View> |
| | | <View className="container"> |
| | | {/* 复位按钮 */} |
| | | <View className="origin-box"> |
| | | <Image onClick={toOrigin} className="origin-icon" src={originIcon} /> |
| | | </View> |
| | | {/* 确认站点 */} |
| | | { |
| | | tripStatus == 0 && <View className="content-box"> |
| | | <View className="content-title">{pageType === 'start' ? '确认起点' : '确认目的地'}</View> |
| | | <View className="site-name">{selectedSite.name || '请选择站点'}</View> |
| | | <View onClick={confirmSite} className="site-confirm">{pageType === 'start' ? '确认起点站点' : '确认目的地站点'}</View> |
| | | </View> |
| | | } |
| | | {/* 发布路线 */} |
| | | { |
| | | tripStatus == 1 && <ReleaseRoute /> |
| | | } |
| | | </View> |
| | | </Map> |
| | | {/* 客服弹窗 */} |
| | | <CustomerServicePopup |
| | | show={showCustomerService} |
| | | onClose={handleCloseCustomerService} |
| | | /> |
| | | </View> |
| | | ) |
| | | } |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | const toSelectSite = () => { |
| | | Taro.navigateTo({ |
| | | url: '/robotaxi/pages/select-map-site/index?pageType=' + pageType, |
| | | }) |
| | | } |
| | | |
| | | |
| | | return ( |
| | |
| | | onBlur={() => searchSite()} |
| | | onConfirm={() => searchSite()} |
| | | ></Input> |
| | | <Image className='clear-icon' src={map}></Image> |
| | | <Image onClick={toSelectSite} className='clear-icon' src={map}></Image> |
| | | </View> |
| | | {/* 历史记录 */} |
| | | <View className='history-card'> |
| New file |
| | |
| | | |
| | | // 辅助函数:计算两点之间的距离(单位:米) |
| | | export const getDistance = (lat1: number, lng1: number, lat2: number, lng2: number) => { |
| | | const radLat1 = lat1 * Math.PI / 180.0; |
| | | const radLat2 = lat2 * Math.PI / 180.0; |
| | | const a = radLat1 - radLat2; |
| | | const b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; |
| | | let s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); |
| | | s = s * 6378.137; // 地球半径 |
| | | s = Math.round(s * 10000) / 10000; |
| | | return s * 1000; |
| | | }; |
| | | |
| | | // 查找最近的标记点 |
| | | export const findNearestMarker = (lat: number, lng: number, markers: any[]) => { |
| | | let minDistance = Infinity; |
| | | let targetMarker: any = null; |
| | | |
| | | markers.forEach(marker => { |
| | | const distance = getDistance(lat, lng, marker.latitude, marker.longitude); |
| | | if (distance < minDistance) { |
| | | minDistance = distance; |
| | | targetMarker = marker; |
| | | } |
| | | }); |
| | | |
| | | return { targetMarker, minDistance }; |
| | | }; |