zjk
2026-04-15 8e33b540a3437c0de02483a4f60fcbacedea8120
feat: 新增收藏功能与行程分享页面
4个文件已添加
7个文件已修改
740 ■■■■■ 已修改文件
src/app.config.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/site-search.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/back-home.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.config.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.less 443 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.tsx 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/site-list-item.tsx 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/hooks/use-site-search.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/app.config.ts
@@ -36,6 +36,7 @@
        'pages/ride-guide/index',
        'pages/agreement-page/index',
        'pages/operating-area/index',
        'pages/share-trip/index',
      ],
    },
    {
src/robotaxi/api/site-search.ts
@@ -41,7 +41,7 @@
export const getCollectPoints = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + '/account/api/v1/mini-program/collect/addrlist',
        method: 'GET',
        method: 'POST',
        payload
    }, true);
}
src/robotaxi/assets/back-home.png
src/robotaxi/pages/select-map-site/index.tsx
@@ -495,6 +495,45 @@
    }
  };
    const handleCollect = async (e: any) => {
      e.stopPropagation();
      try {
        const type = selectedSite.isCollect === 1 ? 2 : 1; // 1=收藏 2=取消收藏
        const payload: any = {
          itemType: 1, // 默认为站点处理
          latitude: selectedSite.latitude,
          longitude: selectedSite.longitude,
          type: type
        };
        // 处理可能不同的 id 字段名 (id / pointId / stationId)
        if (selectedSite.stationId) {
          payload.stationId = selectedSite.stationId;
          payload.itemType = 2; // 如果是场站
        } else if (selectedSite.pointId) {
          payload.pointId = selectedSite.pointId;
        } else if (selectedSite.id) {
          payload.pointId = selectedSite.id;
        }
        await collectPoint(payload);
        setSelectedSite({ ...selectedSite, isCollect: type });
        if (pageType === 'start') {
          Taro.setStorageSync('startSite', { ...selectedSite, isCollect: type });
        }
        if (pageType === 'end') {
          Taro.setStorageSync('endSite', { ...selectedSite, isCollect: type });
        }
        Taro.showToast({
          title: type === 2 ? '已取消收藏' : '收藏成功',
          icon: 'none'
        });
      } catch (error) {
        console.log('收藏操作失败', error);
      }
    };
  return (
    <View className="home-container">
      <Map
@@ -585,7 +624,8 @@
                      true ?
                        <View className='site-item site-active'>
                          <View>{selectedSite.name}</View>
                          <Image className="collect-icon" src={activeStar} />
                          {/* <Image className="collect-icon" src={activeStar} /> */}
                          <Image onClick={handleCollect} className='collect-icon' src={selectedSite.isCollect === 1 ? activeStar : noStar} />
                        </View>
                        :
                        [1, 2].map((item) => (
src/robotaxi/pages/share-trip/index.config.ts
New file
@@ -0,0 +1,5 @@
export default {
  navigationBarTitleText: '行程分享',
  navigationStyle: 'custom',
  disableScroll: true,
}
src/robotaxi/pages/share-trip/index.less
New file
@@ -0,0 +1,443 @@
.home-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
  position: relative;
}
.select-city {
  position: fixed;
  top: 96px;
  left: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 120px;
  height: 120px;
  .select-city-icon {
    width: 120px;
    height: 120px;
  }
}
.container {
  width: 100%;
  position: fixed;
  bottom: 0;
  z-index: 10;
  &-inner {
    position: relative;
  }
}
.tip-box {
  position: absolute;
  top: -74px;
  left: 0;
  width: 100%;
  height: 136px;
  border-radius: 40px 40px 0 0;
  padding: 14px 32px;
  box-sizing: border-box;
  background: linear-gradient(
    96deg,
    #71c5ff -10%,
    #87ceff -10%,
    #898afa 103%,
    #8283ff 103%
  );
  z-index: 8;
  .tip-box-content {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .tip-box-text {
    font-size: 24px;
    line-height: 40px;
    height: 40px;
    color: #ffffff;
  }
  .tip-box-btn {
    width: 120px;
    height: 48px;
    /* 自动布局 */
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 4px 24px;
    box-sizing: border-box;
    border-radius: 1816px;
    background: #ffffff;
    /* 辅助文字/辅助文字 常规 12 */
    font-size: 24px;
    line-height: 40px;
    color: #1a1a1a;
  }
}
.calling-car {
  position: relative;
  z-index: 9;
  padding: 40px 42px 88px 34px;
  background: #f2f3f7;
  border-top: 2px solid #fff;
  border-radius: 40px 40px 0 0;
  &-bg {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 326px;
  }
  &-content {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    &-title {
      font-size: 36px;
      font-weight: 500;
      line-height: 36px;
      letter-spacing: normal;
      color: rgba(0, 0, 0, 0.9);
      display: flex;
      align-items: center;
      .time {
        margin: 0 4px;
        font-size: 40px;
        line-height: 40px;
        color: #768aff;
      }
    }
    &-desc {
      margin-top: 8px;
      font-size: 24px;
      line-height: 34px;
      color: rgba(0, 0, 0, 0.4);
    }
    &-warning {
      margin-top: 8px;
      font-size: 24px;
      line-height: 34px;
      color: #f58900;
    }
    &-desc-red {
      margin-top: 8px;
      font-size: 24px;
      line-height: 34px;
      color: #f53f3f;
    }
    .cancel-btn {
      height: 50px;
      display: flex;
      align-items: center;
      justify-content: center;
      .cancel {
        flex-shrink: 0;
        width: 144px;
        height: 48px;
      }
    }
  }
  .car-card {
    position: relative;
    z-index: 1;
    margin-top: 28px;
    border-radius: 24px;
    background: #ffffff;
    padding: 31.29px 32px 36px 33px;
    .car-card-line {
      margin-top: 29.29px;
      margin-bottom: 36px;
      height: 1px;
      background-color: rgba(0, 0, 0, 0.07);
    }
    &-detail {
      display: flex;
      align-items: center;
      justify-content: space-between;
      &-num {
        font-size: 48px;
        line-height: 64px;
        letter-spacing: normal;
        color: rgba(0, 0, 0, 0.9);
      }
      &-color {
        margin-top: 4px;
        display: flex;
        align-items: center;
        font-size: 24px;
        letter-spacing: normal;
        color: rgba(0, 0, 0, 0.6);
        &-line {
          width: 1.4px;
          height: 20px;
          background: #d8d8d8;
          margin: 0 14px;
        }
      }
      &-img {
        flex-shrink: 0;
        width: 300px;
        height: 141.43px;
      }
    }
    &-temp {
      display: flex;
      align-items: center;
      justify-content: space-between;
      &-title {
        font-size: 28px;
        line-height: 40px;
        letter-spacing: normal;
        color: rgba(0, 0, 0, 0.9);
      }
      &-content {
        display: flex;
        align-items: center;
        gap: 24px;
        &-text {
          font-size: 56px;
          line-height: 72px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.9);
          text-align: center;
        }
        &-img {
          width: 56px;
          height: 56px;
        }
      }
    }
    &-arrive {
      margin-top: 36px;
      display: flex;
      align-items: center;
      gap: 30px;
      height: 156px;
      text-align: center;
      font-size: 24px;
      line-height: 40px;
      color: rgba(0, 0, 0, 0.9);
      &-item {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 8px;
        padding: 22px 0;
        &:first-child {
          position: relative;
          border-radius: 16px;
          background: #ffffff;
          box-sizing: border-box;
          border: 2px solid rgba(0, 0, 0, 0.15);
        }
        &:last-child {
          border-radius: 16px;
          background: #ffffff;
          box-sizing: border-box;
          border: 2px solid rgba(0, 0, 0, 0.15);
        }
        &-img {
          width: 64px;
          height: 64px;
        }
      }
    }
    &-end {
      &-particulars {
        display: flex;
        align-items: center;
        justify-content: space-between;
        &-money {
          display: flex;
          align-items: center;
          view:nth-child(1) {
            font-size: 48px;
            line-height: 40px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
          }
          view:nth-child(2) {
            margin: 0 4px;
            font-size: 24px;
            line-height: 36px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
          }
          view:nth-child(3) {
            font-size: 28px;
            line-height: 36px;
            letter-spacing: normal;
            text-decoration: line-through;
            color: rgba(0, 0, 0, 0.4);
          }
          view:nth-child(4) {
            margin-left: 8px;
            border-radius: 8px;
            box-sizing: border-box;
            border: 2px solid #f58900;
            padding: 0 12px;
            line-height: 34px;
            font-size: 20px;
            font-weight: 500;
            letter-spacing: normal;
            color: #f58900;
          }
        }
        &-view {
          display: flex;
          align-items: center;
          font-size: 24px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.4);
          image {
            margin-left: 4px;
            width: 32px;
            height: 32px;
            transition: transform 0.3s ease-in-out;
            &.rotate {
              transform: rotate(90deg);
            }
          }
        }
      }
      .particulars-detail {
        border-radius: 16px;
        background: #f5f6fa;
        display: flex;
        flex-direction: column;
        gap: 16px;
        overflow: auto;
        max-height: 0;
        margin-top: 0;
        margin-bottom: 0;
        padding: 0 32px;
        opacity: 0;
        transition: all 0.3s ease-in-out;
        &.expand {
          max-height: 600px;
          margin-top: 24px;
          margin-bottom: 16px;
          padding: 24px 32px;
          opacity: 1;
        }
        &-item {
          width: 100%;
          &-title {
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-size: 24px;
            line-height: 40px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
            .color1 {
              color: #f58900;
            }
          }
          &-content {
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-size: 20px;
            line-height: 36px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.4);
          }
        }
      }
      &-cost {
        margin-top: 32px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        &-money {
          font-size: 24px;
          line-height: 44px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.9);
        }
        &-view {
          display: flex;
          align-items: center;
          font-size: 24px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.4);
          image {
            margin-left: 4px;
            width: 32px;
            height: 32px;
          }
        }
      }
      &-pay {
        margin-left: 30px;
        margin-right: 36px;
        margin-top: 60px;
        border-radius: 16px;
        background: linear-gradient(97deg, #8475ff 0%, #7dd6ff 103%);
        line-height: 96px;
        text-align: center;
        font-size: 30px;
        letter-spacing: normal;
        color: #ffffff;
      }
    }
  }
}
src/robotaxi/pages/share-trip/index.tsx
New file
@@ -0,0 +1,113 @@
import Taro from "@tarojs/taro";
import { View, Text, Image, Map } from "@tarojs/components";
import { useState, useEffect } from "react";
import "./index.less";
import homeIcon from "../../assets/back-home.png"; // 占位,建议换成首页小房子图标
import pointer from "../../assets/pointer.png"; // 车辆图标
export default function ShareTrip() {
  const [tripData, setTripData] = useState<any>(null);
  const [longitude, setLongitude] = useState(113.32452);
  const [latitude, setLatitude] = useState(23.10229);
  useEffect(() => {
    // TODO: 替换为真实的获取分享行程数据的接口
    setTimeout(() => {
      setTripData({
        etaMinutes: 4,
        distanceText: "3.6公里",
        plateNumber: "粤PXV7029",
        vehicleColor: "白色",
        vehicleCode: "03",
        vehicleImage: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/car-g9.png", // 占位图
        startName: "广州岭南职业技术学院-(公交站)",
        endName: "小鹏科技园2号楼",
        carLat: 23.10229,
        carLng: 113.32452
      });
    }, 1000);
  }, []);
  const goHome = () => {
    Taro.reLaunch({ url: "/pages/robotaxi/home/index" });
  };
  const markers = tripData
    ? [
        {
          id: 1,
          latitude: tripData.carLat,
          longitude: tripData.carLng,
          iconPath: pointer,
          width: 32,
          height: 32,
          anchor: { x: 0.5, y: 0.5 },
          callout: {
            content: `${tripData.distanceText} ${tripData.etaMinutes}分钟`,
            color: "#6D7FD9",
            fontSize: 12,
            borderRadius: 12,
            bgColor: "#ffffff",
            padding: 6,
            display: "ALWAYS"
          }
        }
      ]
    : [];
  return (
    <View className="home-container">
      <Map
        className="map-container"
        longitude={longitude}
        latitude={latitude}
        scale={14}
        markers={markers}
        style={{ width: "100%", height: "100%" }}
      >
        {/* 顶部返回/首页按钮 */}
        <View onClick={goHome} className="select-city">
          <Image className="select-city-icon" src={homeIcon} />
        </View>
        <View className="container">
          {/* 提醒 */}
          <View className="tip-box">
            <View className="tip-box-content">
              <View className="tip-box-text">
                开启提醒后,亲友行程动态秒通知
              </View>
              <View className="tip-box-btn">去开启</View>
            </View>
          </View>
          <View className="container-inner">
            <View catchMove onClick={e => e.stopPropagation()}>
              <View className="calling-car">
                <Image
                  className="calling-car-bg"
                  src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png"
                />
                <View className="car-card">
                  <View className="car-card-detail">
                    <View>
                      <View className="car-card-detail-num">粤PXV7029</View>
                      <View className="car-card-detail-color">
                        白色
                        <View className="car-card-detail-color-line"></View>
                        车身编码: 03
                      </View>
                    </View>
                    <Image
                      className="car-card-detail-img"
                      src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png"
                    />
                  </View>
                </View>
              </View>
            </View>
          </View>
        </View>
      </Map>
    </View>
  );
}
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
@@ -20,27 +20,44 @@
  const [thumbTop, setThumbTop] = useState(0);
  const [containerHeight, setContainerHeight] = useState(0);
  const [showScrollbar, setShowScrollbar] = useState(false);
  const [current, setCurrent] = useState(1);
  const [hasNextPage, setHasNextPage] = useState(false);
  const [loading, setLoading] = useState(false);
  const fetchData = async () => {
  const fetchData = async (page = 1) => {
    if (loading) return;
    setLoading(true);
    try {
      const res = await getCollectPoints({});
      if (res && Array.isArray(res)) {
        setList(res);
      } else if (res && res.data && Array.isArray(res.data)) {
        setList(res.data);
      } else if (res && res.list && Array.isArray(res.list)) {
        setList(res.list);
      const res = await getCollectPoints({
        current: page,
        size: 4,
      });
      if (res && res.records) {
        setList(prev => page === 1 ? res.records : [...prev, ...res.records]);
        setHasNextPage(res.hasNextPage);
        setCurrent(res.current);
      }
    } catch (e) {
      console.log('获取收藏夹失败', e);
    } finally {
      setLoading(false);
    }
  };
  const handleScrollToLower = () => {
    if (hasNextPage && !loading) {
      fetchData(current + 1);
    }
  };
  const handleDelete = async (item: any) => {
    try {
      await collectPoint({
        action: 2, // 假设 2 是取消收藏
        pointId: item.id || item.pointId,
        type: 2,
        pointId: item.pointId,
        itemType: item.itemType,
        latitude: item.latitude,
        longitude: item.longitude,
      });
      // 重新拉取数据或从列表中移除
      setList(prev => prev.filter(f => (f.id || f.pointId) !== (item.id || item.pointId)));
@@ -127,13 +144,14 @@
            upperThreshold={30}
            showScrollbar={false}
            onScroll={onScroll}
            onScrollToLower={handleScrollToLower}
          >
            <View className='favorite-list-inner'>
              {list.map((item, index) => (
                <View key={item.id || index} className='favorite-item' onClick={() => onSelect(item)}>
                  <View className='favorite-info'>
                    <View className='favorite-name'>{item.name}</View>
                    <View className='favorite-address'>{item.address}</View>
                    <View className='favorite-name'>{item.pointName}</View>
                    <View className='favorite-address'>{item.pointAddress}</View>
                  </View>
                  <Image onClick={(e) => {
                    e.stopPropagation();
src/robotaxi/pages/site-search/components/site-list-item.tsx
@@ -1,4 +1,7 @@
import Taro from '@tarojs/taro';
import { View, Image } from '@tarojs/components';
import { useState, useEffect } from 'react';
import { collectPoint } from '../../../api/site-search';
type Props = {
  // 站点数据
@@ -30,6 +33,45 @@
  onSelect,
  renderHighlightedText
}: Props) {
  const [isCollected, setIsCollected] = useState(!!item.isCollected);
  useEffect(() => {
    setIsCollected(item.isCollect === 1);
  }, [item.isCollect]);
  const handleCollect = async (e: any) => {
    e.stopPropagation();
    try {
      const type = isCollected ? 2 : 1; // 1=收藏 2=取消收藏
      const payload: any = {
        itemType: 1, // 默认为站点处理
        latitude: item.latitude,
        longitude: item.longitude,
        type: type
      };
      // 处理可能不同的 id 字段名 (id / pointId / stationId)
      if (item.stationId) {
        payload.stationId = item.stationId;
        payload.itemType = 2; // 如果是场站
      } else if (item.pointId) {
        payload.pointId = item.pointId;
      } else if (item.id) {
        payload.pointId = item.id;
      }
      await collectPoint(payload);
      setIsCollected(!isCollected);
      Taro.showToast({
        title: isCollected ? '已取消收藏' : '收藏成功',
        icon: 'none'
      });
    } catch (error) {
      console.log('收藏操作失败', error);
    }
  };
  return (
    <View>
      <View hoverClass='common-hover-class' onClick={() => onSelect(item)} className='history-item' key={itemKey}>
@@ -42,7 +84,7 @@
        </View>
        {showDistance ?
          <View className='collect-box'>
            <Image className='collect-icon' src={item.isCollected ? starSrc : noStarSrc} />
            <Image onClick={handleCollect} className='collect-icon' src={isCollected ? starSrc : noStarSrc} />
            {renderHighlightedText(item.distance || '-', 'history-distance')}
          </View>
          : null}
src/robotaxi/pages/site-search/hooks/use-site-search.ts
@@ -113,27 +113,32 @@
    setNonOperationalSite([]);
  };
  // 初始化:拉取附近站点、历史记录、页面类型
  useEffect(() => {
  // 重新拉取初始化数据(历史记录和附近站点)
  const refreshData = async () => {
    const { pageType: type, latitude: lat, longitude: lon, electronicAreaId } = deps.router.params;
    if (lat && lon) {
      fetchNearbySites(Number(lat), Number(lon), electronicAreaId);
    }
    checkOtp(false).then(otp => {
      if (otp && lat && lon) {
        const payload = { lat: Number(lat), lon: Number(lon), electronicAreaId };
        const handleSuccess = (res: any) => {
          if (Array.isArray(res)) {
            setHistorySite(res.map(normalizeSiteItem));
          }
        };
        if (type === 'start') {
          getStartPoints(payload).then(handleSuccess);
        } else if (type === 'end') {
          getEndPoints(payload).then(handleSuccess);
    const otp = await checkOtp(false);
    if (otp && lat && lon) {
      const payload = { lat: Number(lat), lon: Number(lon), electronicAreaId };
      const handleSuccess = (res: any) => {
        if (Array.isArray(res)) {
          setHistorySite(res.map(normalizeSiteItem));
        }
      };
      if (type === 'start') {
        getStartPoints(payload).then(handleSuccess);
      } else if (type === 'end') {
        getEndPoints(payload).then(handleSuccess);
      }
    });
    }
  };
  // 初始化:拉取附近站点、历史记录、页面类型
  useEffect(() => {
    refreshData();
    const { pageType: type } = deps.router.params;
    if (type) {
      setPageType(type);
    }
@@ -147,6 +152,7 @@
    nearbySiteList,
    searchSite,
    debouncedSearch,
    clearSearchResults
    clearSearchResults,
    refreshData
  };
}
src/robotaxi/pages/site-search/index.tsx
@@ -44,7 +44,8 @@
    nearbySiteList,
    searchSite,
    debouncedSearch,
    clearSearchResults
    clearSearchResults,
    refreshData
  } = useSiteSearch({
    router,
    searchValue,
@@ -188,6 +189,11 @@
      setTipType('');
    }, 600);
  }
  const handleCloseFavoritePopup = () => {
    setShowFavoritePopup(false);
    refreshData(); // 关闭弹窗时刷新列表数据(包括历史记录和附近站点)
  };
  const handleSelectFavorite = (item) => {
    setShowFavoritePopup(false);
@@ -338,7 +344,7 @@
      />
      <FavoritePopup
        show={showFavoritePopup}
        onClose={() => setShowFavoritePopup(false)}
        onClose={handleCloseFavoritePopup}
        onSelect={handleSelectFavorite}
      />
    </View>