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

@@ -27,6 +27,8 @@ const ProfilePage: React.FC = () => {
avatar: (user as any)?.avatar || '',
gender: (user as any)?.gender ?? 0,
phone: (user as any)?.phone || '',
merchantName: (user as any)?.merchantName || '',
address: (user as any)?.address || '',
})
}
}, [user])
@@ -162,6 +164,30 @@ const ProfilePage: React.FC = () => {
<Text className='text-sm text-gray-600'>{registerTime || '未知'}</Text>
</View>
{/* 门店名称 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Input
className='flex-1 text-right text-sm text-gray-600'
placeholder='请输入门店名称'
value={(form as any)?.merchantName || ''}
onInput={(e: any) => setForm(prev => ({ ...prev, merchantName: e.detail.value }))}
maxlength={50}
/>
</View>
{/* 门店地址 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Input
className='flex-1 text-right text-sm text-gray-600'
placeholder='请输入门店地址'
value={(form as any)?.address || ''}
onInput={(e: any) => setForm(prev => ({ ...prev, address: e.detail.value }))}
maxlength={100}
/>
</View>
{/* 保存按钮 */}
<BottomButton
text='保存'

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>

View File

@@ -0,0 +1,389 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro'
import { listShopDealerApply, updateShopDealerApply } from '@/api/shop/shopDealerApply'
import type { ShopDealerApply } from '@/api/shop/shopDealerApply/model'
import { addUserRole, listUserRole } from '@/api/system/userRole'
import { getMyClerk } from '@/api/shop/shopStoreUser'
definePageConfig({
navigationBarTitleText: 'VIP会员审核',
})
// Tab 配置
type TabKey = 'pending' | 'approved' | 'rejected'
const TABS: { key: TabKey; label: string; status: number }[] = [
{ key: 'pending', label: '待审核', status: 10 },
{ key: 'approved', label: '已通过', status: 20 },
{ key: 'rejected', label: '已驳回', status: 30 },
]
// VIP 角色信息(对应 system/role 表)
const VIP_ROLE_ID = 2032
const VIP_ROLE_CODE = 'vip'
const VIP_ROLE_NAME = 'VIP会员'
// 格式化时间戳
function formatTime(time?: number | string): string {
if (!time) return '-'
let date: Date
if (typeof time === 'number') {
// 后端 applyTime 是毫秒时间戳
date = new Date(time > 1e12 ? time : time * 1000)
} else {
date = new Date(time)
}
if (isNaN(date.getTime())) return '-'
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const mi = String(date.getMinutes()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${mi}`
}
// 格式化日期为 Spring Boot LocalDateTime 格式
function formatDateTime(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const mi = String(date.getMinutes()).padStart(2, '0')
const s = String(date.getSeconds()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${mi}:${s}`
}
/**
* 给用户添加 VIP 角色
* 先查询是否已有 VIP 角色,没有则新增,避免重复绑定
*/
async function assignVipRole(userId?: number): Promise<void> {
if (!userId) return
// 查询用户已有角色
const userRoles = await listUserRole({ userId })
const hasVip = (userRoles || []).some(
r => r.roleId === VIP_ROLE_ID || r.roleCode === VIP_ROLE_CODE
)
if (hasVip) {
// 已拥有 VIP 角色,跳过
return
}
// 新增 VIP 角色绑定
await addUserRole({
userId,
roleId: VIP_ROLE_ID,
roleCode: VIP_ROLE_CODE,
roleName: VIP_ROLE_NAME,
})
}
const VipReviewPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabKey>('pending')
const [list, setList] = useState<ShopDealerApply[]>([])
const [loading, setLoading] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [isClerk, setIsClerk] = useState(false)
const loadingRef = useRef(false)
// 验证当前用户是否为店员
useEffect(() => {
getMyClerk()
.then(data => {
if (data) {
setIsClerk(true)
} else {
setIsClerk(false)
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
setTimeout(() => Taro.navigateBack(), 1500)
}
})
.catch(() => {
setIsClerk(false)
Taro.showToast({ title: '获取店员信息失败', icon: 'none' })
setTimeout(() => Taro.navigateBack(), 1500)
})
}, [])
// 加载申请列表
const loadList = useCallback(async (tab: TabKey) => {
if (loadingRef.current) return
loadingRef.current = true
setLoading(true)
try {
const tabConfig = TABS.find(t => t.key === tab)
if (!tabConfig) return
// 获取所有申请记录,前端按状态过滤
const data = await listShopDealerApply({})
const filtered = (data || []).filter(item => item.applyStatus === tabConfig.status)
// 按申请时间倒序
filtered.sort((a, b) => {
const ta = a.applyTime ? (typeof a.applyTime === 'number' ? a.applyTime : new Date(a.applyTime).getTime()) : 0
const tb = b.applyTime ? (typeof b.applyTime === 'number' ? b.applyTime : new Date(b.applyTime).getTime()) : 0
return tb - ta
})
setList(filtered)
} catch (e: any) {
Taro.showToast({ title: e.message || '加载失败', icon: 'none' })
setList([])
} finally {
loadingRef.current = false
setLoading(false)
}
}, [])
// 切换 tab 时重新加载
useEffect(() => {
if (isClerk) {
loadList(activeTab)
}
}, [activeTab, isClerk, loadList])
// 页面重新显示时刷新
useDidShow(() => {
if (isClerk) {
loadList(activeTab)
}
})
// 审核通过
const handleApprove = (item: ShopDealerApply) => {
Taro.showModal({
title: '确认通过',
content: `确认通过「${item.realName || item.merchantName || '该用户'}」的VIP会员申请\n\n请确认已实地考察核实门店名称和地址。`,
confirmColor: '#22c55e',
success: async (res) => {
if (!res.confirm) return
setSubmitting(true)
try {
await updateShopDealerApply({
applyId: item.applyId,
userId: item.userId,
realName: item.realName,
merchantName: item.merchantName,
address: item.address,
applyType: item.applyType,
applyStatus: 20, // 审核通过
auditTime: Date.now(),
} as ShopDealerApply)
// 审核通过后,给用户添加 VIP 角色
await assignVipRole(item.userId)
Taro.showToast({ title: '已通过', icon: 'success' })
loadList(activeTab)
} catch (e: any) {
Taro.showToast({ title: e.message || '操作失败', icon: 'none' })
} finally {
setSubmitting(false)
}
},
})
}
// 审核驳回
const handleReject = (item: ShopDealerApply) => {
Taro.showModal({
title: '驳回申请',
content: `确认驳回「${item.realName || item.merchantName || '该用户'}」的VIP会员申请`,
confirmColor: '#ef4444',
success: async (res) => {
if (!res.confirm) return
setSubmitting(true)
try {
await updateShopDealerApply({
applyId: item.applyId,
userId: item.userId,
realName: item.realName,
merchantName: item.merchantName,
address: item.address,
applyType: item.applyType,
applyStatus: 30, // 驳回
auditTime: Date.now(),
rejectReason: '门店信息核实不通过',
} as ShopDealerApply)
Taro.showToast({ title: '已驳回', icon: 'success' })
loadList(activeTab)
} catch (e: any) {
Taro.showToast({ title: e.message || '操作失败', icon: 'none' })
} finally {
setSubmitting(false)
}
},
})
}
// 渲染状态标签
const renderStatusTag = (status: number) => {
if (status === 10) {
return (
<View className='px-2 py-0.5 rounded-full bg-amber-100'>
<Text className='text-xs text-amber-600'></Text>
</View>
)
}
if (status === 20) {
return (
<View className='px-2 py-0.5 rounded-full bg-green-100'>
<Text className='text-xs text-green-600'></Text>
</View>
)
}
if (status === 30) {
return (
<View className='px-2 py-0.5 rounded-full bg-red-100'>
<Text className='text-xs text-red-500'></Text>
</View>
)
}
return null
}
// 渲染申请卡片
const renderCard = (item: ShopDealerApply) => {
const canReview = item.applyStatus === 10
return (
<View key={item.applyId} className='bg-white rounded-xl mx-3 mt-3 p-4'>
{/* 头部:申请人 + 状态 */}
<View className='flex justify-between items-center mb-3'>
<View className='flex items-center gap-2'>
<View className='w-8 h-8 rounded-full bg-green-50 flex items-center justify-center'>
<Text className='text-sm'>👤</Text>
</View>
<View>
<Text className='text-sm font-medium text-gray-800 block'>
{item.realName || item.merchantName || '未知用户'}
</Text>
<Text className='text-xs text-gray-400'>ID: {item.userId}</Text>
</View>
</View>
{renderStatusTag(item.applyStatus!)}
</View>
{/* 门店信息 */}
<View className='bg-gray-50 rounded-lg p-3 mb-3'>
<View className='flex items-start gap-2 mb-2'>
<Text className='text-xs text-gray-400 mt-0.5'>🏪</Text>
<View className='flex-1'>
<Text className='text-xs text-gray-400 block'></Text>
<Text className='text-sm text-gray-700 block mt-0.5'>
{item.merchantName || item.realName || '-'}
</Text>
</View>
</View>
<View className='flex items-start gap-2'>
<Text className='text-xs text-gray-400 mt-0.5'>📍</Text>
<View className='flex-1'>
<Text className='text-xs text-gray-400 block'></Text>
<Text className='text-sm text-gray-700 block mt-0.5'>
{item.address || '-'}
</Text>
</View>
</View>
</View>
{/* 时间信息 */}
<View className='flex justify-between items-center mb-3'>
<Text className='text-xs text-gray-400'>
{formatTime(item.applyTime)}
</Text>
{item.auditTime ? (
<Text className='text-xs text-gray-400'>
{formatTime(item.auditTime)}
</Text>
) : null}
</View>
{/* 驳回原因 */}
{item.applyStatus === 30 && item.rejectReason && (
<View className='bg-red-50 rounded-lg p-3 mb-3'>
<Text className='text-xs text-red-400 block'></Text>
<Text className='text-sm text-red-600 block mt-1'>{item.rejectReason}</Text>
</View>
)}
{/* 操作按钮 */}
{canReview && (
<View className='flex gap-3 border-t border-gray-50 pt-3'>
<View
className='flex-1 py-2.5 rounded-lg border border-red-300 flex items-center justify-center'
onClick={submitting ? undefined : () => handleReject(item)}
>
<Text className='text-sm text-red-500'></Text>
</View>
<View
className='flex-1 py-2.5 rounded-lg flex items-center justify-center'
style={{ backgroundColor: submitting ? '#86efac' : '#22c55e' }}
onClick={submitting ? undefined : () => handleApprove(item)}
>
<Text className='text-sm text-white'></Text>
</View>
</View>
)}
</View>
)
}
if (!isClerk) {
return (
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
<Text className='text-gray-400 text-sm'>...</Text>
</View>
)
}
return (
<View className='min-h-full bg-gray-50'>
{/* 提示信息 */}
<View className='bg-green-50 mx-3 mt-3 rounded-xl p-3 flex items-start gap-2'>
<Text className='text-sm'>💡</Text>
<View className='flex-1'>
<Text className='text-xs text-green-700 block font-medium'></Text>
<Text className='text-xs text-green-600 block mt-1 leading-relaxed'>
VIP会员
</Text>
</View>
</View>
{/* Tab 栏 */}
<View className='bg-white mx-3 mt-3 rounded-xl flex'>
{TABS.map(tab => (
<View
key={tab.key}
className={`flex-1 text-center py-3 ${activeTab === tab.key ? 'border-b-2 border-green-500' : ''}`}
onClick={() => setActiveTab(tab.key)}
>
<Text
className={`text-sm ${activeTab === tab.key ? 'text-green-500 font-medium' : 'text-gray-500'}`}
>
{tab.label}
</Text>
</View>
))}
</View>
{/* 列表 */}
<ScrollView scrollY style={{ height: 'calc(100vh - 180px)' }}>
{loading && list.length === 0 ? (
<View className='flex justify-center items-center py-20'>
<Text className='text-gray-400'>...</Text>
</View>
) : list.length === 0 ? (
<View className='flex justify-center items-center py-20'>
<Text className='text-gray-400 text-sm'>{activeTab === 'pending' ? '待审核' : activeTab === 'approved' ? '已通过' : '已驳回'}</Text>
</View>
) : (
list.map(renderCard)
)}
<View className='h-6' />
</ScrollView>
</View>
)
}
export default VipReviewPage

View File

@@ -0,0 +1,208 @@
import React, { useState, useEffect } from 'react'
import { View, Text, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useUser } from '@/hooks/useUser'
import { addShopDealerApply, listShopDealerApply } from '@/api/shop/shopDealerApply'
import type { ShopDealerApply } from '@/api/shop/shopDealerApply/model'
definePageConfig({
navigationBarTitleText: '升级VIP会员',
})
const VipUpgradePage: React.FC = () => {
const { user, isLoggedIn } = useUser()
const [realName, setRealName] = useState('')
const [address, setAddress] = useState('')
const [loading, setLoading] = useState(false)
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
useEffect(() => {
if (!isLoggedIn) {
Taro.navigateTo({ url: '/passport/login' })
return
}
if (user) {
// 门店名称和地址统一从 shop-dealer-apply 表读取
listShopDealerApply({ userId: (user as any)?.userId || (user as any)?.id })
.then(data => {
if (!data || data.length === 0) return
// 取最新一条申请记录(按 applyId 倒序)
const latest = [...data].sort((a, b) => (b.applyId || 0) - (a.applyId || 0))[0]
if (latest) {
setExistingApply(latest)
setRealName(latest.realName || '')
setAddress(latest.address || '')
}
})
.catch(() => {})
}
}, [user, isLoggedIn])
const handleSave = async () => {
if (!realName.trim()) {
Taro.showToast({ title: '请输入门店名称', icon: 'none' })
return
}
if (!address.trim()) {
Taro.showToast({ title: '请输入门店地址', icon: 'none' })
return
}
setLoading(true)
try {
await addShopDealerApply({
userId: (user as any)?.userId || (user as any)?.id,
realName: realName.trim(),
address: address.trim(),
applyType: 10, // 需后台审核
applyStatus: 10, // 待审核
} as ShopDealerApply)
Taro.showToast({ title: '提交成功,等待审核', icon: 'none' })
setTimeout(() => {
Taro.navigateBack()
}, 1500)
} catch (error) {
console.error('提交失败:', error)
Taro.showToast({ title: '提交失败,请重试', icon: 'none' })
} finally {
setLoading(false)
}
}
// 待审核(10) 和 已通过(20) 时只读展示;已驳回(30) 允许编辑后重新提交
const isReadonly = existingApply?.applyStatus === 10 || existingApply?.applyStatus === 20
if (!isLoggedIn) return null
return (
<View className='min-h-screen bg-gray-50'>
{/* 顶部渐变背景 */}
<View className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
style={{ background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)' }}
>
<View className='absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10' style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
<View className='absolute -bottom-6 -left-6 w-24 h-24 rounded-full opacity-15' style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
<View className='relative z-10 flex flex-col items-center'>
<View className='w-16 h-16 rounded-full bg-white bg-opacity-20 flex items-center justify-center mb-3'>
<Text className='text-3xl'>👑</Text>
</View>
<Text className='text-white text-xl font-bold'>VIP会员</Text>
<Text className='text-white text-opacity-80 text-sm mt-1'>
{existingApply?.applyStatus === 20 ? '恭喜您已是VIP会员' : '填写门店信息申请VIP会员资格'}
</Text>
</View>
</View>
{/* 状态提示 */}
{existingApply && (
<View className='mx-3 -mt-6 bg-white rounded-xl p-4 shadow-sm relative z-20 mb-3'>
<View className='flex items-start gap-3'>
<View className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
existingApply.applyStatus === 10 ? 'bg-amber-100' : 'bg-green-100'
}`}>
<Text className='text-sm'>{existingApply.applyStatus === 10 ? '⏳' : '✅'}</Text>
</View>
<View className='flex-1'>
<Text className='text-gray-800 text-sm font-medium block'>
{existingApply.applyStatus === 10 ? '审核中' : '审核通过'}
</Text>
<Text className='text-gray-500 text-xs mt-1 block leading-relaxed'>
{existingApply.applyStatus === 10
? '您的VIP申请正在审核中请耐心等待。审核结果将通过消息通知您。'
: '恭喜您的VIP会员申请已审核通过现在可以享受全部VIP专属权益。'}
</Text>
</View>
</View>
</View>
)}
{/* 说明卡片 */}
{!existingApply && (
<View className='mx-3 -mt-6 bg-white rounded-xl p-4 shadow-sm relative z-20'>
<View className='flex items-start gap-3'>
<View className='w-8 h-8 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0'>
<Text className='text-sm'>💡</Text>
</View>
<View className='flex-1'>
<Text className='text-gray-800 text-sm font-medium block'></Text>
<Text className='text-gray-500 text-xs mt-1 block leading-relaxed'>
VIP会员专属权益
</Text>
</View>
</View>
</View>
)}
{/* 表单 */}
<View className={`${existingApply ? 'mt-3' : ''} mx-3 mt-3 bg-white rounded-xl overflow-hidden`}>
{/* 门店名称 */}
<View className='px-4 py-4 border-b border-gray-50'>
<Text className='text-sm text-gray-700 font-medium block mb-2'> <Text className='text-red-500'>*</Text></Text>
<Input
className='w-full text-sm text-gray-800 bg-gray-50 rounded-lg px-3 py-3'
placeholder='请输入您的门店名称'
value={realName}
onInput={(e: any) => setRealName(e.detail.value)}
maxlength={50}
disabled={isReadonly}
/>
</View>
{/* 门店地址 */}
<View className='px-4 py-4'>
<Text className='text-sm text-gray-700 font-medium block mb-2'> <Text className='text-red-500'>*</Text></Text>
<Input
className='w-full text-sm text-gray-800 bg-gray-50 rounded-lg px-3 py-3'
placeholder='请输入您的门店详细地址'
value={address}
onInput={(e: any) => setAddress(e.detail.value)}
maxlength={100}
disabled={isReadonly}
/>
</View>
</View>
{/* VIP权益预览 */}
<View className='mx-3 mt-4'>
<Text className='text-base font-medium text-gray-800 mb-3 block'>VIP会员专属权益</Text>
<View className='grid grid-cols-2 gap-3'>
{[
{ icon: '💎', title: '更高佣金', desc: '享受高额团队佣金' },
{ icon: '🎁', title: '专属折扣', desc: '购物享VIP专属价' },
{ icon: '⚡', title: '优先发货', desc: '订单优先处理配送' },
{ icon: '🎧', title: '专属客服', desc: '一对一VIP服务' },
].map(item => (
<View key={item.title} className='bg-white rounded-xl p-4 flex items-center gap-3'>
<View className='w-10 h-10 rounded-full bg-green-50 flex items-center justify-center flex-shrink-0'>
<Text className='text-lg'>{item.icon}</Text>
</View>
<View className='flex-1 min-w-0'>
<Text className='text-gray-800 text-sm font-medium block'>{item.title}</Text>
<Text className='text-gray-400 text-xs mt-0.5 block truncate'>{item.desc}</Text>
</View>
</View>
))}
</View>
</View>
{/* 提交按钮 */}
{!existingApply && (
<View className='mx-3 mt-6 mb-4'>
<View
className='w-full py-4 rounded-xl flex items-center justify-center active:opacity-90'
style={{ background: loading ? '#a3e635' : 'linear-gradient(135deg, #16a34a, #22c55e)' }}
onClick={loading ? undefined : handleSave}
>
<Text className='text-white text-base font-semibold'>
{loading ? '提交中...' : '提交申请'}
</Text>
</View>
<Text className='text-gray-400 text-xs mt-2 text-center block'>
1-3
</Text>
</View>
)}
</View>
)
}
export default VipUpgradePage