hejianhao
2026-02-03 70c41cb483496506267a3c6bfed9c9c79eb630ff
feat(robotaxi): 实现主页地图及交互组件

- 添加高德地图 SDK 及地图组件,集成定位和 POI 查询
- 新增隐私协议弹窗,支持多协议勾选和状态管理
- 实现可拖拽内容面板,包含站点选择、活动展示和产品信息
- 添加地图浮层入口、指针动画、复位按钮等交互元素
- 引入相关图片资源,更新页面配置和路由顺序
12个文件已添加
9个文件已修改
26449 ■■■■■ 已修改文件
package-lock.json 25817 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/app.config.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/api.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/libs/amap-wx.130.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/components/privacy-popup/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/components/privacy-popup/index.tsx 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/components/privacy-popup/utils.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.config.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.less 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/check.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/contact-service.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/origin-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/pointer.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/right-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/select-city.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/site-top-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/suggest.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/tooltip-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/uncheck.png 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json
Diff too large
src/app.config.ts
@@ -8,12 +8,12 @@
if (process.env.TARO_ENV === 'weapp') {
  pages = [
    // 'pages/home/index',
    'pages/robotaxi/home/index',
    'pages/xpeng/index',
    // 'pages/driveNew/index',
    'pages/user/index',
    'pages/user/login',
    'pages/map/index',
    'pages/robotaxi/home/index',
  ];
  subPackages = [
src/constants/api.ts
@@ -125,3 +125,7 @@
export const API_REGISTER_REPORT_POSITION= `${_MINI_HOST}/api/register/report`
// 高德地图KEY
export const AMAP_KEY = 'e5212ad7656a00a09b9968dcbe551726'
src/libs/amap-wx.130.js
New file
@@ -0,0 +1,31 @@
function AMapWX(a){this.key=a.key;this.requestConfig={key:a.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};this.MeRequestConfig={key:a.key,serviceName:"https://restapi.amap.com/rest/me"}}
AMapWX.prototype.getWxLocation=function(a,b){wx.getLocation({type:"gcj02",success:function(c){c=c.longitude+","+c.latitude;wx.setStorage({key:"userLocation",data:c});b(c)},fail:function(c){wx.getStorage({key:"userLocation",success:function(d){d.data&&b(d.data)}});a.fail({errCode:"0",errMsg:c.errMsg||""})}})};
AMapWX.prototype.getMEKeywordsSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.keywords&&(d.keywords=b.keywords);b.city&&(d.city=b.city);b.filter&&(d.filter=b.filter);b.sortrule&&(d.sortrule=b.sortrule);b.pageNum&&(d.pageNum=b.pageNum);b.pageSize&&(d.pageSize=b.pageSize);b.sig&&(d.sig=
b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/local",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&&0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})};
AMapWX.prototype.getMEIdSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.id&&(d.id=b.id);b.sig&&(d.sig=b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/id",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&&
0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})};
AMapWX.prototype.getMEPolygonSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.keywords&&(d.keywords=b.keywords);b.polygon&&(d.polygon=b.polygon);b.filter&&(d.filter=b.filter);b.sortrule&&(d.sortrule=b.sortrule);b.pageNum&&(d.pageNum=b.pageNum);b.pageSize&&(d.pageSize=b.pageSize);
b.sig&&(d.sig=b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/polygon",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&&0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})};
AMapWX.prototype.getMEaroundSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.keywords&&(d.keywords=b.keywords);b.center&&(d.center=b.center);b.radius&&(d.radius=b.radius);b.filter&&(d.filter=b.filter);b.sortrule&&(d.sortrule=b.sortrule);b.pageNum&&(d.pageNum=b.pageNum);b.pageSize&&
(d.pageSize=b.pageSize);b.sig&&(d.sig=b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/around",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&&0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})};
AMapWX.prototype.getGeo=function(a){var b=this.requestConfig,c=a.options;b={key:this.key,extensions:"all",s:b.s,platform:b.platform,appname:this.key,sdkversion:b.sdkversion,logversion:b.logversion};c.address&&(b.address=c.address);c.city&&(b.city=c.city);c.batch&&(b.batch=c.batch);c.sig&&(b.sig=c.sig);wx.request({url:"https://restapi.amap.com/v3/geocode/geo",data:b,method:"GET",header:{"content-type":"application/json"},success:function(d){(d=d.data)&&d.status&&"1"===d.status?a.success(d):a.fail({errCode:"0",
errMsg:d})},fail:function(d){a.fail({errCode:"0",errMsg:d.errMsg||""})}})};
AMapWX.prototype.getRegeo=function(a){function b(d){var e=c.requestConfig;wx.request({url:"https://restapi.amap.com/v3/geocode/regeo",data:{key:c.key,location:d,extensions:"all",s:e.s,platform:e.platform,appname:c.key,sdkversion:e.sdkversion,logversion:e.logversion},method:"GET",header:{"content-type":"application/json"},success:function(g){if(g.data.status&&"1"==g.data.status){g=g.data.regeocode;var h=g.addressComponent,f=[],k=g.roads[0].name+"\u9644\u8fd1",m=d.split(",")[0],n=d.split(",")[1];if(g.pois&&
g.pois[0]){k=g.pois[0].name+"\u9644\u8fd1";var l=g.pois[0].location;l&&(m=parseFloat(l.split(",")[0]),n=parseFloat(l.split(",")[1]))}h.provice&&f.push(h.provice);h.city&&f.push(h.city);h.district&&f.push(h.district);h.streetNumber&&h.streetNumber.street&&h.streetNumber.number?(f.push(h.streetNumber.street),f.push(h.streetNumber.number)):f.push(g.roads[0].name);f=f.join("");a.success([{iconPath:a.iconPath,width:a.iconWidth,height:a.iconHeight,name:f,desc:k,longitude:m,latitude:n,id:0,regeocodeData:g}])}else a.fail({errCode:g.data.infocode,
errMsg:g.data.info})},fail:function(g){a.fail({errCode:"0",errMsg:g.errMsg||""})}})}var c=this;a.location?b(a.location):c.getWxLocation(a,function(d){b(d)})};
AMapWX.prototype.getWeather=function(a){function b(g){var h="base";a.type&&"forecast"==a.type&&(h="all");wx.request({url:"https://restapi.amap.com/v3/weather/weatherInfo",data:{key:d.key,city:g,extensions:h,s:e.s,platform:e.platform,appname:d.key,sdkversion:e.sdkversion,logversion:e.logversion},method:"GET",header:{"content-type":"application/json"},success:function(f){if(f.data.status&&"1"==f.data.status)if(f.data.lives){if((f=f.data.lives)&&0<f.length){f=f[0];var k={city:{text:"\u57ce\u5e02",data:f.city},
weather:{text:"\u5929\u6c14",data:f.weather},temperature:{text:"\u6e29\u5ea6",data:f.temperature},winddirection:{text:"\u98ce\u5411",data:f.winddirection+"\u98ce"},windpower:{text:"\u98ce\u529b",data:f.windpower+"\u7ea7"},humidity:{text:"\u6e7f\u5ea6",data:f.humidity+"%"}};k.liveData=f;a.success(k)}}else f.data.forecasts&&f.data.forecasts[0]&&a.success({forecast:f.data.forecasts[0]});else a.fail({errCode:f.data.infocode,errMsg:f.data.info})},fail:function(f){a.fail({errCode:"0",errMsg:f.errMsg||""})}})}
function c(g){wx.request({url:"https://restapi.amap.com/v3/geocode/regeo",data:{key:d.key,location:g,extensions:"all",s:e.s,platform:e.platform,appname:d.key,sdkversion:e.sdkversion,logversion:e.logversion},method:"GET",header:{"content-type":"application/json"},success:function(h){if(h.data.status&&"1"==h.data.status){h=h.data.regeocode;if(h.addressComponent)var f=h.addressComponent.adcode;else h.aois&&0<h.aois.length&&(f=h.aois[0].adcode);b(f)}else a.fail({errCode:h.data.infocode,errMsg:h.data.info})},
fail:function(h){a.fail({errCode:"0",errMsg:h.errMsg||""})}})}var d=this,e=d.requestConfig;a.city?b(a.city):d.getWxLocation(a,function(g){c(g)})};
AMapWX.prototype.getPoiAround=function(a){function b(e){e={key:c.key,location:e,s:d.s,platform:d.platform,appname:c.key,sdkversion:d.sdkversion,logversion:d.logversion};a.querytypes&&(e.types=a.querytypes);a.querykeywords&&(e.keywords=a.querykeywords);wx.request({url:"https://restapi.amap.com/v3/place/around",data:e,method:"GET",header:{"content-type":"application/json"},success:function(g){if(g.data.status&&"1"==g.data.status){if((g=g.data)&&g.pois){for(var h=[],f=0;f<g.pois.length;f++){var k=0==
f?a.iconPathSelected:a.iconPath;h.push({latitude:parseFloat(g.pois[f].location.split(",")[1]),longitude:parseFloat(g.pois[f].location.split(",")[0]),iconPath:k,width:22,height:32,id:f,name:g.pois[f].name,address:g.pois[f].address})}a.success({markers:h,poisData:g.pois})}}else a.fail({errCode:g.data.infocode,errMsg:g.data.info})},fail:function(g){a.fail({errCode:"0",errMsg:g.errMsg||""})}})}var c=this,d=c.requestConfig;a.location?b(a.location):c.getWxLocation(a,function(e){b(e)})};
AMapWX.prototype.getStaticmap=function(a){function b(e){c.push("location="+e);a.zoom&&c.push("zoom="+a.zoom);a.size&&c.push("size="+a.size);a.scale&&c.push("scale="+a.scale);a.markers&&c.push("markers="+a.markers);a.labels&&c.push("labels="+a.labels);a.paths&&c.push("paths="+a.paths);a.traffic&&c.push("traffic="+a.traffic);e="https://restapi.amap.com/v3/staticmap?"+c.join("&");a.success({url:e})}var c=[];c.push("key="+this.key);var d=this.requestConfig;c.push("s="+d.s);c.push("platform="+d.platform);
c.push("appname="+d.appname);c.push("sdkversion="+d.sdkversion);c.push("logversion="+d.logversion);a.location?b(a.location):this.getWxLocation(a,function(e){b(e)})};
AMapWX.prototype.getInputtips=function(a){var b=Object.assign({},this.requestConfig);a.location&&(b.location=a.location);a.keywords&&(b.keywords=a.keywords);a.type&&(b.type=a.type);a.city&&(b.city=a.city);a.citylimit&&(b.citylimit=a.citylimit);wx.request({url:"https://restapi.amap.com/v3/assistant/inputtips",data:b,method:"GET",header:{"content-type":"application/json"},success:function(c){c&&c.data&&c.data.tips&&a.success({tips:c.data.tips})},fail:function(c){a.fail({errCode:"0",errMsg:c.errMsg||
""})}})};
AMapWX.prototype.getDrivingRoute=function(a){var b=Object.assign({},this.requestConfig);a.origin&&(b.origin=a.origin);a.destination&&(b.destination=a.destination);a.strategy&&(b.strategy=a.strategy);a.waypoints&&(b.waypoints=a.waypoints);a.avoidpolygons&&(b.avoidpolygons=a.avoidpolygons);a.avoidroad&&(b.avoidroad=a.avoidroad);wx.request({url:"https://restapi.amap.com/v3/direction/driving",data:b,method:"GET",header:{"content-type":"application/json"},success:function(c){c&&c.data&&c.data.route&&a.success({paths:c.data.route.paths,
taxi_cost:c.data.route.taxi_cost||""})},fail:function(c){a.fail({errCode:"0",errMsg:c.errMsg||""})}})};
AMapWX.prototype.getWalkingRoute=function(a){var b=Object.assign({},this.requestConfig);a.origin&&(b.origin=a.origin);a.destination&&(b.destination=a.destination);wx.request({url:"https://restapi.amap.com/v3/direction/walking",data:b,method:"GET",header:{"content-type":"application/json"},success:function(c){c&&c.data&&c.data.route&&a.success({paths:c.data.route.paths})},fail:function(c){a.fail({errCode:"0",errMsg:c.errMsg||""})}})};
AMapWX.prototype.getTransitRoute=function(a){var b=Object.assign({},this.requestConfig);a.origin&&(b.origin=a.origin);a.destination&&(b.destination=a.destination);a.strategy&&(b.strategy=a.strategy);a.city&&(b.city=a.city);a.cityd&&(b.cityd=a.cityd);wx.request({url:"https://restapi.amap.com/v3/direction/transit/integrated",data:b,method:"GET",header:{"content-type":"application/json"},success:function(c){c&&c.data&&c.data.route&&(c=c.data.route,a.success({distance:c.distance||"",taxi_cost:c.taxi_cost||
"",transits:c.transits}))},fail:function(c){a.fail({errCode:"0",errMsg:c.errMsg||""})}})};
AMapWX.prototype.getRidingRoute=function(a){var b=Object.assign({},this.requestConfig);a.origin&&(b.origin=a.origin);a.destination&&(b.destination=a.destination);wx.request({url:"https://restapi.amap.com/v3/direction/riding",data:b,method:"GET",header:{"content-type":"application/json"},success:function(c){c&&c.data&&c.data.route&&a.success({paths:c.data.route.paths})},fail:function(c){a.fail({errCode:"0",errMsg:c.errMsg||""})}})};module.exports.AMapWX=AMapWX;
src/pages/robotaxi/components/privacy-popup/index.less
@@ -88,6 +88,10 @@
      color: #FFFFFF;
    }
    .btn-no-agree {
      background: rgba(0, 0, 0, 0.1);
    }
    .close-txt {
      text-align: center;
      margin-top: 40px;
src/pages/robotaxi/components/privacy-popup/index.tsx
@@ -2,6 +2,9 @@
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 { privicyShowManager } from './utils'
type Props = {
  show?: boolean;
@@ -12,6 +15,12 @@
export default function PrivacyPopup({ show = false, onClose, onAgree }: Props) {
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const [isAgree, setIsAgree] = useState({
    0: false,
    1: false,
    2: false,
  });
  const [isAllAgree, setIsAllAgree] = useState(false);
  useEffect(() => {
    if (show && !visible) {
@@ -36,16 +45,42 @@
    }
  }, [show]);
  useEffect(() => {
    setIsAllAgree(Object.values(isAgree).every((item) => item));
  }, [isAgree]);
  const handleClose = () => {
    privicyShowManager.setHasShow(true)
    onClose?.();
  };
  const handleAgree = () => {
    if (!isAllAgree) {
      Taro.showToast({ title: '请先同意全部协议', icon: 'none' });
      return
    };
    privicyShowManager.setHasShow(true)
    onAgree?.();
  };
  const handleLinkClick = (url: string) => {
  };
  const checkItem = (key: keyof typeof isAgree) => () => {
    setIsAgree({
      ...isAgree,
      [key]: !isAgree[key],
    })
  }
  const checkAll = () => {
    setIsAllAgree(!isAllAgree);
    setIsAgree({
      0: !isAllAgree,
      1: !isAllAgree,
      2: !isAllAgree,
    })
  }
  if (!visible) return null;
@@ -63,36 +98,27 @@
        </View>
        <View className='privacy-link'>
          {
            Object.keys(isAgree).map((key) => (
              <View className='link-item' key={key}>
                <Image onClick={checkItem(key)} className='item-radio' src={isAgree[key] ? check : uncheck}
                  mode='aspectFill' />
                <Text className='item-name'>《 {key === '0' ? '用户服务协议' : '小鹏Robotaxi 平台隐私政策'} 》</Text>
              </View>
            ))
          }
          <View className='link-item'>
            <Image className='item-radio' src={`https://s.xiaopeng.com/xp-fe/mini/common/xp-logo-circle-20250728.svg`}
              mode='aspectFill' />
            <Text className='item-name'>《 用户服务协议 》</Text>
          </View>
          <View className='link-item'>
            <Image className='item-radio' src={`https://s.xiaopeng.com/xp-fe/mini/common/xp-logo-circle-20250728.svg`}
              mode='aspectFill' />
            <Text className='item-name'>《 小鹏Robotaxi 平台隐私政策 》</Text>
          </View>
          <View className='link-item'>
            <Image className='item-radio' src={`https://s.xiaopeng.com/xp-fe/mini/common/xp-logo-circle-20250728.svg`}
              mode='aspectFill' />
            <Text className='item-name'>《 小鹏Robotaxi 平台隐私政策 》</Text>
          </View>
          <View className='link-item'>
            <Image className='item-radio' src={`https://s.xiaopeng.com/xp-fe/mini/common/xp-logo-circle-20250728.svg`}
              mode='aspectFill' />
            <Text className='item-name'>《 小鹏Robotaxi 平台隐私政策 》</Text>
          </View>
          <View className='link-item'>
            <Image className='item-radio' src={`https://s.xiaopeng.com/xp-fe/mini/common/xp-logo-circle-20250728.svg`}
            <Image onClick={checkAll} className='item-radio' src={isAllAgree ? check : uncheck}
              mode='aspectFill' />
            <Text className='item-name'>全部勾选</Text>
          </View>
        </View>
        <View className='agree-btn' onClick={handleAgree}>
        <View className={'agree-btn ' + (!isAllAgree && 'btn-no-agree')} onClick={handleAgree}>
          同意并进入
        </View>
        <View className='close-txt'>暂不同意,留在游客模式</View>
        <View className='close-txt' onClick={handleClose}>暂不同意,留在游客模式</View>
      </View>
    </View>
  )
src/pages/robotaxi/components/privacy-popup/utils.ts
@@ -9,7 +9,7 @@
  return Object.values(privacyUrls).includes(url)
}
const STORAGE_KEY = 'has_show_privicy_v1'
const STORAGE_KEY = 'has_show_privicy_Robotaxi'
export const privicyShowManager = {
  getHasShow: () => {
    try {
src/pages/robotaxi/home/index.config.ts
@@ -1,3 +1,4 @@
export default {
  navigationStyle: "custom"
  navigationStyle: "custom",
  disableScroll: true
};
src/pages/robotaxi/home/index.less
@@ -1,3 +1,300 @@
.demo {
  text-align: center;
.select-city {
  position: fixed;
  top: 124px;
  left: 40px;
  display: flex;
  align-items: center;
  font-size: 28px;
  font-weight: bold;
  line-height: 48px;
  letter-spacing: normal;
  color: rgba(0, 0, 0, 0.9);
  .select-city-icon {
    width: 28px;
    height: 28px;
    margin-left: 4px;
  }
}
.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;
  .origin-box {
    position: absolute;
    bottom: 72px;
    right: 32px;
    z-index: 101;
    .origin-icon {
      width: 88px;
      height: 88px;
    }
  }
}
.content-box {
  width: 100%;
  z-index: 100;
  background-color: transparent;
  overflow: hidden;
  margin-top: -52px;
  border-radius: 32px 32px 0 0;
  .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;
          }
        }
      }
      .to-site {
        margin-top: 22px;
        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;
          }
          .to-site-fast {
            border-radius: 12px;
            background: #FFFFFF;
            padding: 8px 21px;
            font-size: 24px;
            font-weight: normal;
            line-height: 48px;
            letter-spacing: normal;
            color: #3D3D3D;
          }
        }
      }
    }
  }
  .activity {
    margin-top: 24px;
    .activity-item {
      margin-bottom: 24px;
      border-top: 2px solid #fff;
      .activity-img {
        border-radius: 32px;
        width: 100%;
        height: 190px;
      }
    }
  }
  .product-name {
    text-align: center;
    font-size: 24px;
    font-weight: 600;
    line-height: 40px;
    letter-spacing: normal;
    background: linear-gradient(99deg, #B7BEDC -8%, #CDD2EB 56%, #C7CDE7 92%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-fill-color: transparent;
  }
}
src/pages/robotaxi/home/index.tsx
@@ -1,11 +1,74 @@
import { useEffect, useState } from 'react';
import { View } from '@tarojs/components';
import Taro 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 { AMAP_KEY } from '../../../constants/api';
import selectCityIcon from '../../../robotaxi/assets/select-city.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 siteTopBg from '../../../robotaxi/assets/site-top-bg.png'
import amapFile from '../../../libs/amap-wx.130'
export default function Home() {
  const [showPrivacy, setShowPrivacy] = useState(true);
  const [showPrivacy, setShowPrivacy] = 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 [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);
  useEffect(() => {
    setShowPrivacy(Taro.getStorageSync('has_show_privicy_Robotaxi') != 'true');
    // 计算活动区域高度,用于初始隐藏
    setTimeout(() => {
      const query = Taro.createSelectorQuery();
      query.select('.content-box').boundingClientRect(rect => {
        if (rect) {
          fullHeight.current = rect.height;
          const query2 = Taro.createSelectorQuery();
          query2.select('.site-boarding-point').boundingClientRect(pointRect => {
            if (pointRect) {
              //计算显示站点停靠点所需的高度
              //pointRect.bottom 是元素相对于视口的底部边缘
              //rect.top 是内容框相对于视口的顶部边缘
              //添加一些边距(例如从 select - site 边距处增加 152 像素)以使其视觉效果更佳
              minHeight.current = (pointRect.bottom - rect.top) + 152;
              setBoxHeight(minHeight.current);
            } else {
              minHeight.current = 536;
              setBoxHeight(536);
            }
          }).exec();
        }
      }).exec();
    }, 500);
    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(() => { });
  }, []);
  const handleClosePrivacy = () => {
    setShowPrivacy(false);
@@ -13,18 +76,148 @@
  const handleAgreePrivacy = () => {
    // 处理用户同意逻辑
    console.log('用户同意了隐私政策');
    setShowPrivacy(false);
  };
    return (
      <View>
        11
        <PrivacyPopup
  const toOrigin = () => {
    const mapCtx = Taro.createMapContext('map');
    mapCtx.moveToLocation();
  }
  const handleRegionChange = (e: any) => {
    const type = e?.type;
    if (type === 'begin') {
      setShowToolTip(false);
    } else if (type === 'end') {
      setBounce(true);
      setTimeout(() => {
        setBounce(false);
        setShowToolTip(true);
      }, 600);
    }
  }
  const handleTouchStart = (e: any) => {
    isDragging.current = true;
    startY.current = e.touches[0].clientY;
    startHeight.current = typeof boxHeight === 'number' ? boxHeight : fullHeight.current;
  }
  const handleTouchMove = (e: any) => {
    if (!isDragging.current) return;
    const deltaY = e.touches[0].clientY - startY.current;
    let newHeight = startHeight.current - deltaY;
    if (newHeight < minHeight.current) newHeight = minHeight.current;
    if (newHeight > fullHeight.current) newHeight = fullHeight.current;
    setBoxHeight(newHeight);
  }
  const handleTouchEnd = () => {
    isDragging.current = false;
    const movableRange = fullHeight.current - minHeight.current;
    if (typeof boxHeight === 'number' && boxHeight > minHeight.current + movableRange / 2) {
      setBoxHeight(fullHeight.current);
    } else {
      setBoxHeight(minHeight.current);
    }
  }
  return (
    <View className="home-container">
      <Map
        id="map"
        longitude={longitude ?? 113.32452}
        latitude={latitude ?? 23.099994}
        scale={14}
        showLocation
        onRegionChange={handleRegionChange}
        style={{ width: '100%', flex: 1 }}
      >
        {/* 选择城市 */}
        <View className="select-city">
          <Text>成都</Text>
          <Image className="select-city-icon" src={selectCityIcon} />
        </View>
        {/* 地图浮层入口 */}
        <View className="map-entry">
          <Image 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 && (
            <View className="toolTip">
              <View className="toolTip-bg">
                <Text className="toolTip-bg-text">限停3分钟</Text>
              </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 className="origin-box">
          <Image onClick={toOrigin} className="origin-icon" src={originIcon} />
        </View>
      </Map>
      {/* 选择站点 */}
      <View
        className="content-box"
        style={{ height: typeof boxHeight === 'number' ? `${boxHeight}px` : 'auto', transition: isDragging.current ? 'none' : 'height 0.3s ease-out' }}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
      >
        <View className="select-site">
          <Image className="site-top-bg" src={siteTopBg} />
          <View className="site-boarding-point">
            {/* 起点 */}
            <View 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="to-site">
              <View className="to-site-dot"></View>
              <View className="to-site-address">
                <View className="to-site-text">你想去哪?</View>
                <View className="to-site-fast">去:体育西·珠江新城</View>
              </View>
            </View>
          </View>
          {/* 运营活动 */}
          <View className="activity">
            {
              [1, 2].map(item => (
                <View key={item} className="activity-item">
                  <Image className="activity-img" src={'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/1bcd9db5-c909-4edc-a3d2-61cc33c54462.jpg'} />
                </View>
              ))
            }
          </View>
          {/* 产品名 */}
          <View className="product-name">XPENG Robotaxi</View>
        </View>
      </View>
      {/* 隐私政策弹窗 */}
      <PrivacyPopup
        show={showPrivacy}
        onClose={handleClosePrivacy}
        onAgree={handleAgreePrivacy}
      />
      </View>
    )
    </View>
  )
}
src/robotaxi/assets/check.png
src/robotaxi/assets/contact-service.png
src/robotaxi/assets/coupon.png
src/robotaxi/assets/origin-icon.png
src/robotaxi/assets/pointer.png
src/robotaxi/assets/right-icon.png
src/robotaxi/assets/select-city.png
src/robotaxi/assets/site-top-bg.png
src/robotaxi/assets/suggest.png
src/robotaxi/assets/tooltip-bg.png
src/robotaxi/assets/uncheck.png