@@ -54,3 +54,71 @@ body::-webkit-scrollbar,
|
||||
padding-bottom: 0 !important;
|
||||
bottom: 0 !important;
|
||||
}
|
||||
|
||||
.benefit-figure {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.benefit-figure-prefix,
|
||||
.benefit-figure-value {
|
||||
line-height: 1;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.benefit-figure-icon {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.benefit-figure--sm {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.benefit-figure--sm .benefit-figure-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.benefit-figure--md .benefit-figure-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.benefit-figure--lg .benefit-figure-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.benefit-figure--xl {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.benefit-figure--xl .benefit-figure-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.mu-float-toast {
|
||||
position: fixed;
|
||||
top: 28%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
z-index: 10020;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.78);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,27 @@
|
||||
import { Image, Text, View } from '@tarojs/components';
|
||||
import iconStoreBenefit from '../assets/icons/store-benefit.png';
|
||||
|
||||
type BenefitFigureSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
type BenefitFigureProps = {
|
||||
value: string;
|
||||
size?: BenefitFigureSize;
|
||||
prefix?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** 好客权益金额:门店核销图标 + 数字,替代人民币符号 */
|
||||
export default function BenefitFigure({
|
||||
value,
|
||||
size = 'md',
|
||||
prefix = '',
|
||||
className = '',
|
||||
}: BenefitFigureProps) {
|
||||
return (
|
||||
<View className={`benefit-figure benefit-figure--${size} ${className}`.trim()}>
|
||||
{prefix ? <Text className="benefit-figure-prefix">{prefix}</Text> : null}
|
||||
<Image className="benefit-figure-icon" src={iconStoreBenefit} mode="aspectFit" />
|
||||
<Text className="benefit-figure-value">{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Text } from '@tarojs/components';
|
||||
import { Text, View } from '@tarojs/components';
|
||||
import BenefitFigure from './BenefitFigure';
|
||||
|
||||
type CouponBadgeProps = {
|
||||
amount: number | string;
|
||||
@@ -10,8 +11,9 @@ export default function CouponBadge({ amount, label = '好客权益' }: CouponBa
|
||||
const n = Number(amount);
|
||||
const display = Number.isFinite(n) ? (Number.isInteger(n) ? String(n) : n.toFixed(0)) : String(amount);
|
||||
return (
|
||||
<Text className="coupon-badge">
|
||||
享 ¥{display} {label}
|
||||
</Text>
|
||||
<View className="coupon-badge">
|
||||
<Text>享</Text>
|
||||
<BenefitFigure value={`${display} ${label}`} size="sm" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CoverView, View } from '@tarojs/components';
|
||||
import { registerFloatingToastListener } from '../lib/floating-toast';
|
||||
|
||||
const TOAST_MS = 2600;
|
||||
|
||||
export default function FloatingToastHost() {
|
||||
const [text, setText] = useState('');
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return registerFloatingToastListener((message) => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setText(message);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setText('');
|
||||
timerRef.current = null;
|
||||
}, TOAST_MS);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!text) return null;
|
||||
|
||||
const Box = process.env.TARO_ENV === 'weapp' ? CoverView : View;
|
||||
return <Box className="mu-float-toast">{text}</Box>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { View } from '@tarojs/components';
|
||||
import FloatingToastHost from './FloatingToastHost';
|
||||
import { pageShellCssVars, useNavBarMetrics } from '../lib/nav-bar';
|
||||
|
||||
type PageShellVariant = 'tab' | 'scroll' | 'sub' | 'plain';
|
||||
@@ -35,6 +36,7 @@ export default function PageShell({
|
||||
return (
|
||||
<View className={classes} style={pageShellCssVars(metrics)}>
|
||||
{children as ReactNode}
|
||||
<FloatingToastHost />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ type ProductCarouselProps = {
|
||||
* 依赖 swiper 原生 auto-height:海报有多高,轮播就有多高,无裁切。
|
||||
*/
|
||||
imageFit?: 'cover' | 'contain' | 'adaptive';
|
||||
/** 预览相册(默认等于 images);门店详情可传入封面+环境图合并列表 */
|
||||
previewUrls?: string[];
|
||||
};
|
||||
|
||||
/** 商品/门店轮播(对齐 h5 ProductCarousel 三变体) */
|
||||
@@ -23,6 +25,7 @@ export default function ProductCarousel({
|
||||
variant = 'detail',
|
||||
previewable = false,
|
||||
imageFit = 'cover',
|
||||
previewUrls,
|
||||
}: ProductCarouselProps) {
|
||||
const slides = images.length > 0 ? images : [''];
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
@@ -33,10 +36,10 @@ export default function ProductCarousel({
|
||||
const wrapClass = `${prefix}-wrap${isContain ? ` ${prefix}-wrap--contain` : ''}${isAdaptive ? ` ${prefix}-wrap--adaptive` : ''}`;
|
||||
|
||||
function previewAt(index: number) {
|
||||
const urls = slides.filter(Boolean);
|
||||
if (!urls.length) return;
|
||||
const current = slides[index] || urls[0];
|
||||
Taro.previewImage({ current, urls }).catch(() => undefined);
|
||||
const album = (previewUrls?.length ? previewUrls : slides).filter(Boolean);
|
||||
if (!album.length) return;
|
||||
const current = slides[index] || album[0];
|
||||
Taro.previewImage({ current, urls: album }).catch(() => undefined);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { forceReloadAfterAccountMerge } from './auth-nav';
|
||||
import { resetStoresSessionBootstrap } from './stores-session';
|
||||
import { resetHomeCatalogBootstrap } from './home-catalog-session';
|
||||
import { reportClientValidationError } from './client-error';
|
||||
import { showFloatingToast } from './floating-toast';
|
||||
|
||||
function resolveApiBase(): string {
|
||||
const origin =
|
||||
@@ -126,7 +127,13 @@ export async function request<T = unknown>(path: string, options: ReqOptions = {
|
||||
}
|
||||
|
||||
export function toast(title: string, icon: 'success' | 'error' | 'none' = 'none') {
|
||||
Taro.showToast({ title, icon, duration: 1800 });
|
||||
const text = title.trim();
|
||||
if (!text) return;
|
||||
if (icon !== 'success' && showFloatingToast(text)) {
|
||||
void Taro.hideToast();
|
||||
return;
|
||||
}
|
||||
Taro.showToast({ title: text, icon, duration: 1800 });
|
||||
}
|
||||
|
||||
export type SessionPayload = {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
type FloatingToastListener = (message: string) => void;
|
||||
|
||||
const listeners = new Set<FloatingToastListener>();
|
||||
|
||||
export function registerFloatingToastListener(fn: FloatingToastListener) {
|
||||
listeners.add(fn);
|
||||
return () => {
|
||||
listeners.delete(fn);
|
||||
};
|
||||
}
|
||||
|
||||
/** 已有页面宿主时返回 true,否则调用方应回退到原生 toast */
|
||||
export function showFloatingToast(message: string): boolean {
|
||||
const text = message.trim();
|
||||
if (!text || listeners.size === 0) return false;
|
||||
listeners.forEach((fn) => fn(text));
|
||||
return true;
|
||||
}
|
||||
@@ -1,7 +1,26 @@
|
||||
/** 金额展示(不依赖 Intl / toLocaleString,兼容微信小程序) */
|
||||
export function toMoneyNumber(amount: unknown): number {
|
||||
if (typeof amount === 'number') return Number.isFinite(amount) ? amount : 0;
|
||||
if (typeof amount === 'string' && amount.trim()) {
|
||||
const n = Number(amount);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
if (amount && typeof amount === 'object') {
|
||||
const o = amount as { toNumber?: () => number; toString?: () => string };
|
||||
if (typeof o.toNumber === 'function') {
|
||||
const n = Number(o.toNumber());
|
||||
if (Number.isFinite(n)) return n;
|
||||
}
|
||||
if (typeof o.toString === 'function' && o.toString !== Object.prototype.toString) {
|
||||
const n = Number(o.toString());
|
||||
if (Number.isFinite(n)) return n;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function formatMoney(amount: number | string): string {
|
||||
const n = typeof amount === 'number' ? amount : Number(amount);
|
||||
if (!Number.isFinite(n)) return '0.00';
|
||||
const n = toMoneyNumber(amount);
|
||||
const fixed = n.toFixed(2);
|
||||
const [intPart, dec] = fixed.split('.');
|
||||
const withComma = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
@@ -82,8 +82,10 @@ function sceneConfig(scene?: ShareScene): MiniShareSceneConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装页面分享:场景配置优先;空字段用 dynamic → 默认分享。
|
||||
* orderDetail 标题支持 {productName}。
|
||||
* 组装页面分享。
|
||||
* - productDetail / storeDetail:title 与 imageUrl 固定用业务字段(不被 HQ 场景配置覆盖)
|
||||
* - 其它场景:场景配置优先;空字段用 dynamic → 默认分享
|
||||
* - orderDetail 标题支持 {productName}
|
||||
*/
|
||||
export function buildSceneSharePayload(
|
||||
scene: ShareScene,
|
||||
@@ -98,23 +100,33 @@ export function buildSceneSharePayload(
|
||||
): PageSharePayload {
|
||||
const def = getShareRuntimeSync().default;
|
||||
const sc = sceneConfig(scene);
|
||||
let title = (sc.title || '').trim();
|
||||
if (title && options?.titleVars) {
|
||||
const vars = options.titleVars;
|
||||
const missingRequired = Object.entries(vars).some(
|
||||
([key, value]) => title.includes(`{${key}}`) && !(value != null && String(value).trim()),
|
||||
);
|
||||
title = missingRequired ? '' : applyShareTitleTemplate(title, vars);
|
||||
}
|
||||
if (!title) {
|
||||
const preferEntity = scene === 'productDetail' || scene === 'storeDetail';
|
||||
|
||||
let title: string;
|
||||
if (preferEntity) {
|
||||
title = (options?.dynamicTitle || '').trim() || def.title;
|
||||
} else {
|
||||
title = (sc.title || '').trim();
|
||||
if (title && options?.titleVars) {
|
||||
const vars = options.titleVars;
|
||||
const missingRequired = Object.entries(vars).some(
|
||||
([key, value]) => title.includes(`{${key}}`) && !(value != null && String(value).trim()),
|
||||
);
|
||||
title = missingRequired ? '' : applyShareTitleTemplate(title, vars);
|
||||
}
|
||||
if (!title) {
|
||||
title = (options?.dynamicTitle || '').trim() || def.title;
|
||||
}
|
||||
}
|
||||
|
||||
const desc = (sc.desc || '').trim() || (options?.dynamicDesc || '').trim() || def.desc;
|
||||
const imgUrl =
|
||||
(sc.imageUrl || '').trim() ||
|
||||
(options?.dynamicImageUrl || '').trim() ||
|
||||
def.imageUrl ||
|
||||
getDefaultShareImageUrl();
|
||||
const imgUrl = preferEntity
|
||||
? (options?.dynamicImageUrl || '').trim() || def.imageUrl || getDefaultShareImageUrl()
|
||||
: (sc.imageUrl || '').trim() ||
|
||||
(options?.dynamicImageUrl || '').trim() ||
|
||||
def.imageUrl ||
|
||||
getDefaultShareImageUrl();
|
||||
|
||||
return {
|
||||
title,
|
||||
desc,
|
||||
|
||||
@@ -97,23 +97,33 @@ export function buildSceneSharePayload(
|
||||
): PageSharePayload {
|
||||
const def = getShareRuntimeSync().default;
|
||||
const sc = sceneConfig(scene);
|
||||
let title = (sc.title || '').trim();
|
||||
if (title && options?.titleVars) {
|
||||
const vars = options.titleVars;
|
||||
const missingRequired = Object.entries(vars).some(
|
||||
([key, value]) => title.includes(`{${key}}`) && !(value != null && String(value).trim()),
|
||||
);
|
||||
title = missingRequired ? '' : applyShareTitleTemplate(title, vars);
|
||||
}
|
||||
if (!title) {
|
||||
const preferEntity = scene === 'productDetail' || scene === 'storeDetail';
|
||||
|
||||
let title: string;
|
||||
if (preferEntity) {
|
||||
title = (options?.dynamicTitle || '').trim() || def.title;
|
||||
} else {
|
||||
title = (sc.title || '').trim();
|
||||
if (title && options?.titleVars) {
|
||||
const vars = options.titleVars;
|
||||
const missingRequired = Object.entries(vars).some(
|
||||
([key, value]) => title.includes(`{${key}}`) && !(value != null && String(value).trim()),
|
||||
);
|
||||
title = missingRequired ? '' : applyShareTitleTemplate(title, vars);
|
||||
}
|
||||
if (!title) {
|
||||
title = (options?.dynamicTitle || '').trim() || def.title;
|
||||
}
|
||||
}
|
||||
|
||||
const desc = (sc.desc || '').trim() || (options?.dynamicDesc || '').trim() || def.desc;
|
||||
const imgUrl =
|
||||
(sc.imageUrl || '').trim() ||
|
||||
(options?.dynamicImageUrl || '').trim() ||
|
||||
def.imageUrl ||
|
||||
getDefaultShareImageUrl();
|
||||
const imgUrl = preferEntity
|
||||
? (options?.dynamicImageUrl || '').trim() || def.imageUrl || getDefaultShareImageUrl()
|
||||
: (sc.imageUrl || '').trim() ||
|
||||
(options?.dynamicImageUrl || '').trim() ||
|
||||
def.imageUrl ||
|
||||
getDefaultShareImageUrl();
|
||||
|
||||
return {
|
||||
title,
|
||||
desc,
|
||||
|
||||
@@ -4,6 +4,7 @@ import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimel
|
||||
import PageShell from '../../components/PageShell';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||
import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../../lib/nav-bar';
|
||||
@@ -150,10 +151,11 @@ export default function BenefitPage() {
|
||||
<View>
|
||||
<Text className="benefit-hero-label">当前好客权益余额</Text>
|
||||
<View className="benefit-hero-amount">
|
||||
<Text className="benefit-hero-symbol">¥</Text>
|
||||
<Text className="benefit-hero-value">
|
||||
{summary ? formatMoney(summary.totalBalance) : '--'}
|
||||
</Text>
|
||||
<BenefitFigure
|
||||
value={summary ? formatMoney(summary.totalBalance) : '--'}
|
||||
size="xl"
|
||||
className="benefit-hero-value"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="benefit-hero-logo">
|
||||
@@ -193,16 +195,19 @@ export default function BenefitPage() {
|
||||
<View className="benefit-coupon-notch benefit-coupon-notch--right" />
|
||||
<View className="benefit-coupon-head">
|
||||
<Text className="benefit-coupon-name">{c.sourceProduct || '好客权益'}</Text>
|
||||
<Text className="benefit-coupon-balance">¥{formatMoney(c.balance)}</Text>
|
||||
<BenefitFigure value={formatMoney(c.balance)} size="md" className="benefit-coupon-balance" />
|
||||
</View>
|
||||
<Text className="benefit-coupon-no">NO. {c.couponNo}</Text>
|
||||
<View className="benefit-progress">
|
||||
<View className="benefit-progress-bar" style={{ width: `${usagePercent(c)}%` }} />
|
||||
</View>
|
||||
<View className="benefit-coupon-footer">
|
||||
<Text className="benefit-coupon-meta">
|
||||
已用 ¥{formatMoney(c.usedAmount)} / 总额 ¥{formatMoney(c.totalAmount)}
|
||||
</Text>
|
||||
<View className="benefit-coupon-meta">
|
||||
<Text>已用</Text>
|
||||
<BenefitFigure value={formatMoney(c.usedAmount)} size="sm" />
|
||||
<Text>/ 总额</Text>
|
||||
<BenefitFigure value={formatMoney(c.totalAmount)} size="sm" />
|
||||
</View>
|
||||
<Text
|
||||
className="benefit-coupon-btn"
|
||||
onClick={() =>
|
||||
@@ -226,7 +231,12 @@ export default function BenefitPage() {
|
||||
<View className="benefit-coupon-notch benefit-coupon-notch--right" />
|
||||
<View className="benefit-coupon-head">
|
||||
<Text className="benefit-coupon-name">{r.storeName || '门店核销'}</Text>
|
||||
<Text className="benefit-coupon-balance">-¥{formatMoney(Number(r.amount))}</Text>
|
||||
<BenefitFigure
|
||||
prefix="-"
|
||||
value={formatMoney(Number(r.amount))}
|
||||
size="md"
|
||||
className="benefit-coupon-balance"
|
||||
/>
|
||||
</View>
|
||||
<Text className="benefit-coupon-no">NO. {r.redeemNo}</Text>
|
||||
<View className="benefit-coupon-footer">
|
||||
|
||||
@@ -9,6 +9,7 @@ import PageShell from '../../components/PageShell';
|
||||
import TabMainHeader from '../../components/TabMainHeader';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { bindWechatForUser } from '../../lib/wechat-auth';
|
||||
import {
|
||||
@@ -447,8 +448,7 @@ export default function MinePage() {
|
||||
<View>
|
||||
<Text className="mine-asset-label">好客权益余额</Text>
|
||||
<View className="mine-asset-amount">
|
||||
<Text className="mine-asset-currency">¥</Text>
|
||||
<Text className="mine-asset-value">{formatMoney(benefitBalance)}</Text>
|
||||
<BenefitFigure value={formatMoney(benefitBalance)} size="lg" className="mine-asset-value" />
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ensurePayReady } from '../../lib/pay-ready';
|
||||
import { fetchUserProfile } from '../../lib/pay-wechat';
|
||||
import { request, toast } from '../../lib/api';
|
||||
import { getProductMainImage } from '../../lib/product-images';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
|
||||
type PreviewProduct = {
|
||||
id: string;
|
||||
@@ -231,7 +232,11 @@ export default function OrderConfirmPickupPage() {
|
||||
</View>
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">好客权益</Text>
|
||||
<Text className="order-row-value--price">¥{Number(preview.benefitAmount).toFixed(2)}</Text>
|
||||
<BenefitFigure
|
||||
value={Number(preview.benefitAmount).toFixed(2)}
|
||||
size="sm"
|
||||
className="order-row-value--price"
|
||||
/>
|
||||
</View>
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">运费</Text>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { fetchUserProfile } from '../../lib/pay-wechat';
|
||||
import { request, toast } from '../../lib/api';
|
||||
import { canCrossCity, isCrossCityAddress } from '../../lib/product-fulfillment';
|
||||
import { getProductMainImage } from '../../lib/product-images';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
|
||||
type Address = {
|
||||
id: string;
|
||||
@@ -384,7 +385,11 @@ export default function OrderConfirmPage() {
|
||||
</View>
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">好客权益</Text>
|
||||
<Text className="order-row-value--price">¥{preview.benefitAmount.toFixed(2)}</Text>
|
||||
<BenefitFigure
|
||||
value={preview.benefitAmount.toFixed(2)}
|
||||
size="sm"
|
||||
className="order-row-value--price"
|
||||
/>
|
||||
</View>
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">运费</Text>
|
||||
|
||||
@@ -13,6 +13,7 @@ import PageShell from '../../components/PageShell';
|
||||
import PageNavBar from '../../components/PageNavBar';
|
||||
import ProductCarousel from '../../components/ProductCarousel';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { ensurePayReady } from '../../lib/pay-ready';
|
||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||
@@ -189,10 +190,10 @@ export default function ProductDetailPage() {
|
||||
<View className="product-detail-promo-icon">
|
||||
<Text className="product-detail-promo-icon-text">惠</Text>
|
||||
</View>
|
||||
<Text className="product-detail-promo-title">
|
||||
买杜康美酒 · 享全城好客礼遇
|
||||
<Text className="product-detail-promo-amount"> ¥{benefit}</Text>
|
||||
</Text>
|
||||
<View className="product-detail-promo-title">
|
||||
<Text>买杜康美酒 · 享全城好客礼遇</Text>
|
||||
<BenefitFigure value={String(benefit)} size="sm" className="product-detail-promo-amount" />
|
||||
</View>
|
||||
</View>
|
||||
<Text className="product-detail-promo-desc">
|
||||
购酒即享本城专属好客权益,为您安排一场地道饭局。权益金可直接用于本地签约门店消费,到店享用,醇香美酒配佳肴。
|
||||
|
||||
@@ -8,6 +8,7 @@ import RedeemQrCode from '../../components/RedeemQrCode';
|
||||
import SubPageHeader from '../../components/SubPageHeader';
|
||||
import { request, toast } from '../../lib/api';
|
||||
import { formatMoney } from '../../lib/money';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
|
||||
const POLL_INTERVAL_MS = 2500;
|
||||
const LAST_REDEEM_RECORD_KEY = 'lastRedeemRecordId';
|
||||
@@ -154,7 +155,7 @@ export default function RedeemCodePage() {
|
||||
<Text className="redeem-timer-label">失效倒计时</Text>
|
||||
</View>
|
||||
<Text className="u-muted">待核销金额</Text>
|
||||
<Text className="redeem-code-amount">¥ {formatMoney(amount)}</Text>
|
||||
<BenefitFigure value={formatMoney(amount)} size="lg" className="redeem-code-amount" />
|
||||
{token ? (
|
||||
<View className="redeem-code-token-wrap" onClick={onTokenTap}>
|
||||
<Text className="redeem-code-token-label">核销码编号(供追查)</Text>
|
||||
|
||||
@@ -6,7 +6,8 @@ import PageShell from '../../components/PageShell';
|
||||
import SubPageHeader from '../../components/SubPageHeader';
|
||||
import { request, toast } from '../../lib/api';
|
||||
import { formatShanghaiDateTime } from '../../lib/datetime';
|
||||
import { formatMoney } from '../../lib/money';
|
||||
import { formatMoney, toMoneyNumber } from '../../lib/money';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
|
||||
const LAST_REDEEM_RESULT_KEY = 'lastRedeemResult';
|
||||
|
||||
@@ -66,7 +67,7 @@ export default function RedeemSuccessPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const amount = Number(record?.amount ?? router.params.amount ?? 0);
|
||||
const amount = toMoneyNumber(record?.amount ?? router.params.amount);
|
||||
const storeName = record?.storeName || '门店';
|
||||
const redeemNo = record?.redeemNo || '—';
|
||||
const redeemedAt = formatChinaDateTime(record?.createdAt);
|
||||
@@ -119,7 +120,7 @@ export default function RedeemSuccessPage() {
|
||||
<Text>✓</Text>
|
||||
</View>
|
||||
<Text className="redeem-success-title">核销成功</Text>
|
||||
<Text className="redeem-success-amount">¥ {formatMoney(amount)}</Text>
|
||||
<BenefitFigure value={formatMoney(amount)} size="lg" className="redeem-success-amount" />
|
||||
<Text className="redeem-success-desc">已在 {storeName} 完成核销</Text>
|
||||
|
||||
<View className="redeem-success-details">
|
||||
|
||||
@@ -7,6 +7,7 @@ import SubPageHeader from '../../components/SubPageHeader';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||
import { formatMoney } from '../../lib/money';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
|
||||
type BenefitSummary = {
|
||||
totalBalance: number;
|
||||
@@ -103,7 +104,7 @@ export default function RedeemPage() {
|
||||
return;
|
||||
}
|
||||
if (value > redeemableMax) {
|
||||
toast(couponId ? '核销金额不能超过该权益可用余额' : '超出可用余额');
|
||||
toast(couponId ? '核销金额不能超过该权益可用余额' : '核销金额不能超过可用余额');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,9 +135,11 @@ export default function RedeemPage() {
|
||||
<Text className="redeem-hero-label">
|
||||
{couponId ? '当前权益可用余额' : '可用余额'}
|
||||
</Text>
|
||||
<Text className="redeem-hero-amount">
|
||||
¥{redeemableMax > 0 ? formatMoney(redeemableMax) : '--'}
|
||||
</Text>
|
||||
<BenefitFigure
|
||||
value={redeemableMax > 0 ? formatMoney(redeemableMax) : '--'}
|
||||
size="xl"
|
||||
className="redeem-hero-amount"
|
||||
/>
|
||||
</View>
|
||||
<View className="redeem-input-wrap">
|
||||
<Input
|
||||
@@ -153,9 +156,10 @@ export default function RedeemPage() {
|
||||
/>
|
||||
</View>
|
||||
<View className="redeem-amount-foot">
|
||||
<Text className="redeem-amount-hint">
|
||||
最高可核销 ¥{formatMoney(redeemableMax)}
|
||||
</Text>
|
||||
<View className="redeem-amount-hint">
|
||||
<Text>最高可核销</Text>
|
||||
<BenefitFigure value={formatMoney(redeemableMax)} size="sm" />
|
||||
</View>
|
||||
<Text className="redeem-fill-max" onClick={fillMaxAmount}>
|
||||
全部核销
|
||||
</Text>
|
||||
|
||||
@@ -16,6 +16,7 @@ import ShareNavButton from '../../components/ShareNavButton';
|
||||
import StoreRedeemMarquee from '../../components/StoreRedeemMarquee';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
import { request, toast } from '../../lib/api';
|
||||
import { toMoneyNumber } from '../../lib/money';
|
||||
import { maskPhone, toDialablePhone } from '../../lib/phone';
|
||||
import { track } from '../../lib/analytics';
|
||||
import { formatShanghaiDateTime } from '../../lib/datetime';
|
||||
@@ -116,9 +117,8 @@ function formatPackagePriceYuan(price: string | number) {
|
||||
return n.toFixed(2).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
function formatRedeemAmountYuan(amount: number | string) {
|
||||
const n = typeof amount === 'number' ? amount : Number(amount);
|
||||
if (!Number.isFinite(n)) return '0';
|
||||
function formatRedeemAmountYuan(amount: unknown) {
|
||||
const n = toMoneyNumber(amount);
|
||||
if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n));
|
||||
return n.toFixed(2).replace(/\.?0+$/, '');
|
||||
}
|
||||
@@ -323,6 +323,8 @@ export default function StoreDetailPage() {
|
||||
|
||||
const envPhotos = envPhotoUrls(store);
|
||||
const heroImages = uniqueUrls([store.coverUrl, ...(store.carouselUrls || [])]);
|
||||
/** 预览相册:封面 + 环境图(去重),页面展示仍分开 */
|
||||
const previewAlbum = uniqueUrls([store.coverUrl, ...envPhotos]);
|
||||
const packages = store.packages ?? [];
|
||||
|
||||
const intro = store.intro?.trim() || '';
|
||||
@@ -337,10 +339,12 @@ export default function StoreDetailPage() {
|
||||
}
|
||||
|
||||
function previewEnv(index: number) {
|
||||
if (!envPhotos.length) return;
|
||||
const current = envPhotos[index];
|
||||
if (!current) return;
|
||||
const urls = previewAlbum.length ? previewAlbum : envPhotos;
|
||||
Taro.previewImage({
|
||||
current: envPhotos[index],
|
||||
urls: envPhotos,
|
||||
current,
|
||||
urls,
|
||||
}).catch(() => toast('无法预览图片'));
|
||||
}
|
||||
|
||||
@@ -362,6 +366,7 @@ export default function StoreDetailPage() {
|
||||
variant="store"
|
||||
previewable
|
||||
imageFit="contain"
|
||||
previewUrls={previewAlbum}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
.benefit-hero-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@
|
||||
}
|
||||
|
||||
.benefit-hero-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: var(--font-headline);
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
@@ -216,6 +218,10 @@
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.benefit-coupon-meta .benefit-figure {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.benefit-coupon-no {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
@@ -244,6 +250,10 @@
|
||||
}
|
||||
|
||||
.benefit-coupon-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--color-subtle-gray);
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background: var(--color-aged-amber);
|
||||
color: var(--color-on-secondary-container);
|
||||
padding: 2px 8px;
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
|
||||
.mine-asset-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
@@ -226,6 +226,8 @@
|
||||
}
|
||||
|
||||
.mine-asset-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: var(--font-headline);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -215,6 +215,8 @@
|
||||
}
|
||||
|
||||
.order-row-value--price {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--color-heritage-red);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -175,6 +175,10 @@
|
||||
|
||||
.product-detail-promo-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: var(--font-headline);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
@@ -184,6 +188,7 @@
|
||||
|
||||
.product-detail-promo-amount {
|
||||
color: var(--color-heritage-red);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-detail-promo-desc {
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
}
|
||||
|
||||
.redeem-hero-amount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-headline);
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
@@ -92,6 +95,10 @@
|
||||
}
|
||||
|
||||
.redeem-amount-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--color-subtle-gray);
|
||||
line-height: 1.4;
|
||||
@@ -279,7 +286,9 @@
|
||||
}
|
||||
|
||||
.redeem-code-amount {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-headline);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
@@ -350,8 +359,9 @@
|
||||
}
|
||||
|
||||
.redeem-success-amount {
|
||||
display: block;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-headline);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
|
||||
Reference in New Issue
Block a user