zjk
2026-04-24 070574f941f892e9ed24f6c6f035452e0fe36e69
feat(home): 新增首次进入营销弹窗及优惠券选择组件增强

- 添加首次进入应用时的营销弹窗组件,支持单张和多张优惠券展示
- 新增营销弹窗相关图片资源文件
- 扩展优惠券选择弹窗,支持显示不可用优惠券列表及展开/收起功能
- 增加获取用户下单可用优惠券列表的API接口
- 移除优惠券页面中未使用的优惠券选择弹窗组件
- 优化首页逻辑,在应用显示时检查是否需要展示营销弹窗
13个文件已添加
6个文件已修改
1157 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/marketing-popup/index.less 364 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/marketing-popup/index.tsx 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/coupon.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/home.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-circle.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-close.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-coupon-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-coupon-topBg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-manyBg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-left.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-top-left.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-top-right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-singleBg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/coupon-select-popup/index.less 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/coupon-select-popup/index.tsx 479 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/coupon/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/marketing-popup/index.less
New file
@@ -0,0 +1,364 @@
.marketing-popup {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10001;
  display: flex;
  justify-content: center;
  pointer-events: auto;
  font-family: PingFang SC;
}
.marketing-popup-mask {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  pointer-events: auto;
  opacity: 0;
  transition: opacity 0.26s ease;
}
.marketing-popup-circle-icon {
  position: absolute;
  top: 248px;
  left: -46px;
  width: 824px;
  height: 876px;
  z-index: 10002;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 0.26s ease, transform 0.26s ease;
}
.marketing-popup-close {
  position: absolute;
  top: -100px;
  right: 0;
  width: 60px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10011;
}
.marketing-popup-close-single {
  margin: 0 auto;
  margin-top: 121px;
  width: 60px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10011;
}
.marketing-popup-close-icon {
  width: 60px;
  height: 60px;
}
.marketing-popup-multi-bg {
  position: absolute;
  top: 0;
  left: 0;
  width: 560px;
  height: 658px;
  z-index: 10002;
  background: url(../../../../../robotaxi/assets/marketing-manyBg.png)
    no-repeat center center;
  background-size: 100% 100%;
}
.marketing-popup-item {
  width: 502px;
  height: 146px;
  display: flex;
  align-items: center;
  background: url(../../../../../robotaxi/assets/marketing-coupon-bg.png)
    no-repeat center center;
  background-size: 100% 100%;
  .marketing-popup-item-left {
    width: 158px;
    box-sizing: border-box;
    height: 100%;
    padding: 20px 0 0 38px;
    .marketing-popup-item-price {
      display: flex;
      .marketing-popup-item-amount {
        font-family: PingFang SC;
        font-size: 64px;
        font-weight: 500;
        line-height: 68px;
        text-align: center;
        display: flex;
        align-items: center;
        letter-spacing: normal;
        background: linear-gradient(180deg, #9b5d33 25%, #4c1d05 100%);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        text-fill-color: transparent;
      }
      .marketing-popup-item-unit {
        font-family: PingFang SC;
        font-size: 24px;
        font-weight: 500;
        line-height: 24px;
        letter-spacing: normal;
        color: #7e4622;
        margin-left: 7px;
        margin-top: 35px;
      }
    }
    .marketing-popup-item-tag {
      font-family: PingFang SC;
      font-size: 20px;
      font-weight: normal;
      line-height: 20px;
      text-align: center;
      display: flex;
      align-items: center;
      letter-spacing: normal;
      color: rgba(155, 93, 51, 0.4);
      margin-top: 11px;
    }
  }
  .marketing-popup-item-right {
    flex: 1;
    height: 100%;
    padding-top: 44px;
    padding-left: 36px;
    box-sizing: border-box;
    .marketing-popup-item-title {
      font-family: PingFang SC;
      font-size: 28px;
      font-weight: 500;
      line-height: 28px;
      text-align: center;
      display: flex;
      align-items: center;
      letter-spacing: normal;
      background: linear-gradient(180deg, #9b5d33 25%, #4c1d05 100%);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-fill-color: transparent;
    }
    .marketing-popup-item-expire {
      font-family: PingFang SC;
      font-size: 20px;
      font-weight: normal;
      line-height: 20px;
      text-align: center;
      display: flex;
      align-items: center;
      letter-spacing: normal;
      color: rgba(155, 93, 51, 0.4);
      margin-top: 12px;
    }
  }
}
.marketing-popup-multi-panel {
  margin-top: 408px;
  z-index: 10010;
  width: 560px;
  height: auto;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  opacity: 0;
  transform: translateY(24px) scale(0.98);
  transition: opacity 0.26s ease, transform 0.26s ease;
  .marketing-popup-panel {
    position: relative;
    z-index: 10010;
  }
  .marketing-popup-title {
    font-family: PingFang SC;
    font-size: 44px;
    font-weight: 500;
    line-height: 48px;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    letter-spacing: normal;
    color: #fff9f2;
    margin-top: 64px;
    margin-bottom: 44px;
  }
  .marketing-popup-action {
    width: 402px;
    height: 88px;
    border-radius: 93.1px;
    background: linear-gradient(359deg, #ffd099 -3%, #ffefca 96%);
    box-shadow: inset 0px -4px 4px 0px rgba(255, 255, 255, 0.4);
    font-family: PingFang SC;
    font-size: 29.79px;
    font-weight: 500;
    line-height: 44.68px;
    display: flex;
    align-items: center;
    justify-content: center;
    letter-spacing: normal;
    color: #5b290e;
    margin: 0 auto;
    margin-top: 40px;
  }
  .marketing-popup-panel-content {
    width: 100%;
    height: 502px;
    .marketing-popup-list {
      width: 100%;
      height: 100%;
      overflow-y: auto;
      box-sizing: border-box;
      .marketing-popup-item {
        margin-bottom: 16px;
      }
    }
  }
}
.marketing-popup-single-panel {
  margin-top: 388px;
  z-index: 10010;
  width: 540px;
  height: auto;
  background: url(../../../../../robotaxi/assets/marketing-singleBg.png)
    no-repeat top center;
  background-size: 100% auto;
  opacity: 0;
  transform: translateY(24px) scale(0.98);
  transition: opacity 0.26s ease, transform 0.26s ease;
  .marketing-popup-title {
    font-size: 36px;
    line-height: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(180deg, #9b5d33 25%, #4c1d05 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-fill-color: transparent;
    margin-top: 68px;
    .marketing-popup-coupon-title-top-icon {
      width: 51px;
      height: 3px ;
    }
  }
  .marketing-popup-coupon-title {
    margin-top: 64px;
    font-size: 56px;
    line-height: 68px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(180deg, #9b5d33 25%, #4c1d05 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-fill-color: transparent;
    .marketing-popup-coupon-title-icon {
      width: 28px;
      height: 44.8px;
    }
  }
  .marketing-popup-coupon-tag-text {
    font-size: 20px;
    line-height: 20px;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgba(155, 93, 51, 0.4);
    margin-top: 20px;
  }
  .marketing-popup-coupon-expire-text {
    font-size: 20px;
    line-height: 20px;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgba(155, 93, 51, 0.4);
    margin-top: 14px;
  }
  .marketing-popup-action {
    border-radius: 93.1px;
    background: linear-gradient(359deg, #ffd099 -3%, #ffefca 96%);
    box-shadow: inset 0px -4px 4px 0px rgba(255, 255, 255, 0.4);
    width: 400.34px;
    height: 89.38px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto;
    margin-top: 112px;
    font-size: 29.79px;
    font-weight: 500;
    line-height: 44.68px;
    display: flex;
    align-items: center;
    color: #5b290e;
  }
}
.marketing-popup.is-active {
  .marketing-popup-mask {
    opacity: 1;
  }
  .marketing-popup-circle-icon {
    opacity: 1;
    transform: scale(1);
  }
  .marketing-popup-multi-panel,
  .marketing-popup-single-panel {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}
.marketing-popup-multi-bg--two {
  width: 560px;
  height: 516px;
  background: url(../../../../../robotaxi/assets/marketing-bg.png) no-repeat
    center center;
  background-size: 100% 100%;
}
.marketing-popup-multi-panel--two {
  .marketing-popup-title {
    margin-bottom: 60px !important;
  }
  .marketing-popup-item {
    margin-bottom: 24px !important;
  }
  .marketing-popup-panel-content {
    height: 344px !important;
  }
  .marketing-popup-action {
    margin-top: 36px !important;
  }
}
src/pages/robotaxi/home/components/marketing-popup/index.tsx
New file
@@ -0,0 +1,200 @@
import { useEffect, useState } from 'react';
import { View, Text, Image, ScrollView } from '@tarojs/components';
import closeIcon from '../../../../../robotaxi/assets/marketing-close.png';
import circleIcon from '../../../../../robotaxi/assets/marketing-circle.png';
import singleLeftIcon from '../../../../../robotaxi/assets/marketing-single-left.png';
import singleRightIcon from '../../../../../robotaxi/assets/marketing-single-right.png';
import singleTopLeftIcon from '../../../../../robotaxi/assets/marketing-single-top-left.png';
import singleTopRightIcon from '../../../../../robotaxi/assets/marketing-single-top-right.png';
import './index.less';
type MarketingPopupProps = {
    show: boolean;
    onClose: () => void;
};
type MarketingCoupon = {
    id: string;
    amount: string;
    unit: string;
    title: string;
    expireText: string;
    tagText: string;
};
const MARKETING_COUPONS: MarketingCoupon[] = [
    {
        id: 'new-user-coupon',
        amount: '1',
        unit: '元',
        title: '1元乘车券',
        expireText: '2026.04.24 23:59到期',
        tagText: '满1元可用'
    },
    // {
    //     id: 'discount-coupon-1',
    //     amount: '1',
    //     unit: '折',
    //     title: '1折出行券',
    //     expireText: '2026.04.24 23:59到期',
    //     tagText: '无门槛'
    // },
    // {
    //     id: 'discount-coupon-5',
    //     amount: '5',
    //     unit: '折',
    //     title: '5折出行券',
    //     expireText: '2026.04.24 23:59到期',
    //     tagText: '无门槛'
    // },
    // {
    //     id: 'discount-coupon-5',
    //     amount: '5',
    //     unit: '折',
    //     title: '5折出行券',
    //     expireText: '2026.04.24 23:59到期',
    //     tagText: '无门槛'
    // },
    // {
    //     id: 'discount-coupon-5',
    //     amount: '5',
    //     unit: '折',
    //     title: '5折出行券',
    //     expireText: '2026.04.24 23:59到期',
    //     tagText: '无门槛'
    // }
];
export default function MarketingPopup(props: MarketingPopupProps) {
    const animationDuration = 260;
    const [shouldRender, setShouldRender] = useState(props.show);
    const [isActive, setIsActive] = useState(props.show);
    useEffect(() => {
        let timer: ReturnType<typeof setTimeout> | null = null;
        if (props.show) {
            setShouldRender(true);
            timer = setTimeout(() => {
                setIsActive(true);
            }, 16);
        } else {
            setIsActive(false);
            timer = setTimeout(() => {
                setShouldRender(false);
            }, animationDuration);
        }
        return () => {
            if (timer) {
                clearTimeout(timer);
            }
        };
    }, [props.show]);
    const isSingleCoupon = MARKETING_COUPONS.length === 1;
    const isTwoCoupons = MARKETING_COUPONS.length === 2;
    const preventTouchEvent = (event: any) => {
        if (event?.stopPropagation) {
            event.stopPropagation();
        }
        if (event?.preventDefault) {
            event.preventDefault();
        }
    };
    const renderCouponItem = (coupon: MarketingCoupon) => {
        return (
            <View className='marketing-popup-item' key={coupon.id}>
                <View className='marketing-popup-item-left'>
                    <View className='marketing-popup-item-price'>
                        <Text className='marketing-popup-item-amount'>{coupon.amount}</Text>
                        <Text className='marketing-popup-item-unit'>{coupon.unit}</Text>
                    </View>
                    <View className='marketing-popup-item-tag'>{coupon.tagText}</View>
                </View>
                <View className='marketing-popup-item-right'>
                    <View className='marketing-popup-item-title'>{coupon.title}</View>
                    <View className='marketing-popup-item-expire'>{coupon.expireText}</View>
                </View>
            </View>
        );
    };
    const renderSingleCouponModule = () => {
        return (
            <View className='marketing-popup-single-panel'>
                <View className='marketing-popup-title'>
                  <Image className='marketing-popup-coupon-title-top-icon' src={singleTopLeftIcon} />
                  <View style={{ margin: '0 14px' }}>恭喜获得</View>
                  <Image className='marketing-popup-coupon-title-top-icon' src={singleTopRightIcon} />
                </View>
                <View className='marketing-popup-coupon-title'>
                  <Image className='marketing-popup-coupon-title-icon' src={singleLeftIcon} />
                  <View style={{ margin: '0 8px' }}>媒体尝鲜券</View>
                  <Image className='marketing-popup-coupon-title-icon' src={singleRightIcon} />
                </View>
                <View className='marketing-popup-coupon-tag-text'>无门槛</View>
                <View className='marketing-popup-coupon-expire-text'>2026.04.24 23:59到期</View>
                <View className='marketing-popup-action' onClick={props.onClose}>
                    立即领取
                </View>
                <View className='marketing-popup-close-single' onClick={props.onClose}>
                    <Image className='marketing-popup-close-icon' src={closeIcon} />
                </View>
            </View>
        );
    };
    const renderMultiCouponModule = () => {
        return (
            <View className={`marketing-popup-multi-panel ${isTwoCoupons ? 'marketing-popup-multi-panel--two' : ''}`}>
                <View className='marketing-popup-close' onClick={props.onClose}>
                    <Image className='marketing-popup-close-icon' src={closeIcon} />
                </View>
                <View
                  className={`marketing-popup-multi-bg ${isTwoCoupons ? 'marketing-popup-multi-bg--two' : ''}`}
                />
                <View className='marketing-popup-panel'>
                    <View className='marketing-popup-title'>恭喜获得新人券</View>
                    <View className='marketing-popup-panel-content'>
                        <ScrollView
                          className='marketing-popup-list'
                          scrollY
                          enhanced
                          showScrollbar={false}
                        >
                            {MARKETING_COUPONS.map(renderCouponItem)}
                            <View style={{ height: '1px' }} />
                        </ScrollView>
                    </View>
                    <View className='marketing-popup-action' onClick={props.onClose}>
                        立即领取
                    </View>
                </View>
            </View>
        );
    };
    if (!shouldRender) return null;
    return (
        <View className={`marketing-popup ${isActive ? 'is-active' : ''}`} catchMove>
            <View
              className='marketing-popup-mask'
              catchMove
              onClick={props.onClose}
              onTouchMove={preventTouchEvent}
              onTouchStart={preventTouchEvent}
            />
            <Image
              className='marketing-popup-circle-icon'
              src={circleIcon}
              onClick={props.onClose}
              onTouchMove={preventTouchEvent}
              onTouchStart={preventTouchEvent}
            />
            {isSingleCoupon ? renderSingleCouponModule() : renderMultiCouponModule()}
        </View>
    );
}
src/pages/robotaxi/home/index.tsx
@@ -7,6 +7,7 @@
import OrderStatusCard from './components/order-status-card';
import MapOverlayTips from './components/map-overlay-tips';
import MapPointer from './components/map-pointer';
import MarketingPopup from './components/marketing-popup';
import selectCityIcon from '../../../robotaxi/assets/select-city.png'
import personalCenter from '../../../robotaxi/assets/personal-center.png'
import coupon from '../../../robotaxi/assets/coupon.png'
@@ -56,7 +57,8 @@
  getBannerList,
  getAgreement,
  getOrderActive,
  getHotFixInfo
  getHotFixInfo,
  firstEnter
} from '../../../robotaxi/api/home';
import { getServicePhone } from '../../../robotaxi/api/customer-service';
import { getOrderDistance } from '../../../robotaxi/api/select-map-site';
@@ -65,6 +67,25 @@
type HomeTripStatus = TripStatus | 0;
export default function Home() {
  const resolveMarketingVisible = (response: any) => {
    if (typeof response === 'boolean') return response;
    if (typeof response === 'number') return response === 1;
    if (Array.isArray(response)) return response.length > 0;
    const data = response?.data ?? response;
    if (typeof data === 'boolean') return data;
    if (typeof data === 'number') return data === 1;
    if (Array.isArray(data)) return data.length > 0;
    return Boolean(
      data?.showMarketing ??
      data?.showPopup ??
      data?.isShow ??
      data?.isFirstEnter ??
      data?.firstEnter
    );
  };
  const renderTailPreserveText = (text: string, tailCount = 3) => {
    const safeText = String(text || '');
    if (!safeText) return null;
@@ -81,6 +102,7 @@
    );
  };
  const [showMarketing, setShowMarketing] = useState(false); // 营销弹窗是否显示
  const [showPrivacy, setShowPrivacy] = useState(false); // 隐私政策弹窗是否显示
  const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示
  const mapState = useMapState(); // 地图状态容器
@@ -384,6 +406,13 @@
  useDidShow(() => {
    bootstrap.onDidShow();
    refreshQuickCapsule();
    firstEnter()
      .then((res: any) => {
        setShowMarketing(resolveMarketingVisible(res));
      })
      .catch(() => {
        setShowMarketing(false);
      });
    if (typeof latitude === 'number' && typeof longitude === 'number') {
      tryStartPollingOnInit({
        lat: latitude,
@@ -515,7 +544,7 @@
            <Image className="select-city-icon" src={selectCityIcon} />
          </View>
          {weatherData && (
            <View className="weather-info">
            <View className="weather-info" onClick={() => setShowMarketing(true)}>
              <Image className="weather-icon" src={getWeatherIconByDesc(weatherData.weather)} />
              <Text>{weatherData.temperature}°C</Text>
            </View>
@@ -672,6 +701,7 @@
        show={showCustomerService}
        onClose={handleCloseCustomerService}
      />
      <MarketingPopup show={showMarketing} onClose={() => setShowMarketing(false)} />
    </View>
  )
}
src/robotaxi/api/coupon.ts
@@ -65,3 +65,11 @@
        method: 'GET'
    }, true);
}
// 冒泡下单可用优惠券
export const getUserCouponList = async (orderMoney: number) => {
    return request({
        url: _ROBOTAXI_HOST + `/account/api/v1/mini-program/getUserCouponList?orderMoney=${orderMoney}`,
        method: 'GET'
    }, true);
}
src/robotaxi/api/home.ts
@@ -110,3 +110,11 @@
        payload
    }, true);
}
// 第一次进入应用
export const firstEnter = async () => {
    return request({
        url: _ROBOTAXI_HOST + '/operation/v1/api/mini-program/acticity/firstEnter',
        method: 'GET'
    }, true);
}
src/robotaxi/assets/marketing-bg.png
src/robotaxi/assets/marketing-circle.png
src/robotaxi/assets/marketing-close.png
src/robotaxi/assets/marketing-coupon-bg.png
src/robotaxi/assets/marketing-coupon-topBg.png
src/robotaxi/assets/marketing-manyBg.png
src/robotaxi/assets/marketing-single-left.png
src/robotaxi/assets/marketing-single-right.png
src/robotaxi/assets/marketing-single-top-left.png
src/robotaxi/assets/marketing-single-top-right.png
src/robotaxi/assets/marketing-singleBg.png
src/robotaxi/components/coupon-select-popup/index.less
@@ -194,6 +194,60 @@
        font-size: 28px;
        color: rgba(0, 0, 0, 0.4);
      }
      .unavailable-section {
        margin-top: 12px;
      }
      .unavailable-toggle {
        height: 52px;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .unavailable-toggle-text {
        font-size: 24px;
        color: rgba(0, 0, 0, 0.4);
      }
      .unavailable-toggle-arrow {
        width: 24px;
        height: 24px;
        margin-left: 8px;
        transform: rotate(90deg);
        transition: transform 0.2s ease;
      }
      .unavailable-toggle-arrow.expanded {
        transform: rotate(270deg);
      }
      .unavailable-list {
        margin-top: 8px;
      }
      .coupon-item-wrap {
        margin-bottom: 24px;
      }
      .coupon-inline-detail {
        margin-top: -12px;
        margin-bottom: 8px;
        padding: 24px 28px;
        border-radius: 0 0 24px 24px;
        background: #ffffff;
        border: 1px dashed rgba(0, 0, 0, 0.1);
        border-top: none;
        box-shadow: 0px 12px 40px 0px rgba(0, 0, 0, 0.05);
      }
      .coupon-inline-detail-text {
        font-size: 24px;
        line-height: 40px;
        color: rgba(0, 0, 0, 0.45);
        white-space: pre-wrap;
      }
    }
    /* 自定义滚动条轨道 */
@@ -227,7 +281,7 @@
      background: url("../../assets/coupon-bg.png") center center / 100% 100%
        no-repeat;
      padding: 28px 24px 28px 28px;
      margin-bottom: 24px;
      margin-bottom: 0;
      display: flex;
      align-items: center;
      justify-content: space-between;
@@ -327,6 +381,12 @@
            width: 24px;
            height: 24px;
            margin-left: 4px;
            transform: rotate(0deg);
            transition: transform 0.2s ease;
          }
          .more-img.expanded {
            transform: rotate(180deg);
          }
        }
      }
src/robotaxi/components/coupon-select-popup/index.tsx
@@ -1,67 +1,103 @@
import Taro from '@tarojs/taro';
import { View, Text, Image, ScrollView, RichText } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './index.less';
import Taro from "@tarojs/taro";
import { View, Text, Image, ScrollView } from "@tarojs/components";
import { useEffect, useState } from "react";
import "./index.less";
import closeIcon from "../../assets/close-icon.png";
import icon from "../../assets/coupon-arrow.png";
import couponSelectIcon from "../../assets/coupon-select.png";
import couponSelectedIcon from "../../assets/coupon-selected.png";
import couponArrowRight from "../../assets/coupon-arrow-right.png";
import { getCouponList, getCouponDetail, CouponDetail } from '../../api/coupon';
import couponSelectedIcon from "../../assets/coupon-selected.png";
import { getUserCouponList } from "../../api/coupon";
type Props = {
  show: boolean;
  orderMoney?: number;
  onClose: () => void;
  onSelect: (item: any) => void;
};
export default function FavoritePopup({ show, onClose, onSelect }: Props) {
  const [list, setList] = useState<any[]>([]);
const toPopupCoupon = (item: any, status: 1 | 3) => {
  return {
    couponId: String(item?.id ?? ""),
    operationCouponId: String(item?.id ?? ""),
    title: item?.couponName || "",
    discountAmount: Number(item?.amount || 0),
    conditionAmount: Number(item?.conditionalAmount || 0),
    expireTime: item?.endTime || "",
    status,
    couponDesc: item?.couponDesc || "",
    unusableReason:
      item?.unusableReason ||
      item?.notUsableReason ||
      item?.notUseReason ||
      item?.reason ||
      ""
  };
};
export default function FavoritePopup({
  show,
  orderMoney = 0,
  onClose,
  onSelect
}: Props) {
  const [usableCoupons, setUsableCoupons] = useState<any[]>([]);
  const [unusableCoupons, setUnusableCoupons] = useState<any[]>([]);
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const [detailAnimationData, setDetailAnimationData] = useState<any>(null);
  const [canScroll, setCanScroll] = useState(false);
  const [thumbHeight, setThumbHeight] = useState(0);
  const [thumbTop, setThumbTop] = useState(0);
  const [showScrollbar, setShowScrollbar] = useState(false);
  const [couponCount, setCouponCount] = useState(0);
  const [activeCouponId, setActiveCouponId] = useState('');
  const [detailVisible, setDetailVisible] = useState(false);
  const [currentCoupon, setCurrentCoupon] = useState<CouponDetail | null>(null);
  const [activeCouponId, setActiveCouponId] = useState("");
  const [expandUnusable, setExpandUnusable] = useState(false);
  const [expandedCouponIds, setExpandedCouponIds] = useState<string[]>([]);
   const getList = async (
      page: number
    ) => {
      try {
        const res = await getCouponList({ pageNum: page, pageSize: 10, status: 1 });
        const newList = res.list || [];
        setList(newList);
        setCouponCount(res.total || 0);
        // 如果返回数量少于 pageSize,说明没有更多
      } catch (e) {
        console.error(e);
      }
    };
  const getList = async () => {
    try {
      const res = await getUserCouponList(orderMoney);
      const payload = res?.data ?? res ?? {};
      const usableListRaw = payload?.usableCouponList || [];
      const notUsableListRaw = payload?.notUsableCouponList || [];
      const usableList = usableListRaw.map((item: any) =>
        toPopupCoupon(item, 1)
      );
      const notUsableList = notUsableListRaw.map((item: any) =>
        toPopupCoupon(item, 3)
      );
      setUsableCoupons(usableList);
      setUnusableCoupons(notUsableList);
      setCouponCount(usableList.length + notUsableList.length);
      setExpandUnusable(false);
    } catch (e) {
      console.error(e);
      setUsableCoupons([]);
      setUnusableCoupons([]);
      setCouponCount(0);
      setExpandUnusable(false);
    }
  };
  const gotoMyCoupon = () => {
    Taro.navigateTo({ url: '/robotaxi/pages/coupon/index' });
    Taro.navigateTo({ url: "/robotaxi/pages/coupon/index" });
  };
  const handleSelectCoupon = (item: any) => {
    const couponKey = item?.operationCouponId
    const couponKey = item?.operationCouponId || item?.couponId || "";
    setActiveCouponId(couponKey);
    onSelect(item);
  };
   const formatThresholdText = (conditionAmount?: number) => {
  const formatThresholdText = (conditionAmount?: number) => {
    const amount = Number(conditionAmount || 0);
    if (amount > 0) {
      return `满${amount}可用`;
      return `满 ${amount} 可用`;
    }
    return "无门槛";
  };
   const formatTime = (t?: string, type?: string) => {
  const formatTime = (t?: string, type?: string) => {
    const d = new Date(t);
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -73,52 +109,20 @@
      : `${y}-${m}-${day}`;
  };
  const openDetailPopup = async (item: any) => {
    try {
      Taro.showLoading({ title: '加载中...' });
      const detail = await getCouponDetail(item.couponId);
      detail.couponDesc = decodeURIComponent(detail.couponDesc || '');
      if (detail.couponDesc) {
        detail.couponDesc = detail.couponDesc.replace(
          /<img/gi,
          '<img style="max-width:100%;height:auto;margin:0 auto;display:block"'
        );
      }
      setCurrentCoupon(detail);
      Taro.hideLoading();
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
      });
      animation.translateY(0).step();
      setDetailAnimationData(animation.export());
      setDetailVisible(true);
    } catch (error) {
      Taro.hideLoading();
      Taro.showToast({ title: '获取详情失败', icon: 'none' });
  const resolveUnusableDetailText = (item: any) => {
    if (!item?.unavailable) {
      return "不可用原因:-";
    }
  };
  const closeDetailPopup = () => {
    const animation = Taro.createAnimation({
      duration: 300,
      timingFunction: 'ease-in'
    });
    animation.translateY(500).step();
    setDetailAnimationData(animation.export());
    setDetailVisible(false);
    return `不可用原因:${item?.unavailable}`;
  };
  useEffect(() => {
    if (list && list.length > 0 && visible) {
    if (visible) {
      setTimeout(() => {
        const query = Taro.createSelectorQuery();
        query.select('.coupon-list').boundingClientRect();
        query.select('.coupon-list-inner').boundingClientRect();
        query.exec((res) => {
        query.select(".coupon-list").boundingClientRect();
        query.select(".coupon-list-inner").boundingClientRect();
        query.exec(res => {
          if (res[0] && res[1]) {
            const wrapperHeight = res[0].height;
            const contentHeight = res[1].height;
@@ -126,7 +130,8 @@
            setCanScroll(isOverflow);
            if (isOverflow) {
              setShowScrollbar(true);
              const calculatedThumbHeight = (wrapperHeight / contentHeight) * wrapperHeight;
              const calculatedThumbHeight =
                (wrapperHeight / contentHeight) * wrapperHeight;
              setThumbHeight(Math.max(calculatedThumbHeight, 20));
            } else {
              setShowScrollbar(false);
@@ -135,171 +140,261 @@
          }
        });
      }, 300);
    } else {
      setCanScroll(false);
      setShowScrollbar(false);
      setThumbTop(0);
      return;
    }
  }, [list, visible]);
    setCanScroll(false);
    setShowScrollbar(false);
    setThumbTop(0);
  }, [
    visible,
    usableCoupons,
    unusableCoupons,
    expandUnusable,
    expandedCouponIds
  ]);
  useEffect(() => {
    if (show && !visible) {
      setVisible(true);
      getList(1);
      setExpandedCouponIds([]);
      getList();
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
        timingFunction: "ease-out"
      });
      animation.translateY(0).step();
      setAnimationData(animation.export());
    } else if (!show && visible) {
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-in'
        timingFunction: "ease-in"
      });
      animation.translateY('100%').step();
      animation.translateY("100%").step();
      setAnimationData(animation.export());
      setTimeout(() => {
        setVisible(false);
      }, 300);
    }
  }, [show]);
  }, [show, visible, orderMoney]);
  if (!visible && !show) return null;
  const hasUnavailableCoupons = unusableCoupons.length > 0;
  return (
    <View className={`coupon-popup ${show ? 'show' : ''}`} catchMove>
      <View className='coupon-mask' onClick={onClose}></View>
      <View className='coupon-popup-content' animation={animationData}>
        <View className='coupon-header'>
          <Text className='coupon-title'>选择优惠券</Text>
          <Image className='close-icon' src={closeIcon} onClick={onClose} />
    <View className={`coupon-popup ${show ? "show" : ""}`} catchMove>
      <View className="coupon-mask" onClick={onClose}></View>
      <View className="coupon-popup-content" animation={animationData}>
        <View className="coupon-header">
          <Text className="coupon-title">选择优惠券</Text>
          <Image className="close-icon" src={closeIcon} onClick={onClose} />
        </View>
        <View className='coupon-list-wrap'>
          <View className='coupon-list-head'>
            <View className='coupon-list-head-item'>
        <View className="coupon-list-wrap">
          <View className="coupon-list-head">
            <View className="coupon-list-head-item">
              平台优惠券({couponCount}张)
            </View>
            <View className='my-coupon-item' onClick={gotoMyCoupon}>
                <Text>我的优惠券</Text>
                <Image className='arrow-icon' src={couponArrowRight} />
            <View className="my-coupon-item" onClick={gotoMyCoupon}>
              <Text>我的优惠券</Text>
              <Image className="arrow-icon" src={couponArrowRight} />
            </View>
          </View>
          <ScrollView
            scrollY={canScroll}
            className='coupon-list'
            className="coupon-list"
            showScrollbar={false}
          >
            <View className='coupon-list-inner'>
              {list.map((item, index) => (
                <View
                  className='coupon-item'
                  key={item.couponId || index}
                  style={{ opacity: ![0, 1].includes(item.status) ? 0.5 : 1 }}
                  onClick={() => handleSelectCoupon(item)}
                >
                  <View className='coupon-content'>
                    <View className='coupon-price-box'>
                      <View className='coupon-price'>
                        <Text className='price'>{item.discountAmount}</Text>
                        <Text className='unit'>元</Text>
                      </View>
                      <View className='no-threshold'>
                        {formatThresholdText(item.conditionAmount)}
                      </View>
                    </View>
                    <View className='coupon-info-box'>
                      <View className='coupon-info'>
                        <Text className='name'>{item.title}</Text>
                        <Text className='time'>
                          {formatTime(item.expireTime, 'list')}到期
                        </Text>
                        <View
                          className='more'
                          onClick={(event) => {
                            event.stopPropagation();
                            openDetailPopup(item);
                          }}
                        >
                          <Text className='more-detail'>查看详情</Text>
                          <Image className='more-img' src={icon}></Image>
            <View className="coupon-list-inner">
              {usableCoupons.map((item, index) => (
                <View className="coupon-item-wrap" key={item.couponId || index}>
                  <View
                    className="coupon-item"
                    onClick={() => handleSelectCoupon(item)}
                  >
                    <View className="coupon-content">
                      <View className="coupon-price-box">
                        <View className="coupon-price">
                          <Text className="price">{item.discountAmount}</Text>
                          <Text className="unit">元</Text>
                        </View>
                        <View className="no-threshold">
                          {formatThresholdText(item.conditionAmount)}
                        </View>
                      </View>
                      {item.status === 1 && <View className='select-box'>
                        <Image
                          className='select-img'
                          src={activeCouponId === item.operationCouponId ? couponSelectedIcon : couponSelectIcon}
                        ></Image>
                      </View>}
                      {/* 状态 1-未使用 2-已使用 3-已过期 0待领取 */}
                      {item.status === 2 && (
                        <Image
                          className='status-img'
                          src={require("../../assets/coupon-used.png")}
                        />
                      ) }
                      { item.status === 3 && (
                        <Image
                          className='status-img'
                          src={require("../../assets/coupon-lost-efficacy.png")}
                        />
                      )}
                      <View className="coupon-info-box">
                        <View className="coupon-info">
                          <Text className="name">{item.title}</Text>
                          <Text className="time">
                            {formatTime(item.expireTime, "list")}到期
                          </Text>
                          <View
                            className="more"
                            onClick={event => {
                              event.stopPropagation();
                              const couponKey =
                                item?.operationCouponId || item?.couponId || "";
                              setExpandedCouponIds(prev =>
                                prev.includes(couponKey)
                                  ? prev.filter(id => id !== couponKey)
                                  : [...prev, couponKey]
                              );
                            }}
                          >
                            <Text className="more-detail">查看详情</Text>
                            <Image
                              className={`more-img ${
                                expandedCouponIds.includes(
                                  item?.operationCouponId ||
                                    item?.couponId ||
                                    ""
                                )
                                  ? "expanded"
                                  : ""
                              }`}
                              src={icon}
                            ></Image>
                          </View>
                        </View>
                        <View className="select-box">
                          <Image
                            className="select-img"
                            src={
                              activeCouponId === item.operationCouponId
                                ? couponSelectedIcon
                                : couponSelectIcon
                            }
                          ></Image>
                        </View>
                      </View>
                    </View>
                  </View>
                  {expandedCouponIds.includes(
                    item?.operationCouponId || item?.couponId || ""
                  ) && (
                    <View className="coupon-inline-detail">
                      {item.stackable && <Text className="coupon-inline-detail-text" >
                        不可叠加:与其他优惠券互斥,不可同时使用;
                      </Text>}
                      {item.effectAndInvalid && <Text className="coupon-inline-detail-text" >
                        生效失效:{item.effectAndInvalid}
                      </Text>}
                    </View>
                  )}
                </View>
              ))}
              {list.length === 0 && (
                <View className='empty-tip'>暂无优惠券</View>
              {hasUnavailableCoupons && (
                <View className="unavailable-section">
                  <View
                    className="unavailable-toggle"
                    onClick={() => setExpandUnusable(prev => !prev)}
                  >
                    <Text className="unavailable-toggle-text">
                      {expandUnusable
                        ? "收起"
                        : `${unusableCoupons.length}张不可用优惠券`}
                    </Text>
                    <Image
                      className={`unavailable-toggle-arrow ${
                        expandUnusable ? "expanded" : ""
                      }`}
                      src={couponArrowRight}
                    />
                  </View>
                  {expandUnusable && (
                    <View className="unavailable-list">
                      {unusableCoupons.map((item, index) => (
                        <View
                          className="coupon-item-wrap"
                          key={item.couponId || index}
                        >
                          <View
                            className="coupon-item"
                            style={{ opacity: 0.5 }}
                          >
                            <View className="coupon-content">
                              <View className="coupon-price-box">
                                <View className="coupon-price">
                                  <Text className="price">
                                    {item.discountAmount}
                                  </Text>
                                  <Text className="unit">元</Text>
                                </View>
                                <View className="no-threshold">
                                  {formatThresholdText(item.conditionAmount)}
                                </View>
                              </View>
                              <View className="coupon-info-box">
                                <View className="coupon-info">
                                  <Text className="name">{item.title}</Text>
                                  <Text className="time">
                                    {formatTime(item.expireTime, "list")}到期
                                  </Text>
                                  <View
                                    className="more"
                                    onClick={event => {
                                      event.stopPropagation();
                                      const couponKey =
                                        item?.operationCouponId ||
                                        item?.couponId ||
                                        "";
                                      setExpandedCouponIds(prev =>
                                        prev.includes(couponKey)
                                          ? prev.filter(id => id !== couponKey)
                                          : [...prev, couponKey]
                                      );
                                    }}
                                  >
                                    <Text className="more-detail">
                                      查看详情
                                    </Text>
                                    <Image
                                      className={`more-img ${
                                        expandedCouponIds.includes(
                                          item?.operationCouponId ||
                                            item?.couponId ||
                                            ""
                                        )
                                          ? "expanded"
                                          : ""
                                      }`}
                                      src={icon}
                                    ></Image>
                                  </View>
                                </View>
                                <View className="select-box">
                                  <Image
                                    className="select-img"
                                    src={couponSelectIcon}
                                  ></Image>
                                </View>
                              </View>
                            </View>
                          </View>
                          {expandedCouponIds.includes(
                            item?.operationCouponId || item?.couponId || ""
                          ) && (
                            <View className="coupon-inline-detail">
                              <Text className="coupon-inline-detail-text">
                                {resolveUnusableDetailText(item)}
                              </Text>
                            </View>
                          )}
                        </View>
                      ))}
                    </View>
                  )}
                </View>
              )}
              {!usableCoupons.length && !unusableCoupons.length && (
                <View className="empty-tip">暂无优惠券</View>
              )}
            </View>
          </ScrollView>
          {showScrollbar && (
            <View className='custom-scrollbar-track'>
              <View
                className='custom-scrollbar-thumb'
                style={{
                  height: `${thumbHeight}px`,
                  transform: `translateY(${thumbTop}px)`
                }}
              />
            </View>
          )}
        </View>
      </View>
      {detailVisible && currentCoupon && (
        <View className='coupon-detail-popup' catchMove>
          <View className='coupon-detail-mask' onClick={closeDetailPopup}></View>
          <View className='coupon-detail-content' animation={detailAnimationData}>
            <View className='coupon-detail-header'>
              <Text className='coupon-detail-title'>{currentCoupon.title}</Text>
              <Image
                className='coupon-detail-close'
                src={closeIcon}
                onClick={closeDetailPopup}
              />
            </View>
            <View className='coupon-detail-row'>
              <Text className='coupon-detail-label'>有效时间</Text>
              <Text className='coupon-detail-value'>
                {formatTime(currentCoupon.expireTime, 'popup')} 失效
              </Text>
            </View>
            <View className='coupon-detail-row'>
              <Text className='coupon-detail-label'>使用时段</Text>
              <Text className='coupon-detail-value'>全天</Text>
            </View>
            <View className='coupon-detail-row'>
              <Text className='coupon-detail-label'>使用地区</Text>
              <Text className='coupon-detail-value'>广州</Text>
            </View>
            <View className='coupon-detail-row'>
              <Text className='coupon-detail-label'>使用说明</Text>
              <RichText nodes={currentCoupon.couponDesc} />
            </View>
          </View>
        </View>
      )}
    </View>
  );
}
src/robotaxi/pages/coupon/index.tsx
@@ -11,7 +11,6 @@
  CouponItem,
  CouponDetail
} from "../../../robotaxi/api/coupon";
import CouponSelectPopup from "../../components/coupon-select-popup";
export default function CouponPage() {
  const [couponStatus] = useState([
@@ -282,7 +281,6 @@
          </View>
        </View>
      )}
      <CouponSelectPopup show onClose={() => {}} onSelect={() => {}} />
    </View>
  );
}