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> ); }