feat(mp): design-handoff 产出的页面样式和组件优化

- 首页/商城/医生端/积分/家庭档案等页面 SCSS + TSX 更新
- TabFilter 组件样式优化
- points service 接口调整
- app.config 路由注册更新
This commit is contained in:
iven
2026-05-18 02:12:41 +08:00
parent 2698c98888
commit e555496528
26 changed files with 1887 additions and 1428 deletions

View File

@@ -3,33 +3,29 @@ import { View, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import {
getProduct,
listProducts,
exchangeProduct,
} from '../../../services/points';
import type { PointsProduct } from '../../../services/points';
import { usePointsStore } from '../../../stores/points';
import { useAuthStore } from '../../../stores/auth';
import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
import PageShell from '@/components/ui/PageShell';
import './index.scss';
const TYPE_INITIAL: Record<string, string> = {
const TYPE_CHAR: 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',
physical: 'physical',
service: 'service',
privilege: 'privilege',
};
export default function ExchangeConfirm() {
@@ -37,6 +33,7 @@ export default function ExchangeConfirm() {
const [product, setProduct] = useState<PointsProduct | null>(null);
const account = usePointsStore((s) => s.account);
const refreshPoints = usePointsStore((s) => s.refresh);
const currentPatient = useAuthStore((s) => s.currentPatient);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const { safeSetTimeout } = useSafeTimeout();
@@ -52,17 +49,21 @@ export default function ExchangeConfirm() {
setLoading(true);
try {
const [productRes] = await Promise.all([
listProducts({ page: 1, page_size: 100 }),
refreshPoints(),
]);
const found = productRes.data.find((p) => p.id === productId);
// 先尝试单商品接口,降级到列表查找
let found: PointsProduct | null = null;
try {
found = await getProduct(productId);
} catch {
const productRes = await listProducts({ page: 1, page_size: 100 });
found = productRes.data.find((p) => p.id === productId) || null;
}
if (!found) {
Taro.showToast({ title: '商品不存在', icon: 'none' });
safeSetTimeout(() => Taro.navigateBack(), 1500);
return;
}
setProduct(found);
await refreshPoints();
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
safeSetTimeout(() => Taro.navigateBack(), 1500);
@@ -82,6 +83,11 @@ export default function ExchangeConfirm() {
const balance = account?.balance ?? 0;
const cost = product?.points_cost ?? 0;
const insufficient = balance < cost;
const remaining = balance - cost;
const productType = product?.product_type || 'physical';
const isService = productType === 'service';
const typeChar = TYPE_CHAR[productType] || '礼';
const typeCls = TYPE_CLASS[productType] || 'physical';
const handleConfirm = useCallback(async () => {
if (!product || submitting) return;
@@ -103,17 +109,19 @@ export default function ExchangeConfirm() {
Taro.showToast({ title: '兑换成功', icon: 'success', duration: 2000 });
safeSetTimeout(() => {
Taro.showModal({
title: '兑换成功',
content: `核销码: ${order.qr_code}\n请凭此码到前台核销`,
showCancel: false,
confirmText: '查看订单',
success: () => {
Taro.redirectTo({
url: `/pages/pkg-mall/orders/index`,
});
},
});
if (isService && order.qr_code) {
Taro.showModal({
title: '兑换成功',
content: `核销码: ${order.qr_code}\n请凭此码到前台核销`,
showCancel: false,
confirmText: '查看订单',
success: () => {
Taro.redirectTo({ url: '/pages/pkg-mall/orders/index' });
},
});
} else {
Taro.redirectTo({ url: '/pages/pkg-mall/orders/index' });
}
}, 2000);
} catch (err) {
const msg = err instanceof Error ? err.message : '兑换失败';
@@ -125,7 +133,7 @@ export default function ExchangeConfirm() {
} finally {
setSubmitting(false);
}
}, [product, submitting, insufficient, cost]);
}, [product, submitting, insufficient, cost, isService]);
if (loading) {
return (
@@ -135,88 +143,72 @@ export default function ExchangeConfirm() {
);
}
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 (
<PageShell className={modeClass}>
<PageShell padding="md" safeBottom={false} scroll={false} className={`exchange-page ${modeClass}`}>
{/* 商品预览卡片 */}
<View className='product-card'>
<View className={`product-icon-wrap ${iconCls}`}>
<Text className='product-icon-char'>{initial}</Text>
<View className='exchange-product-card'>
<View className={`exchange-product-icon exchange-product-icon--${typeCls}`}>
<Text className='exchange-product-icon-char'>{typeChar}</Text>
</View>
<View className='product-meta'>
<Text className='product-name'>{product?.name || ''}</Text>
<Text className='product-type-tag'>{typeLabel}</Text>
<View className='exchange-product-meta'>
<Text className='exchange-product-name'>{product?.name || ''}</Text>
<Text className='exchange-product-points'>{cost.toLocaleString()} </Text>
<Text className='exchange-product-qty'>×1</Text>
</View>
</View>
{/* 收货信息(实体商品) */}
{!isService && currentPatient && (
<View className='exchange-address-card'>
<View className='exchange-address-header'>
<Text className='exchange-address-title'></Text>
<Text className='exchange-address-edit'> </Text>
</View>
<Text className='exchange-address-name'>
{currentPatient.name}
</Text>
<Text className='exchange-address-detail'></Text>
</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 className='exchange-detail-card'>
<Text className='exchange-detail-title'></Text>
<View className='exchange-detail-row'>
<Text className='exchange-detail-label'></Text>
<Text className='exchange-detail-value'>{cost.toLocaleString()}</Text>
</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 className='exchange-detail-row'>
<Text className='exchange-detail-label'>{isService ? '核销方式' : '运费'}</Text>
<Text className='exchange-detail-value'>{isService ? '到院核销' : '¥0.00'}</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
? '已兑完'
: '确认兑换'}
<View className='exchange-detail-row'>
<Text className='exchange-detail-label'></Text>
<Text className='exchange-detail-value exchange-detail-cost'>{cost.toLocaleString()}</Text>
</View>
<View className='exchange-detail-row last'>
<Text className='exchange-detail-label'></Text>
<Text className={`exchange-detail-value ${remaining >= 0 ? 'sufficient' : 'insufficient'}`}>
{remaining.toLocaleString()}
</Text>
</View>
</View>
{/* 确认兑换按钮 */}
<View
className={`exchange-confirm-btn ${insufficient || (product?.stock ?? 0) <= 0 || submitting ? 'disabled' : ''}`}
onClick={insufficient || (product?.stock ?? 0) <= 0 || submitting ? undefined : handleConfirm}
>
<Text className='exchange-confirm-text'>
{submitting
? '兑换中...'
: insufficient
? '积分不足'
: (product?.stock ?? 0) <= 0
? '已兑完'
: '确认兑换'}
</Text>
</View>
</PageShell>
);
}