feat(user): 添加用户数据钩子和仪表板接口
- 新增 useUserData 钩子用于获取用户数据 - 添加用户仪表板相关接口和类型定义 - 更新用户卡片组件,使用新的用户数据钩子 - 修改成为经销商文案为开通VIP
This commit is contained in:
@@ -67,6 +67,7 @@ export interface ShopUserCoupon {
|
|||||||
*/
|
*/
|
||||||
export interface ShopUserCouponParam extends PageParam {
|
export interface ShopUserCouponParam extends PageParam {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
userId?: number;
|
||||||
status?: number;
|
status?: number;
|
||||||
isExpire?: number;
|
isExpire?: number;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
|
|||||||
131
src/api/user/index.ts
Normal file
131
src/api/user/index.ts
Normal file
@@ -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
Normal file
118
src/hooks/useUserData.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ const UserCell = () => {
|
|||||||
title={
|
title={
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Reward className={'text-orange-100 '} size={16}/>
|
<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>
|
<Text className={'text-white opacity-80 pl-3'}>享优惠</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {Button} from '@nutui/nutui-react-taro'
|
import {Button} from '@nutui/nutui-react-taro'
|
||||||
import {Avatar, Tag} 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 {Scan} from '@nutui/icons-react-taro';
|
||||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
@@ -9,11 +10,13 @@ import navTo from "@/utils/common";
|
|||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
|
import {useUserData} from "@/hooks/useUserData";
|
||||||
|
|
||||||
function UserCard() {
|
function UserCard() {
|
||||||
const {
|
const {
|
||||||
isAdmin
|
isAdmin
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { data, refresh } = useUserData()
|
||||||
const {getDisplayName, getRoleName} = useUser();
|
const {getDisplayName, getRoleName} = useUser();
|
||||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||||
const [userInfo, setUserInfo] = useState<User>()
|
const [userInfo, setUserInfo] = useState<User>()
|
||||||
@@ -21,6 +24,15 @@ function UserCard() {
|
|||||||
const [pointsCount, setPointsCount] = useState(0)
|
const [pointsCount, setPointsCount] = useState(0)
|
||||||
const [giftCount, setGiftCount] = useState(0)
|
const [giftCount, setGiftCount] = useState(0)
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
await refresh()
|
||||||
|
Taro.showToast({
|
||||||
|
title: '刷新成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||||
Taro.getSetting({
|
Taro.getSetting({
|
||||||
@@ -182,9 +194,9 @@ function UserCard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'header-bg pt-20'}>
|
<View className={'header-bg pt-20'}>
|
||||||
<div className={'p-4'}>
|
<View className={'p-4'}>
|
||||||
<div
|
<View
|
||||||
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
|
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
|
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
|
||||||
@@ -194,8 +206,8 @@ function UserCard() {
|
|||||||
// borderRadius: '22px 22px 0 0',
|
// borderRadius: '22px 22px 0 0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
||||||
<div className={'flex items-center mx-4'}>
|
<View className={'flex items-center mx-4'}>
|
||||||
{
|
{
|
||||||
IsLogin ? (
|
IsLogin ? (
|
||||||
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
|
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
|
||||||
@@ -205,49 +217,49 @@ function UserCard() {
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<div className={'user-info flex flex-col px-2'}>
|
<View className={'user-info flex flex-col px-2'}>
|
||||||
<div className={'py-1 text-black font-bold'}>{getDisplayName()}</div>
|
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
|
||||||
{IsLogin ? (
|
{IsLogin ? (
|
||||||
<div className={'grade text-xs py-1'}>
|
<View className={'grade text-xs py-1'}>
|
||||||
<Tag type="success" round>
|
<Tag type="success" round>
|
||||||
<div className={'p-1'}>
|
<Text className={'p-1'}>
|
||||||
{getRoleName()}
|
{getRoleName()}
|
||||||
</div>
|
</Text>
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</View>
|
||||||
) : ''}
|
) : ''}
|
||||||
</div>
|
</View>
|
||||||
</div>
|
</View>
|
||||||
{isAdmin() && <Scan onClick={() => navTo('/user/store/verification', true)} />}
|
{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)}>
|
onClick={() => navTo('/user/profile/profile', true)}>
|
||||||
{'个人资料'}
|
{'个人资料'}
|
||||||
</div>
|
</View>
|
||||||
</div>
|
</View>
|
||||||
<div className={'flex justify-around mt-1'}>
|
<View className={'flex justify-around mt-1'}>
|
||||||
<div className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||||
<span className={'text-sm text-gray-500'}>余额</span>
|
<Text className={'text-sm text-gray-500'}>余额</Text>
|
||||||
<span className={'text-xl'}>{userInfo?.balance || '0.00'}</span>
|
<Text className={'text-xl'}>{data?.balance || '0.00'}</Text>
|
||||||
</div>
|
</View>
|
||||||
<div className={'item flex justify-center flex-col items-center'}>
|
<View className={'item flex justify-center flex-col items-center'}>
|
||||||
<span className={'text-sm text-gray-500'}>积分</span>
|
<Text className={'text-sm text-gray-500'}>积分</Text>
|
||||||
<span className={'text-xl'}>{pointsCount}</span>
|
<Text className={'text-xl'}>{data?.points || 0}</Text>
|
||||||
</div>
|
</View>
|
||||||
<div className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/coupon/index', true)}>
|
onClick={() => navTo('/user/coupon/index', true)}>
|
||||||
<span className={'text-sm text-gray-500'}>优惠券</span>
|
<Text className={'text-sm text-gray-500'}>优惠券</Text>
|
||||||
<span className={'text-xl'}>{couponCount}</span>
|
<Text className={'text-xl'}>{data?.coupons || 0}</Text>
|
||||||
</div>
|
</View>
|
||||||
<div className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/gift/index', true)}>
|
onClick={() => navTo('/user/gift/index', true)}>
|
||||||
<span className={'text-sm text-gray-500'}>礼品卡</span>
|
<Text className={'text-sm text-gray-500'}>礼品卡</Text>
|
||||||
<span className={'text-xl'}>{giftCount}</span>
|
<Text className={'text-xl'}>{data?.giftCards || 0}</Text>
|
||||||
</div>
|
</View>
|
||||||
</div>
|
</View>
|
||||||
</div>
|
</View>
|
||||||
</div>
|
</View>
|
||||||
</div>
|
</View>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user