fix(mp): 商品详情页加载超时 + 患者兑换权限

- 移除 getProduct 失败后的 listProducts 慢 fallback(拉 100 条),直接报错
- 处理 productId 为空时 loading 永不结束的卡死问题
- 添加 8s 加载超时保护,超时自动显示错误状态+重试按钮
- 新增迁移 000161:患者角色添加 health.points.manage 兑换权限
This commit is contained in:
iven
2026-05-22 19:44:48 +08:00
parent 09013ab94a
commit 0748d20b4c
4 changed files with 93 additions and 18 deletions

View File

@@ -11,11 +11,26 @@
&__loading, &__empty {
@include flex-center;
flex-direction: column;
gap: $sp-md;
padding: $sp-2xl 0;
font-size: var(--tk-font-body);
color: $tx3;
}
&__retry-btn {
padding: 10px 28px;
border-radius: $r-pill;
background: $pri;
@include touch-target;
}
&__retry-text {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: $white;
}
&__scroll {
flex: 1;
overflow: auto;

View File

@@ -1,11 +1,11 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { getProduct } from '../../../services/points';
import type { PointsProduct } from '../../../services/points';
import { usePointsStore } from '../../../stores/points';
import { useElderClass } from '../../../hooks/useElderClass';
import { usePageData } from '@/hooks/usePageData';
import PageShell from '@/components/ui/PageShell';
import './index.scss';
@@ -21,29 +21,33 @@ const TYPE_LABEL: Record<string, string> = {
privilege: '权益',
};
const LOAD_TIMEOUT = 8000;
export default function ProductDetail() {
const modeClass = useElderClass();
const router = useRouter();
const productId = router.params.product_id || '';
const account = usePointsStore((s) => s.account);
const refreshPoints = usePointsStore((s) => s.refresh);
const [product, setProduct] = useState<PointsProduct | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const fetchProduct = useCallback(async () => {
if (!productId) return;
if (!productId) {
setLoading(false);
setLoadError(true);
return;
}
setLoading(true);
setLoadError(false);
try {
const data = await getProduct(productId);
setProduct(data);
} catch {
try {
const { listProducts } = await import('../../../services/points');
const res = await listProducts({ page: 1, page_size: 100 });
const found = res.data.find((p) => p.id === productId);
if (found) setProduct(found);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
}
setLoadError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
@@ -51,6 +55,23 @@ export default function ProductDetail() {
usePageData(fetchProduct, { throttleMs: 30000 });
useEffect(() => {
refreshPoints();
}, [refreshPoints]);
// 加载超时保护
useEffect(() => {
if (loading) {
timerRef.current = setTimeout(() => {
setLoading(false);
setLoadError(true);
}, LOAD_TIMEOUT);
}
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [loading]);
if (loading) {
return (
<PageShell className={modeClass}>
@@ -61,11 +82,14 @@ export default function ProductDetail() {
);
}
if (!product) {
if (loadError || !product) {
return (
<PageShell className={modeClass}>
<View className='product-detail__empty'>
<Text></Text>
<Text>{!productId ? '参数错误' : '商品不存在或加载失败'}</Text>
<View className='product-detail__retry-btn' onClick={fetchProduct}>
<Text className='product-detail__retry-text'></Text>
</View>
</View>
</PageShell>
);
@@ -93,12 +117,10 @@ export default function ProductDetail() {
return (
<PageShell padding="none" safeBottom={false} scroll={false} className={`product-detail ${modeClass}`}>
<View className='product-detail__scroll'>
{/* 商品大图 */}
<View className={`product-detail__hero ${TYPE_BG[type] || ''}`}>
<Text className='product-detail__hero-type'>{TYPE_LABEL[type]}</Text>
</View>
{/* 商品信息卡 */}
<View className='product-detail__info-card'>
<View className='product-detail__tags'>
<Text className='product-detail__type-badge'>{TYPE_LABEL[type]}</Text>
@@ -113,7 +135,6 @@ export default function ProductDetail() {
)}
</View>
{/* 库存/余额卡 */}
<View className='product-detail__status-card'>
<View className='product-detail__status-row'>
<Text className='product-detail__status-label'></Text>
@@ -133,7 +154,6 @@ export default function ProductDetail() {
</View>
</View>
{/* 温馨提示 */}
<View className='product-detail__notice'>
<Text className='product-detail__notice-text'>
{isService
@@ -143,7 +163,6 @@ export default function ProductDetail() {
</View>
</View>
{/* 底部操作栏 */}
<View className='product-detail__footer'>
<View className='product-detail__footer-left'>
<Text className='product-detail__footer-hint'></Text>