Files
hms/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx
iven 02a96682f6 fix(mp): 修复 72 个 TypeScript 类型错误 — noImplicitAny 全量通过
- 移除 28 个文件中不再需要的 import React (jsx: react-jsx)
- 修复 catch(err: any) → catch(err: unknown) 类型收窄
- 修复 __wxConfig / CanvasRenderingContext2D 全局类型声明
- 修复 DTO 类型不匹配 (UpdateDialysisRecordReq, notificationService)
- 移除 52 个未使用的 import/变量/常量
- 修复 services 类型 (auth.ts tenant_id, actionInbox boolean, device-sync)
2026-05-22 00:13:58 +08:00

217 lines
7.7 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 { useState, useCallback } from 'react';
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_CHAR: Record<string, string> = {
physical: '物',
service: '券',
privilege: '权',
};
const TYPE_CLASS: Record<string, string> = {
physical: 'physical',
service: 'service',
privilege: 'privilege',
};
export default function ExchangeConfirm() {
const modeClass = useElderClass();
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();
const loadData = useCallback(async () => {
const instance = Taro.getCurrentInstance();
const productId = instance.router?.params?.product_id;
if (!productId) {
Taro.showToast({ title: '参数错误', icon: 'none' });
safeSetTimeout(() => Taro.navigateBack(), 1500);
return;
}
setLoading(true);
try {
// 先尝试单商品接口,降级到列表查找
let found: PointsProduct | null = null;
try {
found = await getProduct(productId);
} catch (err) {
console.warn('[exchange] 单商品接口失败,降级列表查找:', err);
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 (err) {
console.warn('[exchange] 加载兑换页面失败:', err);
Taro.showToast({ title: '加载失败', icon: 'none' });
safeSetTimeout(() => Taro.navigateBack(), 1500);
} finally {
setLoading(false);
}
}, [refreshPoints]);
usePageData(
useCallback(async () => {
Taro.setNavigationBarTitle({ title: '确认兑换' });
await loadData();
}, [loadData]),
{ throttleMs: 10000, enablePullDown: false },
);
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;
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 });
safeSetTimeout(() => {
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 : '兑换失败';
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, isService]);
if (loading) {
return (
<PageShell className={modeClass}>
<Loading />
</PageShell>
);
}
return (
<PageShell padding="md" safeBottom={false} scroll={false} className={`exchange-page ${modeClass}`}>
{/* 商品预览卡片 */}
<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='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='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 className='exchange-detail-row'>
<Text className='exchange-detail-label'>{isService ? '核销方式' : '运费'}</Text>
<Text className='exchange-detail-value'>{isService ? '到院核销' : '¥0.00'}</Text>
</View>
<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>
);
}