zjk
2026-04-25 4615f584499e4f5eccacb5d8cf21f1889dac10c4
feat(home): 新增活动悬浮窗并优化营销弹窗数据展示

- 新增活动悬浮窗组件,支持从后端获取显示状态、图片及跳转链接
- 营销弹窗数据改为从接口动态获取,支持多张优惠券展示
- 优化营销弹窗样式,新增优惠券数量显示和门槛条件格式化
- 调整弹窗层级,避免与其他组件重叠
1个文件已添加
5个文件已修改
259 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/marketing-popup/index.less 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/marketing-popup/index.tsx 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.less 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/home.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coverImage.png 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/marketing-popup/index.less
@@ -4,7 +4,7 @@
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10001;
  z-index: 100001;
  display: flex;
  justify-content: center;
  pointer-events: auto;
@@ -29,7 +29,7 @@
  left: -46px;
  width: 824px;
  height: 876px;
  z-index: 10002;
  z-index: 100002;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 0.26s ease, transform 0.26s ease;
@@ -44,7 +44,7 @@
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10011;
  z-index: 1000011;
}
.marketing-popup-close-single {
@@ -55,7 +55,7 @@
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10011;
  z-index: 1000011;
}
.marketing-popup-close-icon {
@@ -69,9 +69,9 @@
  left: 0;
  width: 560px;
  height: 658px;
  z-index: 10002;
  background: url(../../../../../robotaxi/assets/marketing-manyBg.png)
    no-repeat center center;
  z-index: 100002;
  background: url(../../../../../robotaxi/assets/marketing-manyBg.png) no-repeat
    center center;
  background-size: 100% 100%;
}
@@ -83,11 +83,15 @@
  background: url(../../../../../robotaxi/assets/marketing-coupon-bg.png)
    no-repeat center center;
  background-size: 100% 100%;
  position: relative;
  .marketing-popup-item-left {
    width: 158px;
    box-sizing: border-box;
    height: 100%;
    padding: 20px 0 0 38px;
    padding-top: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    .marketing-popup-item-price {
      display: flex;
      .marketing-popup-item-amount {
@@ -165,11 +169,31 @@
      margin-top: 12px;
    }
  }
  .marketing-popup-item-quantity {
    position: absolute;
    top: 0;
    right: 0;
    width: 52px;
    height: 34px;
    background: url(../../../../../robotaxi/assets/marketing-coupon-topBg.png)
      no-repeat center center;
    background-size: 100% 100%;
    font-family: PingFang SC;
    font-size: 28px;
    font-weight: 500;
    line-height: 28px;
    text-align: center;
    letter-spacing: normal;
    color: #ffffff;
    display: flex;
    justify-content: center;
    align-items: center;
  }
}
.marketing-popup-multi-panel {
  margin-top: 408px;
  z-index: 10010;
  z-index: 100010;
  width: 560px;
  height: auto;
  position: relative;
@@ -182,7 +206,7 @@
  .marketing-popup-panel {
    position: relative;
    z-index: 10010;
    z-index: 100010;
  }
  .marketing-popup-title {
@@ -235,7 +259,7 @@
.marketing-popup-single-panel {
  margin-top: 388px;
  z-index: 10010;
  z-index: 100010;
  width: 540px;
  height: auto;
  background: url(../../../../../robotaxi/assets/marketing-singleBg.png)
@@ -259,7 +283,7 @@
    .marketing-popup-coupon-title-top-icon {
      width: 51px;
      height: 3px ;
      height: 3px;
    }
  }
  .marketing-popup-coupon-title {
src/pages/robotaxi/home/components/marketing-popup/index.tsx
@@ -10,6 +10,7 @@
type MarketingPopupProps = {
    show: boolean;
    data: any;
    onClose: () => void;
};
@@ -19,51 +20,9 @@
    unit: string;
    title: string;
    expireText: string;
    tagText: string;
    conditionalAmount: number;
    quantity: 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;
@@ -92,8 +51,22 @@
        };
    }, [props.show]);
    const isSingleCoupon = MARKETING_COUPONS.length === 1;
    const isTwoCoupons = MARKETING_COUPONS.length === 2;
    const sourceCoupons: MarketingCoupon[] = Array.isArray(props.data?.coupons)
      ? props.data.coupons.map((item: any, index: number) => ({
          id: String(item?.id ?? item?.couponId ?? index),
          amount: String(item?.amount ?? item?.discountAmount ?? '0'),
          unit: String(item?.unit ?? (Number(item?.couponType) === 1 ? '折' : '元')),
          title: String(item?.title ?? item?.couponName ?? '--'),
          expireText: item?.endTime ?? '--',
          quantity: String(item?.quantity ?? item?.count ?? '--'),
          conditionalAmount: Number(item?.conditionalAmount ?? 0)
        }))
      : [];
    const popupTitle = String(props.data?.miniProgramPopupTitle || '恭喜获得新人券');
    const popupBtnText = String(props.data?.miniProgramBtnText || '立即领取');
    const isSingleCoupon = sourceCoupons.length === 1;
    const isTwoCoupons = sourceCoupons.length === 2;
    const preventTouchEvent = (event: any) => {
        if (event?.stopPropagation) {
            event.stopPropagation();
@@ -101,6 +74,26 @@
        if (event?.preventDefault) {
            event.preventDefault();
        }
    };
     const formatTime = (t?: string, type?: string) => {
      const d = new Date(t || '');
      const y = d.getFullYear();
      const m = String(d.getMonth() + 1).padStart(2, "0");
      const day = String(d.getDate()).padStart(2, "0");
      const h = String(d.getHours()).padStart(2, "0");
      const min = String(d.getMinutes()).padStart(2, "0");
      return type == "list"
        ? `${y}.${m}.${day} ${h}:${min}`
        : `${y}-${m}-${day}`;
    };
    const formatThresholdText = (conditionalAmount?: number) => {
      const amount = Number(conditionalAmount || 0);
      if (amount > 0) {
        return `满 ${amount} 可用`;
      }
      return "无门槛";
    };
    const renderCouponItem = (coupon: MarketingCoupon) => {
@@ -111,33 +104,35 @@
                        <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 className='marketing-popup-item-tag'>{formatThresholdText(coupon.conditionalAmount)}</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 className='marketing-popup-item-expire'>{formatTime(coupon.expireText, "list")}到期</View>
                </View>
                <View className='marketing-popup-item-quantity'>x{coupon.quantity}</View>
            </View>
        );
    };
    const renderSingleCouponModule = () => {
        const coupon = sourceCoupons[0];
        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>
                  <View style={{ margin: '0 14px' }}>恭喜获得{Number(coupon.quantity) > 0 ? coupon.quantity + '张' : ''}</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>
                  <View style={{ margin: '0 8px' }}>{coupon?.title || '媒体尝鲜券'}</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-coupon-tag-text'>{formatThresholdText(coupon.conditionalAmount)}</View>
                <View className='marketing-popup-coupon-expire-text'>{formatTime(coupon.expireText, "list")}到期</View>
                <View className='marketing-popup-action' onClick={props.onClose}>
                    立即领取
                    {popupBtnText}
                </View>
                <View className='marketing-popup-close-single' onClick={props.onClose}>
                    <Image className='marketing-popup-close-icon' src={closeIcon} />
@@ -156,7 +151,7 @@
                  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-title'>{popupTitle}</View>
                    <View className='marketing-popup-panel-content'>
                        <ScrollView
                          className='marketing-popup-list'
@@ -164,12 +159,12 @@
                          enhanced
                          showScrollbar={false}
                        >
                            {MARKETING_COUPONS.map(renderCouponItem)}
                            {sourceCoupons.map(renderCouponItem)}
                            <View style={{ height: '1px' }} />
                        </ScrollView>
                    </View>
                    <View className='marketing-popup-action' onClick={props.onClose}>
                        立即领取
                        {popupBtnText}
                    </View>
                </View>
            </View>
src/pages/robotaxi/home/index.less
@@ -189,6 +189,23 @@
  }
}
.activity-float-window {
  position: fixed;
  right: 6px;
  bottom: 234px;
  z-index: 10020;
  width: 96px;
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
  .activity-float-window-img {
    width: 96px;
    height: 96px;
  }
}
.pointer-box {
  position: absolute;
  top: 50%;
src/pages/robotaxi/home/index.tsx
@@ -12,6 +12,7 @@
import personalCenter from '../../../robotaxi/assets/personal-center.png'
import coupon from '../../../robotaxi/assets/coupon.png'
import contactService from '../../../robotaxi/assets/contact-service.png'
import floatDefaultImg from '../../../robotaxi/assets/coverImage.png'
import { getWeatherIconByDesc } from '../../../utils/weather';
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import pointer from '../../../robotaxi/assets/pointer.png'
@@ -58,34 +59,16 @@
  getAgreement,
  getOrderActive,
  getHotFixInfo,
  firstEnter
  firstEnter,
  getFloatActicity
} from '../../../robotaxi/api/home';
import { getServicePhone } from '../../../robotaxi/api/customer-service';
import { getOrderDistance } from '../../../robotaxi/api/select-map-site';
import { checkOtp } from '../../../utils/tools';
import { checkOtp, toH5 } from '../../../utils/tools';
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;
@@ -103,6 +86,10 @@
  };
  const [showMarketing, setShowMarketing] = useState(false); // 营销弹窗是否显示
  const [marketingData, setMarketingData] = useState({}); // 营销弹窗数据
  const [showFloatActivity, setShowFloatActivity] = useState(false); // 活动悬浮窗是否显示
  const [floatActivityUrl, setFloatActivityUrl] = useState(''); // 活动悬浮窗跳转地址
  const [floatActivityImg, setFloatActivityImg] = useState(floatDefaultImg); // 活动悬浮窗图片
  const [showPrivacy, setShowPrivacy] = useState(false); // 隐私政策弹窗是否显示
  const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示
  const mapState = useMapState(); // 地图状态容器
@@ -415,18 +402,40 @@
      .then((hasLogin) => {
        if (!hasLogin) {
          setShowMarketing(false);
          setShowFloatActivity(false);
          return;
        }
        return firstEnter()
          .then((res: any) => {
            setShowMarketing(resolveMarketingVisible(res));
        return Promise.all([firstEnter(), getFloatActicity()])
          .then(([firstEnterRes, floatRes]: any[]) => {
            // 兼容 firstEnter 接口返回结构:可能是 res.data 或直接为业务对象
            const firstEnterData = firstEnterRes?.data ?? firstEnterRes ?? {};
            const show = Boolean(firstEnterData?.isFirst);
            setShowMarketing(show);
            setMarketingData(firstEnterData);
            const floatData = floatRes?.data ?? floatRes ?? {};
            const isFloatActivity = Boolean(
              floatData?.enableFloatWindow === 1
            );
            setShowFloatActivity(isFloatActivity);
            setFloatActivityUrl(
              String(
                floatData?.floatWindowJumpUrl ||
                ''
              )
            );
            const img = String(floatData?.coverImage || '');
            setFloatActivityImg(img || floatDefaultImg);
          })
          .catch(() => {
            setShowMarketing(false);
            setShowFloatActivity(false);
            setFloatActivityUrl('');
          });
      })
      .catch(() => {
        setShowMarketing(false);
        setShowFloatActivity(false);
        setFloatActivityUrl('');
      });
    if (typeof latitude === 'number' && typeof longitude === 'number') {
      tryStartPollingOnInit({
@@ -506,6 +515,15 @@
  const handleCloseCustomerService = () => {
    setShowCustomerService(false);
  };
  const handleFloatActivityClick = () => {
    if (!floatActivityUrl) {
      Taro.showToast({ title: '活动页面建设中', icon: 'none' });
      return;
    }
    // 预留 H5 出口,后续可按活动类型扩展埋点和鉴权逻辑
    toH5(floatActivityUrl, true);
  };
  // 初始化浮层锚点:固定在 content-box 顶部上方 32px
@@ -759,7 +777,18 @@
        show={showCustomerService}
        onClose={handleCloseCustomerService}
      />
      <MarketingPopup show={showMarketing} onClose={() => setShowMarketing(false)} />
      {/* 营销活动弹窗 */}
      <MarketingPopup show={showMarketing} data={marketingData} onClose={() => setShowMarketing(false)} />
      {/* 悬浮窗 */}
      {showFloatActivity && (
        <View className='activity-float-window' onClick={handleFloatActivityClick}>
          <Image
            className='activity-float-window-img'
            src={floatActivityImg || floatDefaultImg}
            onError={() => setFloatActivityImg(floatDefaultImg)}
          />
        </View>
       )}
    </View>
  )
}
src/robotaxi/api/home.ts
@@ -118,3 +118,11 @@
        method: 'GET'
    }, true);
}
// 获取是否显示悬浮窗
export const getFloatActicity = async () => {
    return request({
        url: _ROBOTAXI_HOST + '/operation/api/v1/mini-program/acticity/getFloatActicity',
        method: 'GET'
    }, true);
}
src/robotaxi/assets/coverImage.png