feat(store): 新增门店中心及VIP会员升级功能
- 个人信息页新增门店名称和门店地址展示,支持异步加载与只读显示 - 用户信息卡片整体改为绿色渐变背景,添加光晕和白色边框样式 - 新增升级VIP会员入口,突出展示并添加推荐标签 - 新建VIP会员升级页面,包含门店信息表单和VIP权益预览 - 提交升级申请调用新增api,支持状态检测表单只读 - 门店中心页面重构,简化订单管理,新增功能卡片及VIP审核入口 - 页面加载校验店员身份,非店员拒绝访问并提示 - 购物相关页面和组件全面支持VIP会员价格优先显示dealerPrice - 新增ShopDealerApply模型门店相关字段,丰富会员申请数据记录
This commit is contained in:
@@ -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='保存'
|
||||
|
||||
@@ -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>
|
||||
|
||||
389
src/pages/user/vip-review/index.tsx
Normal file
389
src/pages/user/vip-review/index.tsx
Normal 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
|
||||
208
src/pages/user/vip-upgrade/index.tsx
Normal file
208
src/pages/user/vip-upgrade/index.tsx
Normal 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
|
||||
Reference in New Issue
Block a user