feat(user): 更新用户界面和功能实现- 默认 修改 UnifiedQRButton类型为 danger- 更新 Banner 组件使用 getCmsAdByCode 获取广告数据

- 新增 CMS 文章查询接口 getCmsArticleByCode
- 调整 UserCard 组件界面样式和逻辑-优化 BestSellers 商品展示组件
- 更新 IsDealer 组件支持网站字段配置
- 移除用户页面部分冗余代码和样式
- 增加主题样式支持和背景装饰元素
- 调整用户相关组件层级和定位样式
This commit is contained in:
2025-09-26 11:22:09 +08:00
parent 6505b48d3a
commit 915c06ecab
30 changed files with 1190 additions and 294 deletions

View File

@@ -0,0 +1,277 @@
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
import {View, Text, Image} 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";
const UserCard = forwardRef<any, any>((_, ref) => {
const {data, refresh} = useUserData()
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
// 下拉刷新
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)
// 获取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
// 获取存储的邀请参数
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)
}
})
} else {
console.log('登录失败!')
}
}
})
}
return (
<View className={'header-bg pt-20'}>
<View className={'p-4'}>
{/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */}
<View className="relative">
<View
className={'user-card w-full flex flex-col justify-around rounded-xl'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)',
height: '170px',
}}
>
<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 || 'https://oss.wsdns.cn/20250623/62f830b85edb4a7293b8948c25e6f987.jpeg'}
shape="round"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large"
src={userInfo?.avatar || 'https://oss.wsdns.cn/20250623/62f830b85edb4a7293b8948c25e6f987.jpeg'}
shape="round"/>
</Button>
)
}
<View className={'user-info flex flex-col px-2'}>
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
{IsLogin ? (
<View className={'grade text-xs py-1'}>
<Tag type="success" round>
<Text className={'p-1'}>
{getRoleName()}
</Text>
</Tag>
</View>
) : ''}
</View>
</View>
<Space style={{
marginTop: '30px',
marginRight: '10px'
}}>
{/*统一扫码入口 - 支持登录和核销*/}
<UnifiedQRButton
text="扫码"
size="small"
onSuccess={(result) => {
console.log('统一扫码成功:', result);
// 根据扫码类型给出不同的提示
if (result.type === 'verification') {
// 核销成功,可以显示更多信息或跳转到详情页
Taro.showModal({
title: '核销成功',
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
});
}
}}
onError={(error) => {
console.error('统一扫码失败:', error);
}}
/>
</Space>
</View>
<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-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)}>
<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)}>
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.giftCards || 0}</Text>
</View>
</View>
</View>
{/* 个人资料图片,定位在右上角 */}
<View
className="absolute top-0 right-0 overflow-hidden"
style={{
borderRadius: "0 0.75rem 0 0"
}}
onClick={() => navTo('/user/profile/profile', true)}
>
<Image
src="https://oss.wsdns.cn/20250913/7c3de38b377344b89131aba40214f63f.png"
style={{
width: "200rpx"
}}
mode="widthFix"
/>
</View>
</View>
</View>
</View>
)
})
export default UserCard;