- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
211 lines
11 KiB
TypeScript
211 lines
11 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, type UserCardStats, type UserOrderStats } from '@/api/shop/shopUserCard'
|
||
import { getMyClerk } from '@/api/shop/shopStoreUser'
|
||
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 { data: cardStats, run: runCardStats, loading: cardStatsLoading } = useRequest(getUserCardStats, {
|
||
manual: true,
|
||
refreshDeps: [isLoggedIn]
|
||
})
|
||
|
||
// 获取用户订单统计
|
||
const { data: orderStats, run: runOrderStats, loading: orderStatsLoading } = useRequest(getUserOrderStats, {
|
||
manual: true,
|
||
refreshDeps: [isLoggedIn]
|
||
})
|
||
|
||
// 登录后加载数据
|
||
useEffect(() => {
|
||
if (isLoggedIn) {
|
||
runCardStats()
|
||
runOrderStats()
|
||
// 查询门店关联
|
||
getMyClerk().then(data => setStoreInfo(data)).catch(() => setStoreInfo(null))
|
||
} else {
|
||
setStoreInfo(null)
|
||
}
|
||
}, [isLoggedIn])
|
||
|
||
// 每次页面重新显示时(从登录页返回、从其他页面返回)检查登录状态并刷新数据
|
||
useDidShow(() => {
|
||
// 先从 storage 快速同步,立即更新 UI(无网络延迟)
|
||
const hasUser = syncFromStorage()
|
||
if (hasUser) {
|
||
runCardStats()
|
||
runOrderStats()
|
||
// 刷新门店关联
|
||
getMyClerk().then(data => setStoreInfo(data)).catch(() => setStoreInfo(null))
|
||
}
|
||
})
|
||
|
||
const menuItems = [
|
||
// { icon: '🏆', label: '赛事活动', url: '/pages/event/my/index' },
|
||
// { icon: '📅', label: '预约穿线', url: '/pages/booking/list/index' },
|
||
// { icon: '🎫', label: '优惠券', url: '/pages/user/coupon-list' },
|
||
{ 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 [refreshing, setRefreshing] = useState(false)
|
||
|
||
// 下拉刷新:同时刷新用户信息、卡片统计、订单统计
|
||
const onRefresh = async () => {
|
||
if (!isLoggedIn) { setRefreshing(false); return }
|
||
setRefreshing(true)
|
||
try {
|
||
await Promise.all([refreshUser(), runCardStats(), runOrderStats()])
|
||
} 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 bg-white rounded-xl'>
|
||
<View className='flex items-center gap-3' onClick={handleAvatarClick}>
|
||
{isLoggedIn && user?.avatar ? (
|
||
<Image className='w-14 h-14 rounded-full' src={user.avatar} mode='aspectFill' />
|
||
) : (
|
||
<View className='w-14 h-14 rounded-full bg-gray-100 flex items-center justify-center'>
|
||
<Text className='text-2xl text-gray-300'>👤</Text>
|
||
</View>
|
||
)}
|
||
<View className='flex-1'>
|
||
<Text className='text-lg font-medium text-gray-800 block'>
|
||
{isLoggedIn ? (user?.nickname || user?.phone || '用户') : '点击登录'}
|
||
</Text>
|
||
<View className='flex items-center gap-2 mt-1'>
|
||
{isLoggedIn && <MemberBadge levelName={(user as any)?.memberLevelName} />}
|
||
</View>
|
||
</View>
|
||
{isLoggedIn && <Text className='text-gray-300 text-sm'>{'>'}</Text>}
|
||
</View>
|
||
{/* 数据概览 */}
|
||
{isLoggedIn && (
|
||
<View className='grid grid-cols-3 gap-2 mt-4 pt-4 border-t border-gray-50'>
|
||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/wallet' })}>
|
||
{loading ? (
|
||
<Text className='text-lg font-bold text-gray-300 block'>...</Text>
|
||
) : (
|
||
<Text className='text-lg font-bold text-gray-800 block'>
|
||
{cardStats?.balance || '0.00'}
|
||
</Text>
|
||
)}
|
||
<Text className='text-xs text-gray-400'>余额</Text>
|
||
</View>
|
||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/points-record' })}>
|
||
{loading ? (
|
||
<Text className='text-lg font-bold text-gray-300 block'>...</Text>
|
||
) : (
|
||
<Text className='text-lg font-bold text-gray-800 block'>
|
||
{cardStats?.points || (user as any)?.points || 0}
|
||
</Text>
|
||
)}
|
||
<Text className='text-xs text-gray-400'>积分</Text>
|
||
</View>
|
||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/coupon-list' })}>
|
||
{loading ? (
|
||
<Text className='text-lg font-bold text-gray-300 block'>...</Text>
|
||
) : (
|
||
<Text className='text-lg font-bold text-gray-800 block'>
|
||
{cardStats?.coupons || 0}
|
||
</Text>
|
||
)}
|
||
<Text className='text-xs text-gray-400'>优惠券</Text>
|
||
</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.navigateTo({ 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.navigateTo({ url: `/pages/order/list?tab=${item.status}` })}
|
||
>
|
||
<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' : ''
|
||
}`}
|
||
onClick={() => Taro.navigateTo({ url: item.url })}
|
||
>
|
||
<View className='flex items-center gap-3'>
|
||
<Text className='text-base'>{item.icon}</Text>
|
||
<Text className='text-sm text-gray-700'>{item.label}</Text>
|
||
</View>
|
||
<Text className='text-gray-300 text-sm'>{'>'}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
<View className='h-6' />
|
||
</ScrollView>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default UserPage
|