From 0748d20b4c78c44d9fe04668b5e0a043011d3994 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 22 May 2026 19:44:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(mp):=20=E5=95=86=E5=93=81=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=E5=8A=A0=E8=BD=BD=E8=B6=85=E6=97=B6=20+=20=E6=82=A3?= =?UTF-8?q?=E8=80=85=E5=85=91=E6=8D=A2=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 getProduct 失败后的 listProducts 慢 fallback(拉 100 条),直接报错 - 处理 productId 为空时 loading 永不结束的卡死问题 - 添加 8s 加载超时保护,超时自动显示错误状态+重试按钮 - 新增迁移 000161:患者角色添加 health.points.manage 兑换权限 --- .../src/pages/pkg-mall/product/index.scss | 15 +++++ .../src/pages/pkg-mall/product/index.tsx | 55 +++++++++++++------ crates/erp-server/migration/src/lib.rs | 4 ++ ...60522_000161_patient_points_manage_perm.rs | 37 +++++++++++++ 4 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 crates/erp-server/migration/src/m20260522_000161_patient_points_manage_perm.rs diff --git a/apps/miniprogram/src/pages/pkg-mall/product/index.scss b/apps/miniprogram/src/pages/pkg-mall/product/index.scss index 77f3281..5785939 100644 --- a/apps/miniprogram/src/pages/pkg-mall/product/index.scss +++ b/apps/miniprogram/src/pages/pkg-mall/product/index.scss @@ -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; diff --git a/apps/miniprogram/src/pages/pkg-mall/product/index.tsx b/apps/miniprogram/src/pages/pkg-mall/product/index.tsx index 0ce5247..9d7cc75 100644 --- a/apps/miniprogram/src/pages/pkg-mall/product/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/product/index.tsx @@ -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 = { 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(null); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(false); + const timerRef = useRef>(); 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 ( @@ -61,11 +82,14 @@ export default function ProductDetail() { ); } - if (!product) { + if (loadError || !product) { return ( - 商品不存在 + {!productId ? '参数错误' : '商品不存在或加载失败'} + + 重试 + ); @@ -93,12 +117,10 @@ export default function ProductDetail() { return ( - {/* 商品大图 */} {TYPE_LABEL[type]} - {/* 商品信息卡 */} {TYPE_LABEL[type]} @@ -113,7 +135,6 @@ export default function ProductDetail() { )} - {/* 库存/余额卡 */} 库存状态 @@ -133,7 +154,6 @@ export default function ProductDetail() { - {/* 温馨提示 */} {isService @@ -143,7 +163,6 @@ export default function ProductDetail() { - {/* 底部操作栏 */} 需要 diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 1069b21..446dfc3 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -166,6 +166,8 @@ mod m20260521_000161_consultation_media_id_and_suggestion_references; mod m20260521_000162_consultation_session_rating_feedback; mod m20260521_000163_reorganize_menus_by_business_flow; mod m20260521_000164_reorganize_menus_scheme_b; +mod m20260522_000160_article_add_is_public; +mod m20260522_000161_patient_points_manage_perm; pub struct Migrator; @@ -339,6 +341,8 @@ impl MigratorTrait for Migrator { Box::new(m20260521_000162_consultation_session_rating_feedback::Migration), Box::new(m20260521_000163_reorganize_menus_by_business_flow::Migration), Box::new(m20260521_000164_reorganize_menus_scheme_b::Migration), + Box::new(m20260522_000160_article_add_is_public::Migration), + Box::new(m20260522_000161_patient_points_manage_perm::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260522_000161_patient_points_manage_perm.rs b/crates/erp-server/migration/src/m20260522_000161_patient_points_manage_perm.rs new file mode 100644 index 0000000..0f91fe6 --- /dev/null +++ b/crates/erp-server/migration/src/m20260522_000161_patient_points_manage_perm.rs @@ -0,0 +1,37 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + // 患者角色需要 health.points.manage 权限才能兑换商品 + db.execute_unprepared( + "INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope, created_at, updated_at, created_by, updated_by, deleted_at, version) \ + SELECT r.id, p.id, r.tenant_id, 'self', NOW(), NOW(), r.id, r.id, NULL, 1 \ + FROM roles r \ + JOIN permissions p ON p.tenant_id = r.tenant_id AND p.code = 'health.points.manage' AND p.deleted_at IS NULL \ + WHERE r.code = 'patient' AND r.deleted_at IS NULL \ + ON CONFLICT (role_id, permission_id) WHERE deleted_at IS NULL \ + DO UPDATE SET deleted_at = NULL, version = role_permissions.version + 1, updated_at = NOW()" + ).await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + "DELETE FROM role_permissions \ + WHERE role_id IN (SELECT id FROM roles WHERE code = 'patient') \ + AND permission_id IN (SELECT id FROM permissions WHERE code = 'health.points.manage')", + ) + .await?; + + Ok(()) + } +}