- useRequest 改为 stale-while-revalidate 机制,缓存命中时立即展示数据并后台刷新 - 增加本地存储持久化,冷启动时先展示存储数据,避免骨架屏 - user 页面使用 mutate 结合本地缓存即时更新界面 - 网络请求失败时保留缓存数据,仅清除 loading 状态 - 优化加载逻辑,保障 30 秒缓存内数据可快速响应且自动刷新
300 lines
17 KiB
TypeScript
300 lines
17 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||
import Taro, { useDidShow } from '@tarojs/taro'
|
||
import { useUser } from '@/hooks/useUser'
|
||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||
import { useRequest } from '@/hooks/useRequest'
|
||
import { getUserCardStats, getUserOrderStats } from '@/api/shop/shopUserCard'
|
||
import { getMyClerk } from '@/api/shop/shopStoreUser'
|
||
import { listUserRole } from '@/api/system/userRole'
|
||
import type { UserRole } from '@/api/system/userRole/model'
|
||
import { checkAndCacheVipStatus } from '@/utils/vip'
|
||
import MemberBadge from '@/components/business/MemberBadge'
|
||
|
||
const UserPage: React.FC = () => {
|
||
const scrollHeight = useScrollHeight()
|
||
|
||
const { user, isLoggedIn, loading: userLoading, refreshUser, syncFromStorage } = useUser()
|
||
|
||
// 查询门店关联信息
|
||
const [storeInfo, setStoreInfo] = useState<any>(null)
|
||
// 用户角色列表
|
||
const [userRoles, setUserRoles] = useState<UserRole[]>([])
|
||
|
||
// 获取用户卡片统计(余额/积分/优惠券/礼品卡)
|
||
// stale-while-revalidate: 缓存命中即时展示 + 后台静默刷新
|
||
// localStorage 持久化: 冷启动先读 storage 即时展示,避免骨架屏
|
||
const { data: cardStats, run: runCardStats, loading: cardStatsLoading, mutate: mutateCardStats } = useRequest(getUserCardStats, {
|
||
manual: true,
|
||
cacheKey: 'user_card_stats',
|
||
cacheTime: 30 * 1000,
|
||
onSuccess: (data) => {
|
||
// 请求成功后持久化到本地存储,下次冷启动可直接展示
|
||
Taro.setStorageSync('user_card_stats_v2', data)
|
||
},
|
||
})
|
||
|
||
// 获取用户订单统计(同上 stale-while-revalidate + localStorage 持久化)
|
||
const { data: orderStats, run: runOrderStats, loading: orderStatsLoading, mutate: mutateOrderStats } = useRequest(getUserOrderStats, {
|
||
manual: true,
|
||
cacheKey: 'user_order_stats',
|
||
cacheTime: 30 * 1000,
|
||
onSuccess: (data) => {
|
||
Taro.setStorageSync('user_order_stats_v2', data)
|
||
},
|
||
})
|
||
|
||
// 登录后加载数据
|
||
useEffect(() => {
|
||
if (isLoggedIn) {
|
||
// 冷启动优化:先从 localStorage 读取缓存即时展示,消除骨架屏
|
||
const cachedCardStats = Taro.getStorageSync('user_card_stats_v2')
|
||
if (cachedCardStats) mutateCardStats(cachedCardStats)
|
||
const cachedOrderStats = Taro.getStorageSync('user_order_stats_v2')
|
||
if (cachedOrderStats) mutateOrderStats(cachedOrderStats)
|
||
// 然后发起网络请求(stale-while-revalidate 后台刷新)
|
||
runCardStats()
|
||
runOrderStats()
|
||
// 查询门店关联
|
||
getMyClerk().then(data => setStoreInfo(data)).catch(() => setStoreInfo(null))
|
||
// 查询用户角色
|
||
listUserRole({ userId: user?.userId }).then(data => setUserRoles(data || [])).catch(() => setUserRoles([]))
|
||
// 检查并缓存 VIP 状态
|
||
checkAndCacheVipStatus((user as any)?.userId || (user as any)?.id)
|
||
} else {
|
||
setStoreInfo(null)
|
||
setUserRoles([])
|
||
}
|
||
}, [isLoggedIn])
|
||
|
||
// 每次页面重新显示时(从登录页返回、从其他页面返回)检查登录状态并刷新数据
|
||
useDidShow(() => {
|
||
// 先从 storage 快速同步,立即更新 UI(无网络延迟)
|
||
const hasUser = syncFromStorage()
|
||
if (hasUser) {
|
||
// 冷启动优化:先从 localStorage 读取上次缓存的数据即时展示,消除骨架屏
|
||
const cachedCardStats = Taro.getStorageSync('user_card_stats_v2')
|
||
if (cachedCardStats) {
|
||
mutateCardStats(cachedCardStats)
|
||
}
|
||
const cachedOrderStats = Taro.getStorageSync('user_order_stats_v2')
|
||
if (cachedOrderStats) {
|
||
mutateOrderStats(cachedOrderStats)
|
||
}
|
||
// 然后发起网络请求(stale-while-revalidate 自动在后台刷新)
|
||
runCardStats()
|
||
runOrderStats()
|
||
// 刷新门店关联
|
||
getMyClerk().then(data => setStoreInfo(data)).catch(() => setStoreInfo(null))
|
||
// 刷新用户角色
|
||
const userId = Taro.getStorageSync('UserId')
|
||
if (userId) {
|
||
listUserRole({ userId }).then(data => setUserRoles(data || [])).catch(() => {})
|
||
// 刷新 VIP 状态缓存
|
||
checkAndCacheVipStatus(userId)
|
||
}
|
||
}
|
||
})
|
||
|
||
const menuItems = [
|
||
// 升级VIP会员 - 醒目样式
|
||
{ icon: '👑', label: '升级VIP会员', url: '/pages/user/vip-upgrade/index', highlight: true },
|
||
// 门店中心:仅门店店员/店长显示(通过 /shop/shop-store-user/my 判断)
|
||
...(storeInfo ? [{ icon: '🏪', label: '门店中心', url: '/pages/store/center/index' }] : []),
|
||
{ icon: '💰', label: '我的钱包', url: '/pages/user/wallet' },
|
||
{ icon: '📍', label: '收货地址', url: '/pages/user/address-list' },
|
||
// { icon: '⭐', label: '积分明细', url: '/pages/user/points-record' },
|
||
{ icon: '❤️', label: '我的收藏', url: '/pages/user/favorite-list/index' },
|
||
// { icon: '🕐', label: '浏览历史', url: '/pages/user/history-list/index' },
|
||
{ icon: '❓', label: '帮助中心', url: '/pages/user/help-center/index' },
|
||
// { icon: '🔔', label: '消息通知', url: '/pages/index/notification' },
|
||
// 门店管理:仅门店经理/店员显示
|
||
// ...(storeInfo ? [{ icon: '🏪', label: '门店管理', url: '/pages/store/list/index' }] : []),
|
||
{ icon: '⚙️', label: '设置', url: '/pages/user/setting' },
|
||
]
|
||
|
||
const handleAvatarClick = () => {
|
||
if (!isLoggedIn) {
|
||
Taro.navigateTo({ url: '/passport/login' })
|
||
} else {
|
||
Taro.navigateTo({ url: '/pages/user/profile' })
|
||
}
|
||
}
|
||
|
||
const loading = cardStatsLoading || orderStatsLoading
|
||
|
||
// 是否有可用数据(缓存或请求结果),用于判断是否需要骨架屏
|
||
const hasCardData = !!cardStats
|
||
const hasOrderData = !!orderStats
|
||
|
||
// 下拉刷新状态
|
||
const [refreshing, setRefreshing] = useState(false)
|
||
|
||
// 下拉刷新:同时刷新用户信息、卡片统计、订单统计、角色
|
||
const onRefresh = async () => {
|
||
if (!isLoggedIn) { setRefreshing(false); return }
|
||
setRefreshing(true)
|
||
try {
|
||
await Promise.all([refreshUser(), runCardStats(), runOrderStats()])
|
||
// 刷新角色
|
||
const uid = user?.userId
|
||
if (uid) {
|
||
listUserRole({ userId: uid }).then(data => setUserRoles(data || [])).catch(() => {})
|
||
// 刷新 VIP 状态
|
||
checkAndCacheVipStatus(uid)
|
||
}
|
||
} finally {
|
||
setRefreshing(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<View className='h-full bg-gray-50'>
|
||
<ScrollView scrollY refresherEnabled={!!isLoggedIn} refresherTriggered={refreshing} onRefresherRefresh={onRefresh} style={{ height: scrollHeight }}>
|
||
{/* 用户信息卡片 */}
|
||
<View className='mx-3 mt-3 p-4 rounded-xl relative overflow-hidden' style={{ background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)' }}>
|
||
{/* 装饰性光晕 */}
|
||
<View className='absolute -top-8 -right-8 w-32 h-32 rounded-full opacity-20' style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
|
||
<View className='absolute -bottom-4 -left-4 w-20 h-20 rounded-full opacity-10' style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
|
||
<View className='relative z-10'>
|
||
<View className='flex items-center gap-3' onClick={handleAvatarClick}>
|
||
{isLoggedIn && user?.avatar ? (
|
||
<Image className='w-14 h-14 rounded-full border-2 border-white shadow-sm' src={user.avatar} mode='aspectFill' />
|
||
) : (
|
||
<View className='w-14 h-14 rounded-full bg-white bg-opacity-20 flex items-center justify-center border-2 border-white border-opacity-30'>
|
||
<Text className='text-2xl'>👤</Text>
|
||
</View>
|
||
)}
|
||
<View className='flex-1'>
|
||
<Text className='text-lg font-medium text-white block'>
|
||
{isLoggedIn ? (user?.nickname || user?.phone || '用户') : '点击登录'}
|
||
</Text>
|
||
<View className='flex items-center gap-2 mt-1 flex-wrap'>
|
||
{/*{isLoggedIn && (*/}
|
||
{/* <View*/}
|
||
{/* className='inline-flex items-center rounded-full text-xs px-2 py-1'*/}
|
||
{/* style={{ background: 'rgba(255,255,255,0.2)', color: '#ffffff', border: '1px solid rgba(255,255,255,0.3)' }}*/}
|
||
{/* >*/}
|
||
{/* <Text>{(user as any)?.memberLevelName || '普通用户'}</Text>*/}
|
||
{/* </View>*/}
|
||
{/*)}*/}
|
||
{userRoles.map(role => (
|
||
<View
|
||
key={role.roleId}
|
||
className='inline-flex items-center rounded-full text-xs px-2 py-0.5'
|
||
style={{ background: 'rgba(255,255,255,0.2)', color: '#ffffff', border: '1px solid rgba(255,255,255,0.3)' }}
|
||
>
|
||
<Text>{role.roleName}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
{isLoggedIn && <Text className='text-white text-opacity-60 text-sm'>{'>'}</Text>}
|
||
</View>
|
||
{/* 数据概览 */}
|
||
{isLoggedIn && (
|
||
<View className='grid grid-cols-3 gap-2 mt-4 pt-4 border-t border-white border-opacity-15'>
|
||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/wallet' })}>
|
||
{!hasCardData ? (
|
||
<View className='h-7 w-16 mx-auto rounded animate-pulse' style={{ background: 'rgba(255,255,255,0.25)' }} />
|
||
) : (
|
||
<Text className='text-lg font-bold text-white block'>
|
||
{cardStats?.balance || '0.00'}
|
||
</Text>
|
||
)}
|
||
<Text className='text-xs text-white text-opacity-70'>余额</Text>
|
||
</View>
|
||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/points-record' })}>
|
||
{!hasCardData ? (
|
||
<View className='h-7 w-10 mx-auto rounded animate-pulse' style={{ background: 'rgba(255,255,255,0.25)' }} />
|
||
) : (
|
||
<Text className='text-lg font-bold text-white block'>
|
||
{cardStats?.points || (user as any)?.points || 0}
|
||
</Text>
|
||
)}
|
||
<Text className='text-xs text-white text-opacity-70'>积分</Text>
|
||
</View>
|
||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/coupon-list' })}>
|
||
{!hasCardData ? (
|
||
<View className='h-7 w-10 mx-auto rounded animate-pulse' style={{ background: 'rgba(255,255,255,0.25)' }} />
|
||
) : (
|
||
<Text className='text-lg font-bold text-white block'>
|
||
{cardStats?.coupons || 0}
|
||
</Text>
|
||
)}
|
||
<Text className='text-xs text-white text-opacity-70'>优惠券</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
{/* 我的订单快捷入口 */}
|
||
<View className='bg-white rounded-xl mx-3 mt-3 p-4'>
|
||
<View className='flex justify-between items-center mb-3'>
|
||
<Text className='text-base font-medium text-gray-800'>我的订单</Text>
|
||
<Text className='text-xs text-gray-400' onClick={() => Taro.switchTab({ url: '/pages/order/list' })}>
|
||
全部订单 {'>'}
|
||
</Text>
|
||
</View>
|
||
<View className='grid grid-cols-4 gap-2'>
|
||
{[
|
||
{ icon: '💳', label: '待付款', status: 0, count: orderStats?.pending },
|
||
{ icon: '📦', label: '待发货', status: 1, count: orderStats?.paid },
|
||
{ icon: '🚚', label: '待收货', status: 2, count: orderStats?.shipped },
|
||
{ icon: '✅', label: '已完成', status: 3, count: orderStats?.completed },
|
||
].map(item => (
|
||
<View
|
||
key={item.status}
|
||
className='flex flex-col items-center py-2 relative'
|
||
onClick={() => { Taro.setStorageSync('order_tab', item.status); Taro.switchTab({ url: '/pages/order/list' }) }}
|
||
>
|
||
<Text className='text-xl mb-1'>{item.icon}</Text>
|
||
<Text className='text-xs text-gray-600'>{item.label}</Text>
|
||
{/* 数量角标 */}
|
||
{item.count && item.count > 0 && (
|
||
<View className='absolute top-0 right-2 bg-red-500 text-white text-xs rounded-full min-w-4 h-4 flex items-center justify-center px-1'>
|
||
{item.count > 99 ? '99+' : item.count}
|
||
</View>
|
||
)}
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
{/* 功能菜单 */}
|
||
<View className='bg-white rounded-xl mx-3 mt-3 overflow-hidden'>
|
||
{menuItems.map((item, idx) => (
|
||
<View
|
||
key={item.label}
|
||
className={`flex items-center justify-between px-4 py-3 ${
|
||
idx < menuItems.length - 1 ? 'border-b border-gray-50' : ''
|
||
} ${(item as any).highlight ? '' : ''}`}
|
||
style={(item as any).highlight ? {
|
||
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
|
||
} : {}}
|
||
onClick={() => Taro.navigateTo({ url: item.url })}
|
||
>
|
||
<View className='flex items-center gap-3'>
|
||
<View className={`${(item as any).highlight ? 'w-8 h-8 rounded-lg flex items-center justify-center text-lg' : ''}`} style={(item as any).highlight ? { background: 'linear-gradient(135deg, #f59e0b, #d97706)' } : {}}>
|
||
<Text className='text-base'>{(item as any).highlight ? '' : item.icon}{(item as any).highlight ? item.icon : ''}</Text>
|
||
</View>
|
||
<View className='flex items-center gap-2'>
|
||
<Text className={`text-sm ${(item as any).highlight ? 'text-amber-900 font-semibold' : 'text-gray-700'}`}>{item.label}</Text>
|
||
{(item as any).highlight && (
|
||
<View className='px-1.5 py-0.5 rounded-full' style={{ background: 'linear-gradient(135deg, #dc2626, #b91c1c)' }}>
|
||
<Text className='text-white text-xs'>推荐</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
<Text className={`text-sm ${(item as any).highlight ? 'text-amber-700' : 'text-gray-300'}`}>{'>'}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
<View className='h-6' />
|
||
</ScrollView>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default UserPage
|