forked from gxwebsoft/mp-10550
- 在水票核销页面添加自动扫描模式支持 - 添加路由参数检测实现自动开启扫码功能 - 添加首次展示时自动触发扫码逻辑 - 修改用户卡片组件显示实际水票总数而非礼品卡数量 - 添加独立的水票总数刷新机制 - 在用户登录和信息刷新时同步更新水票总数
318 lines
12 KiB
TypeScript
318 lines
12 KiB
TypeScript
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
|
||
import {View, Text} from '@tarojs/components'
|
||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||
import Taro from '@tarojs/taro';
|
||
import {useEffect, useState, forwardRef, useImperativeHandle} from "react";
|
||
import {User} from "@/api/system/user/model";
|
||
import navTo from "@/utils/common";
|
||
import {TenantId} from "@/config/app";
|
||
import {useUser} from "@/hooks/useUser";
|
||
import {useUserData} from "@/hooks/useUserData";
|
||
import {getStoredInviteParams} from "@/utils/invite";
|
||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||
import {useThemeStyles} from "@/hooks/useTheme";
|
||
import {getRootDomain} from "@/utils/domain";
|
||
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||
|
||
const UserCard = forwardRef<any, any>((_, ref) => {
|
||
const {data, refresh} = useUserData()
|
||
const {getDisplayName} = useUser();
|
||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||
const [userInfo, setUserInfo] = useState<User>()
|
||
const [ticketTotal, setTicketTotal] = useState<number>(0)
|
||
|
||
const themeStyles = useThemeStyles();
|
||
|
||
// 角色名称:优先取用户 roles 数组的第一个角色名称
|
||
const getRoleName = () => {
|
||
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
|
||
}
|
||
|
||
// 下拉刷新
|
||
const handleRefresh = async () => {
|
||
await refresh()
|
||
reloadTicketTotal()
|
||
Taro.showToast({
|
||
title: '刷新成功',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
|
||
// 暴露方法给父组件
|
||
useImperativeHandle(ref, () => ({
|
||
handleRefresh
|
||
}))
|
||
|
||
useEffect(() => {
|
||
// 独立于用户信息授权:只要有登录 token,就可以拉取水票总数
|
||
reloadTicketTotal()
|
||
|
||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||
Taro.getSetting({
|
||
success: (res) => {
|
||
if (res.authSetting['scope.userInfo']) {
|
||
// 用户已经授权过,可以直接获取用户信息
|
||
console.log('用户已经授权过,可以直接获取用户信息')
|
||
reload();
|
||
} else {
|
||
// 用户未授权,需要弹出授权窗口
|
||
console.log('用户未授权,需要弹出授权窗口')
|
||
showAuthModal();
|
||
}
|
||
}
|
||
});
|
||
}, []);
|
||
|
||
const reloadTicketTotal = () => {
|
||
const token = Taro.getStorageSync('access_token')
|
||
if (!token) {
|
||
setTicketTotal(0)
|
||
return
|
||
}
|
||
getMyGltUserTicketTotal()
|
||
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
|
||
.catch((err) => {
|
||
console.error('个人中心水票总数加载失败:', err)
|
||
setTicketTotal(0)
|
||
})
|
||
}
|
||
|
||
const reload = () => {
|
||
Taro.getUserInfo({
|
||
success: (res) => {
|
||
const avatar = res.userInfo.avatarUrl;
|
||
setUserInfo({
|
||
avatar,
|
||
nickname: res.userInfo.nickName,
|
||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||
})
|
||
getUserInfo().then((data) => {
|
||
if (data) {
|
||
setUserInfo(data)
|
||
setIsLogin(true);
|
||
Taro.setStorageSync('UserId', data.userId)
|
||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||
refresh().then()
|
||
reloadTicketTotal()
|
||
|
||
// 获取openId
|
||
if (!data.openid) {
|
||
Taro.login({
|
||
success: (res) => {
|
||
getWxOpenId({code: res.code}).then(() => {
|
||
})
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}).catch(() => {
|
||
console.log('未登录')
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
const showAuthModal = () => {
|
||
Taro.showModal({
|
||
title: '授权提示',
|
||
content: '需要获取您的用户信息',
|
||
confirmText: '去授权',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// 用户点击确认,打开授权设置页面
|
||
openSetting();
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
const openSetting = () => {
|
||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||
Taro.openSetting({
|
||
success: (res) => {
|
||
if (res.authSetting['scope.userInfo']) {
|
||
// 用户授权成功,可以获取用户信息
|
||
reload();
|
||
} else {
|
||
// 用户拒绝授权,提示授权失败
|
||
Taro.showToast({
|
||
title: '授权失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
/* 获取用户手机号 */
|
||
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
|
||
const {code, encryptedData, iv} = detail
|
||
|
||
// 判断用户是否已登录
|
||
if(IsLogin){
|
||
return navTo(`/user/profile/profile`)
|
||
}
|
||
|
||
// 获取存储的邀请参数
|
||
const inviteParams = getStoredInviteParams()
|
||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||
|
||
Taro.login({
|
||
success: function () {
|
||
if (code) {
|
||
Taro.request({
|
||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||
method: 'POST',
|
||
data: {
|
||
code,
|
||
encryptedData,
|
||
iv,
|
||
notVerifyPhone: true,
|
||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||
sceneType: 'save_referee',
|
||
tenantId: TenantId
|
||
},
|
||
header: {
|
||
'content-type': 'application/json',
|
||
TenantId
|
||
},
|
||
success: function (res) {
|
||
if (res.data.code == 1) {
|
||
Taro.showToast({
|
||
title: res.data.message,
|
||
icon: 'error',
|
||
duration: 2000
|
||
})
|
||
return false;
|
||
}
|
||
// 登录成功
|
||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
||
setUserInfo(res.data.data.user)
|
||
setIsLogin(true)
|
||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||
refresh().then()
|
||
reloadTicketTotal()
|
||
}
|
||
})
|
||
} else {
|
||
console.log('登录失败!')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
return (
|
||
<View className={'pt-14'}>
|
||
{/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */}
|
||
<View className="relative z-20">
|
||
<View
|
||
className={'user-card w-full flex flex-col justify-around rounded-xl'}
|
||
>
|
||
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
||
<View className={'flex items-center mx-4'}>
|
||
{IsLogin && (
|
||
<View style={{color: '#ffffff', height: 'auto'}} onClick={() => navTo(`/user/profile/profile`)}>
|
||
<View className={'flex items-center gap-2'}>
|
||
<Avatar
|
||
size={'large'}
|
||
src={userInfo?.avatar || ''}
|
||
/>
|
||
<View className={'flex flex-col'}>
|
||
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
|
||
{getRootDomain() && (
|
||
<View><Tag type="success">{getRoleName()}</Tag></View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
{!IsLogin && (
|
||
<Button style={{color: '#ffffff', height: 'auto'}} open-type="getPhoneNumber"
|
||
onGetPhoneNumber={handleGetPhoneNumber}>
|
||
<View className={'flex items-center gap-2'}>
|
||
<Avatar
|
||
size={'large'}
|
||
src={userInfo?.avatar || ''}
|
||
/>
|
||
<View className={'flex flex-col'}>
|
||
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
|
||
</View>
|
||
</View>
|
||
</Button>
|
||
)}
|
||
</View>
|
||
<Space style={{
|
||
marginTop: '30px',
|
||
marginRight: '10px'
|
||
}}>
|
||
{/*统一扫码入口 - 支持登录和核销*/}
|
||
<UnifiedQRButton
|
||
text="扫一扫"
|
||
size="small"
|
||
onSuccess={(result) => {
|
||
console.log('统一扫码成功:', result);
|
||
// 根据扫码类型给出不同的提示
|
||
if (result.type === 'verification') {
|
||
const businessType = result?.data?.businessType;
|
||
if (businessType === 'gift' && result?.data?.gift) {
|
||
const gift = result.data.gift;
|
||
Taro.showModal({
|
||
title: '核销成功',
|
||
content: `已成功核销:${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}`
|
||
});
|
||
return;
|
||
}
|
||
if (businessType === 'ticket' && result?.data?.ticket) {
|
||
const ticket = result.data.ticket;
|
||
const qty = result.data.qty || 1;
|
||
Taro.showModal({
|
||
title: '核销成功',
|
||
content: `已成功核销:${ticket.templateName || '水票'},本次使用${qty}次,剩余可用${ticket.availableQty ?? 0}次`
|
||
});
|
||
return;
|
||
}
|
||
Taro.showModal({
|
||
title: '核销成功',
|
||
content: '已成功核销'
|
||
});
|
||
}
|
||
}}
|
||
onError={(error) => {
|
||
console.error('统一扫码失败:', error);
|
||
}}
|
||
/>
|
||
</Space>
|
||
</View>
|
||
<View className={'py-2'}>
|
||
<View className={'flex justify-around mt-1'}>
|
||
<View className={'item flex justify-center flex-col items-center'}
|
||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>余额</Text>
|
||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.balance || '0.00'}</Text>
|
||
</View>
|
||
<View className={'item flex justify-center flex-col items-center'}>
|
||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>积分</Text>
|
||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.points || 0}</Text>
|
||
</View>
|
||
<View className={'item flex justify-center flex-col items-center'}
|
||
onClick={() => navTo('/user/coupon/index', true)}>
|
||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>优惠券</Text>
|
||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
||
</View>
|
||
<View className={'item flex justify-center flex-col items-center'}
|
||
onClick={() => navTo('/user/gift/index', true)}>
|
||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>
|
||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)
|
||
})
|
||
|
||
export default UserCard;
|