Files
hms/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx
iven 93c77c5857 fix(mp): T40 UI 设计系统合规审计修复 — 60 页面全覆盖
- 新增 $white 语义变量 + --tk-font-display Token
- 44 处 #fff → $white,2 处 background: #fff → $card
- 14 处 border-radius 硬编码统一为 $r-xs/$r-lg/$r
- 3 处 TSX inline 颜色提取为 SCSS 类(exchange/orders/action-inbox)
- ErrorBoundary 重构:6 个 inline style → SCSS 类 + Design Token
- 2 处离调色板颜色修正(#0284C7→$tx2, #94A3B8→$tx3)
- 2 处静默 catch 块添加状态清理(article/health)
- 趋势页补 Loading/EmptyState;咨询页 GuestGuard 统一
- 4 处 #FFFFFF → $white(mixins/index/exchange/variables)
2026-05-13 23:26:00 +08:00

215 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useCallback } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import {
listProducts,
exchangeProduct,
} from '../../../services/points';
import type { PointsProduct } from '../../../services/points';
import { usePointsStore } from '../../../stores/points';
import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
const TYPE_INITIAL: Record<string, string> = {
physical: '物',
service: '券',
privilege: '权',
};
const TYPE_LABEL: Record<string, string> = {
physical: '实物商品',
service: '服务券',
privilege: '权益卡',
};
const TYPE_CLASS: Record<string, string> = {
physical: 'product-icon-wrap--physical',
service: 'product-icon-wrap--service',
privilege: 'product-icon-wrap--privilege',
};
export default function ExchangeConfirm() {
const modeClass = useElderClass();
const [product, setProduct] = useState<PointsProduct | null>(null);
const { account, refresh: refreshPoints } = usePointsStore();
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
useDidShow(() => {
Taro.setNavigationBarTitle({ title: '确认兑换' });
loadData();
});
const loadData = useCallback(async () => {
const instance = Taro.getCurrentInstance();
const productId = instance.router?.params?.product_id;
if (!productId) {
Taro.showToast({ title: '参数错误', icon: 'none' });
setTimeout(() => Taro.navigateBack(), 1500);
return;
}
setLoading(true);
try {
const [productRes] = await Promise.all([
listProducts({ page: 1, page_size: 100 }),
refreshPoints(),
]);
const found = productRes.data.find((p) => p.id === productId);
if (!found) {
Taro.showToast({ title: '商品不存在', icon: 'none' });
setTimeout(() => Taro.navigateBack(), 1500);
return;
}
setProduct(found);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
setTimeout(() => Taro.navigateBack(), 1500);
} finally {
setLoading(false);
}
}, [refreshPoints]);
const balance = account?.balance ?? 0;
const cost = product?.points_cost ?? 0;
const insufficient = balance < cost;
const handleConfirm = useCallback(async () => {
if (!product || submitting) return;
if (insufficient) {
Taro.showToast({ title: '积分不足', icon: 'none' });
return;
}
const modalRes = await Taro.showModal({
title: '确认兑换',
content: `确定花费 ${cost} 积分兑换「${product.name}」吗?`,
});
if (!modalRes.confirm) return;
setSubmitting(true);
try {
const order = await exchangeProduct(product.id);
Taro.showToast({ title: '兑换成功', icon: 'success', duration: 2000 });
setTimeout(() => {
Taro.showModal({
title: '兑换成功',
content: `核销码: ${order.qr_code}\n请凭此码到前台核销`,
showCancel: false,
confirmText: '查看订单',
success: () => {
Taro.navigateTo({
url: `/pages/pkg-mall/orders/index`,
});
},
});
}, 2000);
} catch (err) {
const msg = err instanceof Error ? err.message : '兑换失败';
if (msg.includes('余额不足') || msg.includes('insufficient')) {
Taro.showToast({ title: '积分不足', icon: 'none' });
} else {
Taro.showToast({ title: msg, icon: 'none' });
}
} finally {
setSubmitting(false);
}
}, [product, submitting, insufficient, cost]);
if (loading) {
return (
<View className={`exchange-page ${modeClass}`}>
<Loading />
</View>
);
}
const productType = product?.product_type || 'physical';
const initial = TYPE_INITIAL[productType] || '礼';
const typeLabel = TYPE_LABEL[productType] || '商品';
const iconCls = TYPE_CLASS[productType] || 'product-icon-wrap--service';
return (
<View className={`exchange-page ${modeClass}`}>
{/* 商品预览卡片 */}
<View className='product-card'>
<View className={`product-icon-wrap ${iconCls}`}>
<Text className='product-icon-char'>{initial}</Text>
</View>
<View className='product-meta'>
<Text className='product-name'>{product?.name || ''}</Text>
<Text className='product-type-tag'>{typeLabel}</Text>
</View>
</View>
{/* 兑换明细 */}
<View className='detail-section'>
<Text className='detail-section-title'></Text>
<View className='detail-card'>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value detail-cost'>{cost.toLocaleString()}</Text>
</View>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text
className={`detail-value ${insufficient ? 'detail-insufficient' : 'detail-sufficient'}`}
>
{balance.toLocaleString()}
</Text>
</View>
{insufficient && (
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value detail-insufficient'>
-{(cost - balance).toLocaleString()}
</Text>
</View>
)}
<View className='detail-row last'>
<Text className='detail-label'></Text>
<Text className='detail-value'>
{product && product.stock > 0 ? `剩余 ${product.stock}` : '已兑完'}
</Text>
</View>
</View>
</View>
{/* 温馨提示 */}
<View className='notice-section'>
<Text className='notice-title'></Text>
<Text className='notice-text'>
</Text>
<Text className='notice-text'>退</Text>
</View>
{/* 底部操作 */}
<View className='exchange-footer'>
<View className='footer-cost'>
<Text className='footer-cost-label'></Text>
<Text className='footer-cost-num'>{cost.toLocaleString()}</Text>
<Text className='footer-cost-unit'></Text>
</View>
<View
className={`confirm-btn ${insufficient || (product?.stock ?? 0) <= 0 || submitting ? 'disabled' : ''}`}
onClick={insufficient || (product?.stock ?? 0) <= 0 || submitting ? undefined : handleConfirm}
>
<Text className='confirm-btn-text'>
{submitting
? '兑换中...'
: insufficient
? '积分不足'
: (product?.stock ?? 0) <= 0
? '已兑完'
: '确认兑换'}
</Text>
</View>
</View>
</View>
);
}