Files
mp-10550/src/pages/user/components/UserCard.tsx
赵忠林 cbcf591f71 feat(admin): 实现管理员模式切换和扫码登录功能
- 新增管理员模式切换方案,统一管理所有管理员功能
- 实现扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录
- 添加管理员面板组件,集中展示所有管理员功能
- 开发扫码登录按钮和扫描器组件,方便集成到不同页面
- 优化用户界面设计,提高管理员用户的使用体验
2025-09-01 14:26:00 +08:00

306 lines
10 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 {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';
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 {useQRLogin} from "@/hooks/useQRLogin";
import {useAdminMode} from "@/hooks/useAdminMode";
import AdminPanel from "@/components/AdminPanel";
const UserCard = forwardRef<any, any>((_, ref) => {
const {
isAdmin
} = useUser();
const { data, refresh } = useUserData()
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
// 扫码登录Hook
const { startScan, isLoading: isScanLoading } = useQRLogin();
// 管理员模式Hook
const { isAdminMode, toggleAdminMode } = useAdminMode();
// 管理员面板显示状态
const [showAdminPanel, setShowAdminPanel] = useState(false);
// 下拉刷新
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('登录失败!')
}
}
})
}
// 处理扫码登录
const handleQRLogin = async () => {
try {
await startScan();
} catch (error) {
console.error('扫码登录失败:', error);
}
};
// 打开管理员面板
const handleOpenAdminPanel = () => {
setShowAdminPanel(true);
};
// 关闭管理员面板
const handleCloseAdminPanel = () => {
setShowAdminPanel(false);
};
return (
<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)', // 这种情况建议使用类名来控制样式(引入外联样式)
// width: '720rpx',
// margin: '10px auto 0px auto',
height: '170px',
// borderRadius: '22px 22px 0 0',
}}
>
<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"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large" src={userInfo?.avatar} 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>
{isAdmin() && (
<View className="flex items-center space-x-2">
{/* 管理员模式切换按钮 */}
<View
className={`px-3 py-1 rounded-full text-xs font-medium transition-all ${
isAdminMode
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-600 border border-gray-300'
}`}
onClick={toggleAdminMode}
>
{isAdminMode ? '管理员' : '普通用户'}
</View>
{/* 管理员功能入口 */}
{isAdminMode && (
<View
className="px-3 py-1 bg-blue-50 border border-blue-200 rounded-full text-xs text-blue-600 font-medium"
onClick={handleOpenAdminPanel}
>
</View>
)}
</View>
)}
<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)}>
{'个人资料'}
</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)}>
<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>
{/* 管理员面板 */}
{isAdmin() && (
<AdminPanel
visible={showAdminPanel}
onClose={handleCloseAdminPanel}
/>
)}
</View>
)
})
export default UserCard;