| src/pages/robotaxi/home/components/home-overlay-layer.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/robotaxi/home/hooks/use-coupon-message-ws.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/robotaxi/home/index.less | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/robotaxi/home/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/api/coupon.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/components/coupon-select-popup/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/coupon/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/share-trip/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | 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);