Browse Source

feat(user): 添加用户数据钩子和仪表板接口

- 新增 useUserData 钩子用于获取用户数据
- 添加用户仪表板相关接口和类型定义
- 更新用户卡片组件,使用新的用户数据钩子
- 修改成为经销商文案为开通VIP
dev
科技小王子 7 days ago
parent
commit
1fba71c8b3
  1. 1
      src/api/shop/shopUserCoupon/model/index.ts
  2. 131
      src/api/user/index.ts
  3. 118
      src/hooks/useUserData.ts
  4. 2
      src/pages/user/components/IsDealer.tsx
  5. 86
      src/pages/user/components/UserCard.tsx

1
src/api/shop/shopUserCoupon/model/index.ts

@ -67,6 +67,7 @@ export interface ShopUserCoupon {
*/
export interface ShopUserCouponParam extends PageParam {
id?: number;
userId?: number;
status?: number;
isExpire?: number;
sortBy?: string;

131
src/api/user/index.ts

@ -0,0 +1,131 @@
import request from '@/utils/request-legacy'
import type { ApiResult } from '@/api/index'
// 用户余额信息
export interface UserBalance {
balance: string
frozenBalance: string
totalIncome: string
totalExpense: string
}
// 用户积分信息
export interface UserPoints {
points: number
totalEarned: number
totalUsed: number
expiringSoon: number
}
// 用户优惠券统计
export interface UserCoupons {
count: number
available: number
used: number
expired: number
}
// 用户礼品卡统计
export interface UserGiftCards {
count: number
unused: number
used: number
expired: number
}
// 用户订单统计
export interface UserOrderStats {
pending: number
paid: number
shipped: number
completed: number
refund: number
total: number
}
// 用户完整数据
export interface UserDashboard {
balance: UserBalance
points: UserPoints
coupons: UserCoupons
giftCards: UserGiftCards
orders: UserOrderStats
lastUpdateTime: string
}
/**
*
*/
export async function getUserBalance() {
const res = await request.get<ApiResult<UserBalance>>('/user/balance')
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
*
*/
export async function getUserPoints() {
const res = await request.get<ApiResult<UserPoints>>('/user/points')
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
*
*/
export async function getUserCoupons() {
const res = await request.get<ApiResult<UserCoupons>>('/user/coupons/stats')
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
*
*/
export async function getUserGiftCards() {
const res = await request.get<ApiResult<UserGiftCards>>('/user/gift-cards/stats')
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
*
*/
export async function getUserOrderStats() {
const res = await request.get<ApiResult<UserOrderStats>>('/user/orders/stats')
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
*
*/
export async function getUserDashboard() {
const res = await request.get<ApiResult<UserDashboard>>('/user/dashboard')
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
*
*/
export async function refreshUserData() {
const res = await request.post<ApiResult<unknown>>('/user/refresh-cache')
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}

118
src/hooks/useUserData.ts

@ -0,0 +1,118 @@
import { useState, useEffect, useCallback } from 'react'
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
import {pageShopGift} from "@/api/shop/shopGift";
import {useUser} from "@/hooks/useUser";
interface UserData {
balance: number
points: number
coupons: number
giftCards: number
orders: {
pending: number
paid: number
shipped: number
completed: number
refund: number
}
}
interface UseUserDataReturn {
data: UserData | null
loading: boolean
error: string | null
refresh: () => Promise<void>
updateBalance: (newBalance: string) => void
updatePoints: (newPoints: number) => void
}
export const useUserData = (): UseUserDataReturn => {
const {user} = useUser()
const [data, setData] = useState<UserData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// 获取用户数据
const fetchUserData = useCallback(async () => {
try {
setLoading(true)
setError(null)
// 并发请求所有数据
const [couponsRes, giftCardsRes] = await Promise.all([
pageShopUserCoupon({ page: 1, limit: 1, userId: user?.userId}),
pageShopGift({ page: 1, limit: 1, userId: user?.userId, status: 0})
])
const newData: UserData = {
balance: user?.balance || 0.00,
points: user?.points || 0,
coupons: couponsRes?.count || 0,
giftCards: giftCardsRes?.count || 0,
orders: {
pending: 0,
paid: 0,
shipped: 0,
completed: 0,
refund: 0
}
}
setData(newData)
} catch (err) {
setError(err instanceof Error ? err.message : '获取用户数据失败')
} finally {
setLoading(false)
}
}, [])
// 刷新数据
const refresh = useCallback(async () => {
await fetchUserData()
}, [fetchUserData])
// 初始化加载
useEffect(() => {
fetchUserData().then()
}, [fetchUserData])
return {
data,
loading,
error,
refresh: fetchUserData
}
}
// 轻量级版本 - 只获取基础数据
export const useUserBasicData = () => {
const {user} = useUser()
const [balance, setBalance] = useState<number>(0)
const [points, setPoints] = useState<number>(0)
const [loading, setLoading] = useState(false)
const fetchBasicData = useCallback(async () => {
setLoading(true)
try {
setBalance(user?.balance || 0)
setPoints(user?.points || 0)
} catch (error) {
console.error('获取基础数据失败:', error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchBasicData()
}, [fetchBasicData])
return {
balance,
points,
loading,
refresh: fetchBasicData,
updateBalance: setBalance,
updatePoints: setPoints
}
}

2
src/pages/user/components/IsDealer.tsx

@ -107,7 +107,7 @@ const UserCell = () => {
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>VIP</Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}

86
src/pages/user/components/UserCard.tsx

@ -1,5 +1,6 @@
import {Button} from '@nutui/nutui-react-taro'
import {Avatar, Tag} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import {Scan} from '@nutui/icons-react-taro';
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
@ -9,11 +10,13 @@ import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {useUser} from "@/hooks/useUser";
import {useUserData} from "@/hooks/useUserData";
function UserCard() {
const {
isAdmin
} = useUser();
const { data, refresh } = useUserData()
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
@ -21,6 +24,15 @@ function UserCard() {
const [pointsCount, setPointsCount] = useState(0)
const [giftCount, setGiftCount] = useState(0)
// 下拉刷新
const handleRefresh = async () => {
await refresh()
Taro.showToast({
title: '刷新成功',
icon: 'success'
})
}
useEffect(() => {
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
@ -182,9 +194,9 @@ function UserCard() {
}
return (
<div className={'header-bg pt-20'}>
<div className={'p-4'}>
<div
<View className={'header-bg pt-20'}>
<View className={'p-4'}>
<View
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
@ -194,8 +206,8 @@ function UserCard() {
// borderRadius: '22px 22px 0 0',
}}
>
<div className={'user-card-header flex w-full justify-between items-center pt-4'}>
<div className={'flex items-center mx-4'}>
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
<View className={'flex items-center mx-4'}>
{
IsLogin ? (
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
@ -205,49 +217,49 @@ function UserCard() {
</Button>
)
}
<div className={'user-info flex flex-col px-2'}>
<div className={'py-1 text-black font-bold'}>{getDisplayName()}</div>
<View className={'user-info flex flex-col px-2'}>
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
{IsLogin ? (
<div className={'grade text-xs py-1'}>
<View className={'grade text-xs py-1'}>
<Tag type="success" round>
<div className={'p-1'}>
<Text className={'p-1'}>
{getRoleName()}
</div>
</Text>
</Tag>
</div>
</View>
) : ''}
</div>
</div>
</View>
</View>
{isAdmin() && <Scan onClick={() => navTo('/user/store/verification', true)} />}
<div className={'mr-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
<View className={'mr-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
onClick={() => navTo('/user/profile/profile', true)}>
{'个人资料'}
</div>
</div>
<div className={'flex justify-around mt-1'}>
<div className={'item flex justify-center flex-col items-center'}
</View>
</View>
<View className={'flex justify-around mt-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{userInfo?.balance || '0.00'}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{pointsCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.balance || '0.00'}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}>
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.points || 0}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{couponCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.coupons || 0}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{giftCount}</span>
</div>
</div>
</div>
</div>
</div>
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.giftCards || 0}</Text>
</View>
</View>
</View>
</View>
</View>
)
}

Loading…
Cancel
Save