zjk
2026-05-20 7aa7351122d3f349d99062588349f3279b4c1b0b
feat(robotaxi): 新增实时优惠券消息推送并优化优惠券展示逻辑

修复拼车页面白名单校验的逻辑错误,新增优惠券WebSocket监听与弹窗提示,补充couponType字段适配折扣券展示,优化优惠券相关页面的金额显示逻辑,调整优惠券选择弹窗的数据映射。
1个文件已添加
7个文件已修改
259 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/home-overlay-layer.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-coupon-message-ws.ts 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.less 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/coupon.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/coupon-select-popup/index.tsx 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/coupon/index.tsx 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/home-overlay-layer.tsx
@@ -38,6 +38,10 @@
  onFloatActivityClick: () => void;
  /** 悬浮窗图片兜底。 */
  onFloatActivityError: () => void;
  /** 是否展示优惠券消息通知。 */
  showCouponMessage: boolean;
  /** 优惠券消息通知文案。 */
  couponMessageContent: string;
};
/**
@@ -61,6 +65,11 @@
          </View>
        </View>
      )}
      {props.showCouponMessage && props.couponMessageContent && (
        <View className='coupon-message-toast'>
          {props.couponMessageContent}
        </View>
      )}
      <PrivacyPopup
        show={props.showPrivacy}
        onClose={props.onClosePrivacy}
src/pages/robotaxi/home/hooks/use-coupon-message-ws.ts
New file
@@ -0,0 +1,107 @@
import { useEffect, useRef, useState } from 'react';
import wsManager from '../../../../utils/websocket';
type CouponMessagePayload = {
  content?: any;
};
type Deps = {
  /** 登录校验。 */
  checkOtp: (silent?: boolean) => Promise<unknown>;
};
/**
 * 首页优惠券消息 WebSocket 监听。
 */
export default function useCouponMessageWs(deps: Deps) {
  const [couponMessageContent, setCouponMessageContent] = useState(''); // 当前展示的消息文案
  const [showCouponMessage, setShowCouponMessage] = useState(false); // 是否展示消息提示
  const couponMessageTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 自动关闭定时器
  const couponMessageBoundRef = useRef(false); // 当前页面是否已绑定消息监听
  const { checkOtp } = deps;
  const clearCouponMessageTimer = () => {
    if (couponMessageTimerRef.current) {
      clearTimeout(couponMessageTimerRef.current);
      couponMessageTimerRef.current = null;
    }
  };
  const resolveCouponMessageText = (payload?: CouponMessagePayload) => {
    const rawContent = payload?.content;
    if (typeof rawContent === 'string') {
      try {
        const parsedContent = JSON.parse(rawContent);
        if (parsedContent && typeof parsedContent?.content === 'string') {
          return parsedContent.content;
        }
      } catch (error) {
      }
      return rawContent;
    }
    if (rawContent && typeof rawContent?.content === 'string') {
      return rawContent.content;
    }
    return '';
  };
  useEffect(() => {
    const couponMessageHandler = (payload?: CouponMessagePayload) => {
      const nextContent = resolveCouponMessageText(payload).trim();
      if (!nextContent) {
        return;
      }
      setCouponMessageContent(nextContent);
      setShowCouponMessage(true);
      clearCouponMessageTimer();
      // 新消息到达时重置倒计时,保证每条消息都完整展示 3 秒
      couponMessageTimerRef.current = setTimeout(() => {
        setShowCouponMessage(false);
        couponMessageTimerRef.current = null;
      }, 3000);
    };
    let openHandler: (() => void) | null = null;
    const bindCouponMessage = () => {
      if (couponMessageBoundRef.current) {
        return;
      }
      wsManager.on('COUPON_MESSAGE', couponMessageHandler);
      couponMessageBoundRef.current = true;
    };
    void checkOtp(false).then((otp) => {
      if (!otp) {
        return;
      }
      if (wsManager.isConnectedStatus()) {
        bindCouponMessage();
        return;
      }
      openHandler = () => {
        bindCouponMessage();
        if (openHandler) {
          wsManager.off('open', openHandler);
        }
      };
      wsManager.on('open', openHandler);
    });
    return () => {
      clearCouponMessageTimer();
      if (openHandler) {
        wsManager.off('open', openHandler);
      }
      if (couponMessageBoundRef.current) {
        wsManager.off('COUPON_MESSAGE', couponMessageHandler);
        couponMessageBoundRef.current = false;
      }
    };
  }, [checkOtp]);
  return {
    showCouponMessage,
    couponMessageContent
  };
}
src/pages/robotaxi/home/index.less
@@ -206,6 +206,37 @@
  }
}
.coupon-message-toast {
  position: fixed;
  top: 403px;
  right: 144px;
  z-index: 10018;
  max-width: 560px;
  min-height: 64px;
  padding: 12px 24px;
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.7);
  color: #FFFFFF;
  font-size: 24px;
  line-height: 40px;
  text-align: center;
  box-sizing: border-box;
  word-break: break-all;
  &::after {
    content: '';
    position: absolute;
    top: 50%;
    right: -12px;
    transform: translateY(-50%);
    width: 0;
    height: 0;
    border-top: 10px solid transparent;
    border-bottom: 10px solid transparent;
    border-left: 12px solid rgba(0, 0, 0, 0.7);
  }
}
.pointer-box {
  position: absolute;
  top: 50%;
@@ -707,4 +738,4 @@
    background-clip: text;
    text-fill-color: transparent;
  }
}
}
src/pages/robotaxi/home/index.tsx
@@ -19,6 +19,7 @@
import useNearbyVehicles from './hooks/use-nearby-vehicles';
import useHotfixCheck from './hooks/use-hotfix-check';
import useOrderWs from './hooks/use-order-ws';
import useCouponMessageWs from './hooks/use-coupon-message-ws';
import useWeather from './hooks/use-weather';
import useMarketing from './hooks/use-marketing';
import useFenceManager from './hooks/use-fence-manager';
@@ -226,6 +227,7 @@
  });
  useOrderWs({ checkOtp, setTripStatus });
  const { showCouponMessage, couponMessageContent } = useCouponMessageWs({ checkOtp });
  const {
    vehicleMarkers,
@@ -491,6 +493,8 @@
        floatActivityImg={floatActivityImg}
        onFloatActivityClick={handleFloatActivityClick}
        onFloatActivityError={handleFloatActivityError}
        showCouponMessage={showCouponMessage}
        couponMessageContent={couponMessageContent}
      />
    </View>
  );
src/robotaxi/api/coupon.ts
@@ -4,6 +4,7 @@
export interface CouponItem {
    couponId: string;
    title: string;
    couponType?: number;
    discountAmount: number;
    conditionAmount?: number;
    expireTime: string;
src/robotaxi/components/coupon-select-popup/index.tsx
@@ -19,9 +19,10 @@
const toPopupCoupon = (item: any, status: 1 | 3) => {
  return {
    couponId: String(item?.id ?? ""),
    id: String(item?.id ?? ""),
    operationCouponId: String(item?.id ?? ""),
    title: item?.couponName || "",
    couponType: Number(item?.couponType || 0),
    discountAmount: Number(item?.amount || 0),
    conditionAmount: Number(item?.conditionalAmount || 0),
    expireTime: item?.endTime || "",
@@ -32,7 +33,9 @@
      item?.notUsableReason ||
      item?.notUseReason ||
      item?.reason ||
      ""
      "",
    stackable: item.stackable,
    effectAndInvalid: item.effectAndInvalid
  };
};
@@ -104,6 +107,19 @@
      return `满 ${amount} 可用`;
    }
    return "无门槛";
  };
  const formatCouponAmount = (item: any) => {
    const amount = Number(item?.discountAmount || 0);
    // 折扣券接口返回 0.6 表示 6 折,这里转成用户可读文案
    if (Number(item?.couponType) === 1) {
      return `${amount * 10}`.replace(/\.0$/, "");
    }
    return `${amount}`.replace(/\.0$/, "");
  };
  const formatCouponUnit = (item: any) => {
    return Number(item?.couponType) === 1 ? "折" : "元";
  };
  const formatTime = (t?: string, type?: string) => {
@@ -224,8 +240,10 @@
                    <View className="coupon-content">
                      <View className="coupon-price-box">
                        <View className="coupon-price">
                          <Text className="price">{item.discountAmount}</Text>
                          <Text className="unit">元</Text>
                          <Text className="price">
                            {formatCouponAmount(item)}
                          </Text>
                          <Text className="unit">{formatCouponUnit(item)}</Text>
                        </View>
                        <View className="no-threshold">
                          {formatThresholdText(item.conditionAmount)}
@@ -241,23 +259,17 @@
                            className="more"
                            onClick={event => {
                              event.stopPropagation();
                              const couponKey =
                                item?.operationCouponId || item?.couponId || "";
                              setExpandedCouponIds(prev =>
                                prev.includes(couponKey)
                                  ? prev.filter(id => id !== couponKey)
                                  : [...prev, couponKey]
                                prev.includes(item.id)
                                  ? prev.filter(id => id !== item.id)
                                  : [...prev, item.id]
                              );
                            }}
                          >
                            <Text className="more-detail">查看详情</Text>
                            <Image
                              className={`more-img ${
                                expandedCouponIds.includes(
                                  item?.operationCouponId ||
                                    item?.couponId ||
                                    ""
                                )
                                expandedCouponIds.includes(item.id)
                                  ? "expanded"
                                  : ""
                              }`}
@@ -278,16 +290,22 @@
                      </View>
                    </View>
                  </View>
                  {expandedCouponIds.includes(
                    item?.operationCouponId || item?.couponId || ""
                  ) && (
                  {expandedCouponIds.includes(item.id) && (
                    <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>
                        {item.stackable && (
                          <Text className="coupon-inline-detail-text">
                            {item.stackable}
                          </Text>
                        )}
                      </View>
                      <View>
                        {item.effectAndInvalid && (
                          <Text className="coupon-inline-detail-text">
                            {item.effectAndInvalid}
                          </Text>
                        )}
                      </View>
                    </View>
                  )}
                </View>
@@ -326,9 +344,11 @@
                              <View className="coupon-price-box">
                                <View className="coupon-price">
                                  <Text className="price">
                                    {item.discountAmount}
                                    {formatCouponAmount(item)}
                                  </Text>
                                  <Text className="unit">元</Text>
                                  <Text className="unit">
                                    {formatCouponUnit(item)}
                                  </Text>
                                </View>
                                <View className="no-threshold">
                                  {formatThresholdText(item.conditionAmount)}
@@ -344,14 +364,10 @@
                                    className="more"
                                    onClick={event => {
                                      event.stopPropagation();
                                      const couponKey =
                                        item?.operationCouponId ||
                                        item?.couponId ||
                                        "";
                                      setExpandedCouponIds(prev =>
                                        prev.includes(couponKey)
                                          ? prev.filter(id => id !== couponKey)
                                          : [...prev, couponKey]
                                        prev.includes(item.id)
                                          ? prev.filter(id => id !== item.id)
                                          : [...prev, item.id]
                                      );
                                    }}
                                  >
@@ -360,11 +376,7 @@
                                    </Text>
                                    <Image
                                      className={`more-img ${
                                        expandedCouponIds.includes(
                                          item?.operationCouponId ||
                                            item?.couponId ||
                                            ""
                                        )
                                        expandedCouponIds.includes(item.id)
                                          ? "expanded"
                                          : ""
                                      }`}
src/robotaxi/pages/coupon/index.tsx
@@ -75,6 +75,19 @@
    return "无门槛";
  };
  const formatCouponAmount = (item: CouponItem) => {
    const amount = Number(item?.discountAmount || 0);
    // 折扣券接口返回 0.6 表示 6 折,这里转成用户可读文案
    if (Number(item?.couponType) === 1) {
      return `${amount * 10}`.replace(/\.0$/, "");
    }
    return `${amount}`.replace(/\.0$/, "");
  };
  const formatCouponUnit = (item: CouponItem) => {
    return Number(item?.couponType) === 1 ? "折" : "元";
  };
  useEffect(() => {
    // 切换 tab 时重置并加载第一页
    setPageNum(1);
@@ -183,8 +196,8 @@
                <View className="coupon-content">
                  <View className="coupon-price-box">
                    <View className="coupon-price">
                      <Text className="price">{item.discountAmount}</Text>
                      <Text className="unit">元</Text>
                      <Text className="price">{formatCouponAmount(item)}</Text>
                      <Text className="unit">{formatCouponUnit(item)}</Text>
                    </View>
                    <View className="no-threshold">
                      {formatThresholdText(item.conditionAmount)}
src/robotaxi/pages/share-trip/index.tsx
@@ -50,7 +50,7 @@
      // 登录成功后校验白名单,控制左上角 home 返回落点
      void isWhiteList()
        .then((res: boolean) => {
          setIsRobotaxiWhiteList(!res);
          setIsRobotaxiWhiteList(res);
        })
        .catch(() => {
          setIsRobotaxiWhiteList(false);