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