Files
mp-10550/src/pages/user/components/UserCard.tsx
赵忠林 f96918bf86 feat(ticket): 添加水票功能支持
- 在订单模型中增加formId字段用于标识商品ID
- 更新统一扫码组件以支持水票和礼品卡核销
- 实现水票列表页面,包含我的水票和核销记录两个标签页
- 添加水票核销二维码生成功能
- 支持水票的分页加载和搜索功能
- 实现水票核销记录的展示
- 添加水票状态变更历史追踪
- 更新订单状态判断逻辑以支持特定商品完成状态
- 扩展扫码验证功能以处理水票业务类型
2026-02-04 11:00:54 +08:00

296 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
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 themeStyles = useThemeStyles();
// 角色名称:优先取用户 roles 数组的第一个角色名称
const getRoleName = () => {
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
}
// 下拉刷新
const handleRefresh = async () => {
await refresh()
Taro.showToast({
title: '刷新成功',
icon: 'success'
})
}
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
handleRefresh
}))
useEffect(() => {
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户已经授权过,可以直接获取用户信息
console.log('用户已经授权过,可以直接获取用户信息')
reload();
} else {
// 用户未授权,需要弹出授权窗口
console.log('用户未授权,需要弹出授权窗口')
showAuthModal();
}
}
});
}, []);
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()
// 获取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()
}
})
} 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}>{data?.giftCards || 0}</Text>
</View>
</View>
</View>
</View>
</View>
</View>
)
})
export default UserCard;