feat(store): 新增门店中心及VIP会员升级功能

- 个人信息页新增门店名称和门店地址展示,支持异步加载与只读显示
- 用户信息卡片整体改为绿色渐变背景,添加光晕和白色边框样式
- 新增升级VIP会员入口,突出展示并添加推荐标签
- 新建VIP会员升级页面,包含门店信息表单和VIP权益预览
- 提交升级申请调用新增api,支持状态检测表单只读
- 门店中心页面重构,简化订单管理,新增功能卡片及VIP审核入口
- 页面加载校验店员身份,非店员拒绝访问并提示
- 购物相关页面和组件全面支持VIP会员价格优先显示dealerPrice
- 新增ShopDealerApply模型门店相关字段,丰富会员申请数据记录
This commit is contained in:
2026-07-04 10:26:47 +08:00
parent 2deaaffe13
commit 994695c405
19 changed files with 1744 additions and 632 deletions

View File

@@ -4,10 +4,11 @@ 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 { 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 = () => {
@@ -44,6 +45,8 @@ const UserPage: React.FC = () => {
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([])
@@ -64,11 +67,15 @@ const UserPage: React.FC = () => {
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' },
@@ -110,6 +117,8 @@ const UserPage: React.FC = () => {
const uid = user?.userId
if (uid) {
listUserRole({ userId: uid }).then(data => setUserRoles(data || [])).catch(() => {})
// 刷新 VIP 状态
checkAndCacheVipStatus(uid)
}
} finally {
setRefreshing(false)
@@ -120,69 +129,81 @@ const UserPage: React.FC = () => {
<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 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 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 flex-wrap'>
{isLoggedIn && <MemberBadge levelName={(user as any)?.memberLevelName} />}
{userRoles.map(role => (
<View
key={role.roleId}
className='inline-flex items-center rounded-full text-xs px-2 py-0.5'
style={{ background: '#eff6ff', color: '#1d4ed8', border: '1px solid #bfdbfe' }}
>
<Text>{role.roleName}</Text>
</View>
))}
</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' })}>
{!hasCardData ? (
<View className='h-7 w-16 mx-auto bg-gray-200 rounded animate-pulse' />
) : (
<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' })}>
{!hasCardData ? (
<View className='h-7 w-10 mx-auto bg-gray-200 rounded animate-pulse' />
) : (
<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' })}>
{!hasCardData ? (
<View className='h-7 w-10 mx-auto bg-gray-200 rounded animate-pulse' />
) : (
<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'>
@@ -223,14 +244,26 @@ const UserPage: React.FC = () => {
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'>
<Text className='text-base'>{item.icon}</Text>
<Text className='text-sm text-gray-700'>{item.label}</Text>
<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-gray-300 text-sm'>{'>'}</Text>
<Text className={`text-sm ${(item as any).highlight ? 'text-amber-700' : 'text-gray-300'}`}>{'>'}</Text>
</View>
))}
</View>