diff --git a/apps/miniprogram/config/prod.ts b/apps/miniprogram/config/prod.ts index b24ab38..1925f04 100644 --- a/apps/miniprogram/config/prod.ts +++ b/apps/miniprogram/config/prod.ts @@ -1,8 +1,20 @@ import type { UserConfigExport } from '@tarojs/cli'; export default { - logger: { quiet: false }, - mini: { miniCssExtractPluginOption: { ignoreOrder: true } }, + logger: { quiet: true }, + mini: { + miniCssExtractPluginOption: { ignoreOrder: true }, + terserOption: { + compress: { + drop_console: true, + drop_debugger: true, + pure_funcs: ['console.log', 'console.info', 'console.debug'], + }, + format: { + comments: false, + }, + }, + }, h5: { miniCssExtractPluginOption: { ignoreOrder: true, diff --git a/apps/miniprogram/src/app.config.ts b/apps/miniprogram/src/app.config.ts index 1726de1..35ad8fb 100644 --- a/apps/miniprogram/src/app.config.ts +++ b/apps/miniprogram/src/app.config.ts @@ -3,44 +3,50 @@ export default defineAppConfig({ 'pages/index/index', 'pages/login/index', 'pages/health/index', - 'pages/health/input/index', - 'pages/health/trend/index', - 'pages/health/daily-monitoring/index', + 'pages/consultation/index', + 'pages/consultation/detail/index', + 'pages/mall/index', + 'pages/profile/index', 'pages/appointment/index', 'pages/appointment/create/index', 'pages/appointment/detail/index', 'pages/article/index', - 'pages/article/detail/index', - 'pages/report/detail/index', - 'pages/ai-report/list/index', - 'pages/ai-report/detail/index', - 'pages/followup/detail/index', - 'pages/consultation/index', - 'pages/consultation/detail/index', - 'pages/mall/index', - 'pages/mall/exchange/index', - 'pages/mall/orders/index', - 'pages/mall/detail/index', - 'pages/profile/index', - 'pages/profile/family/index', - 'pages/profile/family-add/index', - 'pages/profile/reports/index', - 'pages/profile/followups/index', - 'pages/profile/medication/index', - 'pages/profile/settings/index', 'pages/legal/user-agreement', 'pages/legal/privacy-policy', - 'pages/doctor/index', - 'pages/doctor/patients/index', - 'pages/doctor/patients/detail/index', - 'pages/doctor/consultation/index', - 'pages/doctor/consultation/detail/index', - 'pages/doctor/followup/index', - 'pages/doctor/followup/detail/index', - 'pages/doctor/report/index', - 'pages/doctor/report/detail/index', - 'pages/events/index', - 'pages/device-sync/index', + ], + subPackages: [ + { + root: 'pages/health', + pages: ['trend/index', 'input/index', 'daily-monitoring/index'], + }, + { + root: 'pages/doctor', + pages: [ + 'index', 'patients/index', 'patients/detail/index', + 'consultation/index', 'consultation/detail/index', + 'followup/index', 'followup/detail/index', + 'report/index', 'report/detail/index', + ], + }, + { + root: 'pages/mall', + pages: ['exchange/index', 'orders/index', 'detail/index'], + }, + { + root: 'pages/profile', + pages: [ + 'family/index', 'family-add/index', 'reports/index', + 'followups/index', 'medication/index', 'settings/index', + ], + }, + { + root: 'pages', + pages: [ + 'article/detail/index', 'ai-report/list/index', + 'ai-report/detail/index', 'report/detail/index', + 'followup/detail/index', 'events/index', 'device-sync/index', + ], + }, ], tabBar: { color: '#A8A29E', diff --git a/apps/miniprogram/src/app.tsx b/apps/miniprogram/src/app.tsx index 61681ca..d701cac 100644 --- a/apps/miniprogram/src/app.tsx +++ b/apps/miniprogram/src/app.tsx @@ -2,10 +2,14 @@ import { useEffect, PropsWithChildren } from 'react'; import Taro from '@tarojs/taro'; import ErrorBoundary from './components/ErrorBoundary'; import { flushEvents } from './services/analytics'; +import { useAuthStore } from './stores/auth'; import './app.scss'; function App({ children }: PropsWithChildren>) { + const restoreAuth = useAuthStore((s) => s.restore); + useEffect(() => { + restoreAuth(); const timer = setInterval(() => { flushEvents(); }, 30000); diff --git a/apps/miniprogram/src/components/EcCanvas/index.tsx b/apps/miniprogram/src/components/EcCanvas/index.tsx index ec0a29c..577c153 100644 --- a/apps/miniprogram/src/components/EcCanvas/index.tsx +++ b/apps/miniprogram/src/components/EcCanvas/index.tsx @@ -29,7 +29,7 @@ export interface EcCanvasRef { setOption: (option: echarts.EChartsOption) => void; } -const EcCanvas = forwardRef( +const EcCanvas = React.memo(React.forwardRef( ({ canvasId, height = 300 }, ref) => { const chartInstance = useRef(null); const canvasNode = useRef(null); @@ -91,7 +91,7 @@ const EcCanvas = forwardRef( ); }, -); +)); EcCanvas.displayName = 'EcCanvas'; diff --git a/apps/miniprogram/src/components/EmptyState/index.tsx b/apps/miniprogram/src/components/EmptyState/index.tsx index 4bdee10..defe12c 100644 --- a/apps/miniprogram/src/components/EmptyState/index.tsx +++ b/apps/miniprogram/src/components/EmptyState/index.tsx @@ -10,7 +10,7 @@ interface EmptyStateProps { onAction?: () => void; } -export default function EmptyState({ +export default React.memo(function EmptyState({ icon = '📭', text, hint, @@ -29,4 +29,4 @@ export default function EmptyState({ )} ); -} +}); diff --git a/apps/miniprogram/src/components/ErrorState/index.tsx b/apps/miniprogram/src/components/ErrorState/index.tsx index 1c7ef15..796e12e 100644 --- a/apps/miniprogram/src/components/ErrorState/index.tsx +++ b/apps/miniprogram/src/components/ErrorState/index.tsx @@ -7,7 +7,7 @@ interface ErrorStateProps { onRetry?: () => void; } -export default function ErrorState({ +export default React.memo(function ErrorState({ text = '加载失败,请稍后重试', onRetry, }: ErrorStateProps) { @@ -22,4 +22,4 @@ export default function ErrorState({ )} ); -} +}); diff --git a/apps/miniprogram/src/components/Loading/index.tsx b/apps/miniprogram/src/components/Loading/index.tsx index 8ccfe5a..d84d71f 100644 --- a/apps/miniprogram/src/components/Loading/index.tsx +++ b/apps/miniprogram/src/components/Loading/index.tsx @@ -6,11 +6,11 @@ interface LoadingProps { text?: string; } -export default function Loading({ text = '加载中...' }: LoadingProps) { +export default React.memo(function Loading({ text = '加载中...' }: LoadingProps) { return ( {text} ); -} +}); diff --git a/apps/miniprogram/src/components/StepIndicator/index.tsx b/apps/miniprogram/src/components/StepIndicator/index.tsx index a4bd659..d57bf69 100644 --- a/apps/miniprogram/src/components/StepIndicator/index.tsx +++ b/apps/miniprogram/src/components/StepIndicator/index.tsx @@ -12,7 +12,7 @@ interface StepIndicatorProps { onChange?: (index: number) => void; } -export default function StepIndicator({ steps, current, onChange }: StepIndicatorProps) { +export default React.memo(function StepIndicator({ steps, current, onChange }: StepIndicatorProps) { return ( {steps.map((step, idx) => { @@ -39,4 +39,4 @@ export default function StepIndicator({ steps, current, onChange }: StepIndicato })} ); -} +}); diff --git a/apps/miniprogram/src/components/TrendChart/index.tsx b/apps/miniprogram/src/components/TrendChart/index.tsx index b397961..b03f097 100644 --- a/apps/miniprogram/src/components/TrendChart/index.tsx +++ b/apps/miniprogram/src/components/TrendChart/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useCallback, useState } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import EcCanvas from '../EcCanvas'; import type { EcCanvasRef } from '../EcCanvas'; @@ -12,7 +12,7 @@ interface TrendChartProps { height?: number; } -export default function TrendChart({ +export default React.memo(function TrendChart({ data, referenceMin, referenceMax, @@ -20,7 +20,6 @@ export default function TrendChart({ height = 500, }: TrendChartProps) { const chartRef = useRef(null); - const [chartReady, setChartReady] = useState(false); const getOption = useCallback(() => { if (!data || data.length === 0) return null; @@ -108,7 +107,6 @@ export default function TrendChart({ const option = getOption(); if (option) { chartRef.current.setOption(option); - setChartReady(true); } } }, [data, getOption]); @@ -123,14 +121,7 @@ export default function TrendChart({ return ( - {!chartReady && ( - - - - - - )} ); -} +}); diff --git a/apps/miniprogram/src/components/WeekCalendar/index.tsx b/apps/miniprogram/src/components/WeekCalendar/index.tsx index f777419..45627c4 100644 --- a/apps/miniprogram/src/components/WeekCalendar/index.tsx +++ b/apps/miniprogram/src/components/WeekCalendar/index.tsx @@ -23,7 +23,7 @@ function getWeekDates(offset: number): string[] { const WEEKDAYS = ['一', '二', '三', '四', '五', '六', '日']; -export default function WeekCalendar({ scheduledDates, selectedDate, onSelectDate }: WeekCalendarProps) { +export default React.memo(function WeekCalendar({ scheduledDates, selectedDate, onSelectDate }: WeekCalendarProps) { const [weekOffset, setWeekOffset] = useState(0); const dates = getWeekDates(weekOffset); const today = (() => { @@ -60,4 +60,4 @@ export default function WeekCalendar({ scheduledDates, selectedDate, onSelectDat ); -} +}); diff --git a/apps/miniprogram/src/pages/article/index.tsx b/apps/miniprogram/src/pages/article/index.tsx index 3cdc8b3..ee376c6 100644 --- a/apps/miniprogram/src/pages/article/index.tsx +++ b/apps/miniprogram/src/pages/article/index.tsx @@ -119,7 +119,7 @@ export default function ArticleList() { {a.cover_image && ( - + )} diff --git a/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx b/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx index 94b4460..7027652 100644 --- a/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx +++ b/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx @@ -4,6 +4,9 @@ import Taro, { useDidShow } from '@tarojs/taro'; import { z } from 'zod'; import { createDailyMonitoring } from '@/services/health'; import { useAuthStore } from '@/stores/auth'; +import { useHealthStore } from '@/stores/health'; +import { usePointsStore } from '@/stores/points'; +import { clearRequestCache } from '@/services/request'; import { trackEvent } from '@/services/analytics'; import './index.scss'; @@ -217,6 +220,9 @@ export default function DailyMonitoring() { }); trackEvent('daily_monitoring_submit', { date: recordDate }); + useHealthStore.getState().clearCache(); + clearRequestCache('/health/'); + usePointsStore.getState().invalidate(); Taro.showToast({ title: '上报成功', icon: 'success' }); setTimeout(() => { diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index c674843..0f98e55 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -3,12 +3,24 @@ import { View, Text, ScrollView } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { useHealthStore } from '../../stores/health'; import { listDailyMonitoring, DailyMonitoring } from '../../services/health'; -import { getCheckinStatus, CheckinStatus } from '../../services/points'; +import { usePointsStore } from '../../stores/points'; import { useAuthStore } from '../../stores/auth'; import { trackEvent } from '../../services/analytics'; import Loading from '../../components/Loading'; import './index.scss'; +const QUICK_ACTIONS = [ + { label: '日常上报', char: '日', bg: 'icon-primary' }, + { label: '体征录入', char: '录', bg: 'icon-accent' }, + { label: '查看趋势', char: '势', bg: 'icon-warn' }, +]; + +const TREND_LINKS = [ + { label: '血压趋势', indicator: 'blood_pressure_systolic', char: '压' }, + { label: '心率趋势', indicator: 'heart_rate', char: '率' }, + { label: '血糖趋势', indicator: 'blood_sugar_fasting', char: '糖' }, +]; + function getStatusTag(status?: string) { if (status === 'high') return { label: '偏高', cls: 'tag-warn' }; if (status === 'low') return { label: '偏低', cls: 'tag-warn' }; @@ -39,23 +51,17 @@ function getBarPercent(value: number | undefined, ref?: string): number { export default function Health() { const { todaySummary, loading, refreshToday } = useHealthStore(); + const { checkinStatus, refresh: refreshPoints } = usePointsStore(); const { currentPatient } = useAuthStore(); - const [checkinStatus, setCheckinStatus] = useState(null); const [recentRecords, setRecentRecords] = useState([]); useDidShow(() => { refreshToday(); - loadExtraData(); + refreshPoints(); + loadRecentRecords(); }); - const loadExtraData = async () => { - try { - const status = await getCheckinStatus(); - setCheckinStatus(status); - } catch { - // points API 可能不可用 - } - + const loadRecentRecords = async () => { if (currentPatient) { try { const resp = await listDailyMonitoring(currentPatient.id, { page: 1, page_size: 3 }); @@ -91,16 +97,12 @@ export default function Health() { ]; const quickActions = [ - { label: '日常上报', char: '日', bg: 'icon-primary', action: goToDailyMonitoring }, - { label: '体征录入', char: '录', bg: 'icon-accent', action: goToInput }, - { label: '查看趋势', char: '势', bg: 'icon-warn', action: () => goToTrend('blood_pressure_systolic') }, + { ...QUICK_ACTIONS[0], action: goToDailyMonitoring }, + { ...QUICK_ACTIONS[1], action: goToInput }, + { ...QUICK_ACTIONS[2], action: () => goToTrend('blood_pressure_systolic') }, ]; - const trendLinks = [ - { label: '血压趋势', indicator: 'blood_pressure_systolic', char: '压' }, - { label: '心率趋势', indicator: 'heart_rate', char: '率' }, - { label: '血糖趋势', indicator: 'blood_sugar_fasting', char: '糖' }, - ]; + const trendLinks = TREND_LINKS; const formatBp = (record: DailyMonitoring) => { const parts: string[] = []; diff --git a/apps/miniprogram/src/pages/health/input/index.tsx b/apps/miniprogram/src/pages/health/input/index.tsx index 403aff8..0223bbb 100644 --- a/apps/miniprogram/src/pages/health/input/index.tsx +++ b/apps/miniprogram/src/pages/health/input/index.tsx @@ -5,6 +5,8 @@ import { z } from 'zod'; import { inputVitalSign } from '../../../services/health'; import { useAuthStore } from '../../../stores/auth'; import { useHealthStore } from '@/stores/health'; +import { usePointsStore } from '@/stores/points'; +import { clearRequestCache } from '@/services/request'; import { trackEvent } from '@/services/analytics'; import './index.scss'; @@ -88,6 +90,8 @@ export default function HealthInput() { note: note || undefined, }); clearCache(); + clearRequestCache('/health/'); + usePointsStore.getState().invalidate(); Taro.showToast({ title: '录入成功', icon: 'success' }); trackEvent('health_data_input', { type: currentIndicator }); setTimeout(() => Taro.navigateBack(), 1000); diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index f374048..4186e4c 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -11,6 +11,14 @@ import * as followupApi from '@/services/followup'; import * as articleApi from '../../services/article'; import './index.scss'; +const QUICK_SERVICES = [ + { label: '预约挂号', char: '约', path: '/pages/appointment/create/index' }, + { label: '健康录入', char: '录', path: '/pages/health/input/index' }, + { label: '健康趋势', char: '势', path: '/pages/health/trend/index' }, + { label: '资讯文章', char: '文', path: '/pages/article/index' }, + { label: 'AI 报告', char: 'AI', path: '/pages/ai-report/list/index' }, +]; + interface UpcomingItem { id: string; title: string; @@ -21,14 +29,13 @@ interface UpcomingItem { } export default function Index() { - const { user, currentPatient, restore: restoreAuth } = useAuthStore(); + const { user, currentPatient } = useAuthStore(); const { todaySummary, loading, refreshToday } = useHealthStore(); const [upcomingItems, setUpcomingItems] = useState([]); const [upcomingLoading, setUpcomingLoading] = useState(false); const [articles, setArticles] = useState([]); useDidShow(() => { - restoreAuth(); refreshToday(); loadUpcoming(); loadArticles(); @@ -92,18 +99,6 @@ export default function Index() { const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好'; const displayName = user?.display_name || currentPatient?.name || '访客'; - const quickServices = [ - { label: '预约挂号', char: '约', path: '/pages/appointment/create/index' }, - { label: '健康录入', char: '录', path: '/pages/health/input/index' }, - { label: '健康趋势', char: '势', path: '/pages/health/trend/index' }, - { label: '资讯文章', char: '文', path: '/pages/article/index' }, - { label: 'AI 报告', char: 'AI', path: '/pages/ai-report/list/index' }, - ]; - - const handleServiceClick = (path: string) => { - Taro.navigateTo({ url: path }); - }; - const healthItems = [ { label: '血压', value: todaySummary?.blood_pressure ? `${todaySummary.blood_pressure.systolic}/${todaySummary.blood_pressure.diastolic}` : '--/--', unit: 'mmHg', status: todaySummary?.blood_pressure?.status }, { label: '心率', value: todaySummary?.heart_rate ? `${todaySummary.heart_rate.value}` : '--', unit: 'bpm', status: todaySummary?.heart_rate?.status }, @@ -165,8 +160,8 @@ export default function Index() { 快捷服务 - {quickServices.map((svc) => ( - handleServiceClick(svc.path)}> + {QUICK_SERVICES.map((svc) => ( + Taro.navigateTo({ url: svc.path })}> {svc.char} diff --git a/apps/miniprogram/src/pages/mall/detail/index.tsx b/apps/miniprogram/src/pages/mall/detail/index.tsx index 602f4ba..a51b7d5 100644 --- a/apps/miniprogram/src/pages/mall/detail/index.tsx +++ b/apps/miniprogram/src/pages/mall/detail/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useCallback, useRef } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useDidShow, useReachBottom, usePullDownRefresh } from '@tarojs/taro'; -import { getAccount, listMyTransactions } from '../../../services/points'; -import type { PointsAccount, PointsTransaction } from '../../../services/points'; +import { listMyTransactions } from '../../../services/points'; +import type { PointsTransaction } from '../../../services/points'; +import { usePointsStore } from '../../../stores/points'; import EmptyState from '../../../components/EmptyState'; import Loading from '../../../components/Loading'; import './index.scss'; @@ -14,7 +15,7 @@ const TYPE_TABS = [ ]; export default function PointsDetail() { - const [account, setAccount] = useState(null); + const { account, refresh: refreshPoints } = usePointsStore(); const [transactions, setTransactions] = useState([]); const [activeTab, setActiveTab] = useState(''); const [page, setPage] = useState(1); @@ -22,15 +23,6 @@ export default function PointsDetail() { const [loading, setLoading] = useState(false); const loadingRef = useRef(false); - const fetchAccount = useCallback(async () => { - try { - const acct = await getAccount(); - setAccount(acct); - } catch { - // 账户可能尚未创建 - } - }, []); - const fetchTransactions = useCallback( async (pageNum: number, type: string, isRefresh = false) => { if (loadingRef.current) return; @@ -65,9 +57,9 @@ export default function PointsDetail() { const loadAll = useCallback( async (type?: string) => { const t = type !== undefined ? type : activeTab; - await Promise.all([fetchAccount(), fetchTransactions(1, t, true)]); + await Promise.all([refreshPoints(), fetchTransactions(1, t, true)]); }, - [fetchAccount, fetchTransactions, activeTab], + [refreshPoints, fetchTransactions, activeTab], ); useDidShow(() => { diff --git a/apps/miniprogram/src/pages/mall/exchange/index.tsx b/apps/miniprogram/src/pages/mall/exchange/index.tsx index 843808b..d3f29b7 100644 --- a/apps/miniprogram/src/pages/mall/exchange/index.tsx +++ b/apps/miniprogram/src/pages/mall/exchange/index.tsx @@ -2,11 +2,11 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { - getAccount, listProducts, exchangeProduct, } from '../../../services/points'; -import type { PointsAccount, PointsProduct } from '../../../services/points'; +import type { PointsProduct } from '../../../services/points'; +import { usePointsStore } from '../../../stores/points'; import Loading from '../../../components/Loading'; import './index.scss'; @@ -30,7 +30,7 @@ const TYPE_COLOR: Record = { export default function ExchangeConfirm() { const [product, setProduct] = useState(null); - const [account, setAccount] = useState(null); + const { account, refresh: refreshPoints } = usePointsStore(); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); @@ -50,9 +50,9 @@ export default function ExchangeConfirm() { setLoading(true); try { - const [productRes, accountRes] = await Promise.all([ + const [productRes] = await Promise.all([ listProducts({ page: 1, page_size: 100 }), - getAccount(), + refreshPoints(), ]); const found = productRes.data.find((p) => p.id === productId); if (!found) { @@ -61,14 +61,13 @@ export default function ExchangeConfirm() { return; } setProduct(found); - setAccount(accountRes); } 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; diff --git a/apps/miniprogram/src/pages/mall/index.tsx b/apps/miniprogram/src/pages/mall/index.tsx index 6d7ab08..cfd7957 100644 --- a/apps/miniprogram/src/pages/mall/index.tsx +++ b/apps/miniprogram/src/pages/mall/index.tsx @@ -1,14 +1,10 @@ import React, { useState, useCallback, useRef } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useDidShow, useReachBottom, usePullDownRefresh } from '@tarojs/taro'; -import { - getAccount, - dailyCheckin, - getCheckinStatus, - listProducts, -} from '../../services/points'; -import type { PointsAccount, PointsProduct, CheckinStatus } from '../../services/points'; +import { listProducts } from '../../services/points'; +import type { PointsProduct } from '../../services/points'; import { useAuthStore } from '../../stores/auth'; +import { usePointsStore } from '../../stores/points'; import Loading from '../../components/Loading'; import './index.scss'; @@ -27,8 +23,7 @@ const TYPE_BG: Record = { export default function Mall() { const { currentPatient } = useAuthStore(); - const [account, setAccount] = useState(null); - const [checkinStatus, setCheckinStatus] = useState(null); + const { account, checkinStatus, refresh: refreshPoints, doCheckin } = usePointsStore(); const [products, setProducts] = useState([]); const [productType, setProductType] = useState(''); const [page, setPage] = useState(1); @@ -38,24 +33,6 @@ export default function Mall() { const [noProfile, setNoProfile] = useState(false); const loadingRef = useRef(false); - const fetchAccountAndCheckin = useCallback(async () => { - if (!currentPatient) { - setNoProfile(true); - return; - } - setNoProfile(false); - try { - const [acct, status] = await Promise.all([ - getAccount(), - getCheckinStatus(), - ]); - setAccount(acct); - setCheckinStatus(status); - } catch { - // 账户可能尚未创建 - } - }, [currentPatient]); - const fetchProducts = useCallback( async (pageNum: number, type: string, isRefresh = false) => { if (loadingRef.current) return; @@ -88,9 +65,14 @@ export default function Mall() { const loadAll = useCallback( async (type?: string) => { const t = type !== undefined ? type : productType; - await Promise.all([fetchAccountAndCheckin(), fetchProducts(1, t, true)]); + if (!currentPatient) { + setNoProfile(true); + return; + } + setNoProfile(false); + await Promise.all([refreshPoints(), fetchProducts(1, t, true)]); }, - [fetchAccountAndCheckin, fetchProducts, productType], + [currentPatient, refreshPoints, fetchProducts, productType], ); useDidShow(() => { @@ -114,11 +96,10 @@ export default function Mall() { if (checkinLoading || checkinStatus?.checked_in_today) return; setCheckinLoading(true); try { - const result = await dailyCheckin(); - setCheckinStatus(result); - const acct = await getAccount(); - setAccount(acct); - Taro.showToast({ title: '签到成功', icon: 'success', duration: 2000 }); + const ok = await doCheckin(); + if (ok) { + Taro.showToast({ title: '签到成功', icon: 'success', duration: 2000 }); + } } catch (err) { Taro.showToast({ title: err instanceof Error ? err.message : '签到失败', diff --git a/apps/miniprogram/src/pages/profile/index.tsx b/apps/miniprogram/src/pages/profile/index.tsx index 03ac631..d76e786 100644 --- a/apps/miniprogram/src/pages/profile/index.tsx +++ b/apps/miniprogram/src/pages/profile/index.tsx @@ -1,9 +1,8 @@ -import React, { useState, useCallback } from 'react'; +import React from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { useAuthStore } from '../../stores/auth'; -import { getAccount, getCheckinStatus } from '../../services/points'; -import type { PointsAccount, CheckinStatus } from '../../services/points'; +import { usePointsStore } from '../../stores/points'; import './index.scss'; const MENU_ITEMS = [ @@ -17,28 +16,13 @@ const MENU_ITEMS = [ ]; export default function Profile() { - const { user, restore: restoreAuth, logout } = useAuthStore(); - const [pointsAccount, setPointsAccount] = useState(null); - const [checkinInfo, setCheckinInfo] = useState(null); + const { user, logout } = useAuthStore(); + const { account: pointsAccount, checkinStatus: checkinInfo, refresh: refreshPoints } = usePointsStore(); useDidShow(() => { - restoreAuth(); - loadPointsInfo(); + refreshPoints(); }); - const loadPointsInfo = useCallback(async () => { - try { - const [acct, status] = await Promise.all([ - getAccount(), - getCheckinStatus(), - ]); - setPointsAccount(acct); - setCheckinInfo(status); - } catch { - // 账户可能尚未创建 - } - }, []); - const handleMenuClick = (path: string) => { Taro.navigateTo({ url: path }); }; diff --git a/apps/miniprogram/src/services/request.ts b/apps/miniprogram/src/services/request.ts index 4023b6c..2c44cce 100644 --- a/apps/miniprogram/src/services/request.ts +++ b/apps/miniprogram/src/services/request.ts @@ -2,7 +2,6 @@ import Taro from '@tarojs/taro'; import { secureGet, secureSet, secureRemove } from '@/utils/secure-storage'; const BASE_URL = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1'; -const IS_DEV = process.env.NODE_ENV !== 'production'; interface ApiResponse { success: boolean; @@ -21,6 +20,7 @@ async function getHeaders(): Promise> { return headers; } +// --- Token refresh deduplication --- let refreshPromise: Promise | null = null; async function tryRefreshToken(): Promise { @@ -56,16 +56,11 @@ async function doRefresh(): Promise { return false; } -export async function request(method: string, path: string, data?: unknown): Promise { +// --- Core request --- +async function request(method: string, path: string, data?: unknown): Promise { const headers = await getHeaders(); const url = `${BASE_URL}${path}`; - if (IS_DEV) { - console.log(`[API] ${method} ${path}`); - } - const res = await Taro.request({ url, method: method as any, data, header: headers, timeout: 30000 }); - if (IS_DEV) { - console.log(`[API] ${method} ${path} → ${res.statusCode}`); - } + const res = await Taro.request({ url, method: method as any, data, header: headers, timeout: 15000 }); if (res.statusCode === 401) { const refreshed = await tryRefreshToken(); @@ -94,9 +89,56 @@ function buildQuery(params?: Record): strin : ''; } +// --- GET request cache + deduplication --- +interface CacheEntry { data: unknown; expiry: number } +const responseCache = new Map(); +const inflightRequests = new Map>(); +const DEFAULT_CACHE_TTL = 60_000; + +function getCacheKey(url: string): string { + const patientId = Taro.getStorageSync('current_patient_id') || ''; + return `${url}#${patientId}`; +} + +export function clearRequestCache(prefix?: string): void { + if (prefix) { + for (const key of responseCache.keys()) { + if (key.includes(prefix)) responseCache.delete(key); + } + } else { + responseCache.clear(); + } +} + export const api = { - get: (path: string, params?: Record) => - request('GET', `${path}${buildQuery(params)}`), + get: (path: string, params?: Record, cacheTtl?: number): Promise => { + const url = `${path}${buildQuery(params)}`; + const cacheKey = getCacheKey(url); + + const cached = responseCache.get(cacheKey); + if (cached && Date.now() < cached.expiry) { + return Promise.resolve(cached.data as T); + } + + const inflight = inflightRequests.get(cacheKey); + if (inflight) return inflight as Promise; + + const promise = request('GET', url).then((data) => { + inflightRequests.delete(cacheKey); + const ttl = cacheTtl ?? DEFAULT_CACHE_TTL; + if (ttl > 0) { + responseCache.set(cacheKey, { data, expiry: Date.now() + ttl }); + } + return data; + }).catch((err) => { + inflightRequests.delete(cacheKey); + throw err; + }); + + inflightRequests.set(cacheKey, promise); + return promise; + }, + post: (path: string, data?: unknown) => request('POST', path, data), put: (path: string, data?: unknown) => request('PUT', path, data), delete: (path: string) => request('DELETE', path), diff --git a/apps/miniprogram/src/stores/auth.ts b/apps/miniprogram/src/stores/auth.ts index 5970921..aa3a8cf 100644 --- a/apps/miniprogram/src/stores/auth.ts +++ b/apps/miniprogram/src/stores/auth.ts @@ -2,6 +2,7 @@ import { create } from 'zustand'; import Taro from '@tarojs/taro'; import * as authApi from '@/services/auth'; import { secureGet, secureSet, secureRemove } from '@/utils/secure-storage'; +import { clearRequestCache } from '@/services/request'; interface BindPhoneResp { access_token: string; @@ -129,6 +130,7 @@ export const useAuthStore = create((set, get) => ({ }, logout: () => { + clearRequestCache(); secureRemove('access_token'); secureRemove('refresh_token'); secureRemove('user_data'); diff --git a/apps/miniprogram/src/stores/health.ts b/apps/miniprogram/src/stores/health.ts index a68711b..c8887ec 100644 --- a/apps/miniprogram/src/stores/health.ts +++ b/apps/miniprogram/src/stores/health.ts @@ -9,26 +9,33 @@ interface CachedTrend { interface HealthState { todaySummary: healthApi.TodaySummary | null; + todaySummaryFetchedAt: number; trendData: Record; loading: boolean; - refreshToday: () => Promise; + refreshToday: (force?: boolean) => Promise; getTrend: (indicator: string, range: string) => Promise<{ date: string; value: number }[]>; clearCache: () => void; } -const CACHE_TTL = 5 * 60 * 1000; // 5 分钟 +const CACHE_TTL = 5 * 60 * 1000; +const TODAY_SUMMARY_TTL = 60_000; export const useHealthStore = create((set, get) => ({ todaySummary: null, + todaySummaryFetchedAt: 0, trendData: {}, loading: false, - refreshToday: async () => { + refreshToday: async (force = false) => { + const state = get(); + if (!force && state.todaySummary && Date.now() - state.todaySummaryFetchedAt < TODAY_SUMMARY_TTL) { + return; + } set({ loading: true }); try { const patientId = Taro.getStorageSync('current_patient_id') || undefined; const data = await healthApi.getTodaySummary(patientId); - set({ todaySummary: data, loading: false }); + set({ todaySummary: data, todaySummaryFetchedAt: Date.now(), loading: false }); } catch { set({ loading: false }); } @@ -51,5 +58,5 @@ export const useHealthStore = create((set, get) => ({ } }, - clearCache: () => set({ trendData: {}, todaySummary: null }), + clearCache: () => set({ trendData: {}, todaySummary: null, todaySummaryFetchedAt: 0 }), })); diff --git a/apps/miniprogram/src/stores/points.ts b/apps/miniprogram/src/stores/points.ts new file mode 100644 index 0000000..207a806 --- /dev/null +++ b/apps/miniprogram/src/stores/points.ts @@ -0,0 +1,49 @@ +import { create } from 'zustand'; +import * as pointsApi from '@/services/points'; + +interface PointsState { + account: pointsApi.PointsAccount | null; + checkinStatus: pointsApi.CheckinStatus | null; + loading: boolean; + lastFetched: number; + refresh: () => Promise; + invalidate: () => void; + doCheckin: () => Promise; +} + +const CACHE_TTL = 2 * 60 * 1000; + +export const usePointsStore = create((set, get) => ({ + account: null, + checkinStatus: null, + loading: false, + lastFetched: 0, + + refresh: async () => { + if (Date.now() - get().lastFetched < CACHE_TTL && get().account) return; + set({ loading: true }); + try { + const [account, checkinStatus] = await Promise.all([ + pointsApi.getAccount(), + pointsApi.getCheckinStatus(), + ]); + set({ account, checkinStatus, loading: false, lastFetched: Date.now() }); + } catch { + set({ loading: false }); + } + }, + + invalidate: () => set({ lastFetched: 0 }), + + doCheckin: async () => { + try { + const result = await pointsApi.dailyCheckin(); + set({ checkinStatus: result, lastFetched: 0 }); + const account = await pointsApi.getAccount(); + set({ account }); + return true; + } catch { + return false; + } + }, +})); diff --git a/apps/miniprogram/src/utils/secure-storage.ts b/apps/miniprogram/src/utils/secure-storage.ts index 1702c3d..b1f7fc6 100644 --- a/apps/miniprogram/src/utils/secure-storage.ts +++ b/apps/miniprogram/src/utils/secure-storage.ts @@ -1,10 +1,10 @@ import Taro from '@tarojs/taro'; -import CryptoJS from 'crypto-js'; +import AES from 'crypto-js/aes'; +import Utf8 from 'crypto-js/enc-utf8'; const ENCRYPTION_KEY = process.env.TARO_APP_ENCRYPTION_KEY || ''; -const IS_DEV = process.env.NODE_ENV !== 'production'; -if (!ENCRYPTION_KEY && IS_DEV) { +if (!ENCRYPTION_KEY && process.env.NODE_ENV !== 'production') { console.warn('[secure-storage] TARO_APP_ENCRYPTION_KEY 未设置,敏感数据将以明文存储'); } @@ -15,7 +15,7 @@ function encrypt(plaintext: string): string { } return plaintext; } - return CryptoJS.AES.encrypt(plaintext, ENCRYPTION_KEY).toString(); + return AES.encrypt(plaintext, ENCRYPTION_KEY).toString(); } function decrypt(ciphertext: string): string | null { @@ -26,8 +26,8 @@ function decrypt(ciphertext: string): string | null { return ciphertext; } try { - const bytes = CryptoJS.AES.decrypt(ciphertext, ENCRYPTION_KEY); - const result = bytes.toString(CryptoJS.enc.Utf8); + const bytes = AES.decrypt(ciphertext, ENCRYPTION_KEY); + const result = bytes.toString(Utf8); if (!result) return null; return result; } catch {