feat(user): 优化用户权限管理与扫码功能

- 添加 isAdmin 状态检查逻辑支持多种数据类型 (true/1/'1')
- 实现统一扫码按钮的管理员权限控制,仅管理员可查看
- 集成 saveStorageByLoginUser 工具函数统一处理登录用户信息存储
- 优化扫码取消操作的错误处理,区分用户主动取消与实际错误
- 同步本地存储中的用户信息以便其他钩子读取管理员标识
This commit is contained in:
2026-02-06 02:29:02 +08:00
parent 5bddf6e438
commit c0954564a6
3 changed files with 66 additions and 43 deletions

View File

@@ -271,7 +271,14 @@ export function useUnifiedQRScan() {
} }
}, },
fail: (err) => { fail: (err) => {
reject(new Error(err.errMsg || '扫码失败')); const msg = (err as any)?.errMsg || '';
// `scanCode:fail cancel` is a user-driven cancel; don't treat it as an error toast.
if (typeof msg === 'string' && msg.toLowerCase().includes('cancel')) {
cancelRef.current = true;
reject(new Error('取消扫码'));
return;
}
reject(new Error(msg || '扫码失败'));
} }
}); });
}); });
@@ -323,6 +330,11 @@ export function useUnifiedQRScan() {
return result; return result;
} catch (err: any) { } catch (err: any) {
// User cancelled scanning (e.g. `scanCode:fail cancel`).
if (cancelRef.current) {
reset();
return null;
}
if (!cancelRef.current) { if (!cancelRef.current) {
setState(UnifiedScanState.ERROR); setState(UnifiedScanState.ERROR);
const errorMessage = err.message || '处理失败'; const errorMessage = err.message || '处理失败';

View File

@@ -280,11 +280,14 @@ export const useUser = () => {
// 检查用户是否是管理员 // 检查用户是否是管理员
const isAdmin = () => { const isAdmin = () => {
return user?.isAdmin === true; // Some backends use `1/0` (or `1/2`) instead of boolean.
const v: any = (user as any)?.isAdmin;
return v === true || v === 1 || v === '1';
}; };
const isSuperAdmin = () => { const isSuperAdmin = () => {
return user?.isSuperAdmin === true; const v: any = (user as any)?.isSuperAdmin;
return v === true || v === 1 || v === '1';
}; };
// 获取用户余额 // 获取用户余额

View File

@@ -13,15 +13,20 @@ import UnifiedQRButton from "@/components/UnifiedQRButton";
import {useThemeStyles} from "@/hooks/useTheme"; import {useThemeStyles} from "@/hooks/useTheme";
import {getRootDomain} from "@/utils/domain"; import {getRootDomain} from "@/utils/domain";
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket' import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
import { saveStorageByLoginUser } from '@/utils/server'
const UserCard = forwardRef<any, any>((_, ref) => { const UserCard = forwardRef<any, any>((_, ref) => {
const {data, refresh} = useUserData() const {data, refresh} = useUserData()
const {getDisplayName} = useUser(); const {getDisplayName, isAdmin} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false) const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>() const [userInfo, setUserInfo] = useState<User>()
const [ticketTotal, setTicketTotal] = useState<number>(0) const [ticketTotal, setTicketTotal] = useState<number>(0)
const themeStyles = useThemeStyles(); const themeStyles = useThemeStyles();
const canShowScanButton = (() => {
const v: any = (userInfo as any)?.isAdmin
return isAdmin() || v === true || v === 1 || v === '1'
})()
// 角色名称:优先取用户 roles 数组的第一个角色名称 // 角色名称:优先取用户 roles 数组的第一个角色名称
const getRoleName = () => { const getRoleName = () => {
@@ -96,6 +101,8 @@ const UserCard = forwardRef<any, any>((_, ref) => {
if (data) { if (data) {
setUserInfo(data) setUserInfo(data)
setIsLogin(true); setIsLogin(true);
// Keep local storage user info in sync so other hooks (e.g. unified scan) can read admin flags.
Taro.setStorageSync('User', data)
Taro.setStorageSync('UserId', data.userId) Taro.setStorageSync('UserId', data.userId)
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票) // 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
refresh().then() refresh().then()
@@ -193,8 +200,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
return false; return false;
} }
// 登录成功 // 登录成功
Taro.setStorageSync('access_token', res.data.data.access_token) saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
Taro.setStorageSync('UserId', res.data.data.user.userId)
setUserInfo(res.data.data.user) setUserInfo(res.data.data.user)
setIsLogin(true) setIsLogin(true)
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票) // 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
@@ -249,47 +255,49 @@ const UserCard = forwardRef<any, any>((_, ref) => {
</Button> </Button>
)} )}
</View> </View>
<Space style={{ {/*统一扫码入口 - 仅管理员可见*/}
marginTop: '30px', {canShowScanButton && (
marginRight: '10px' <Space style={{
}}> marginTop: '30px',
{/*统一扫码入口 - 支持登录和核销*/} marginRight: '10px'
<UnifiedQRButton }}>
text="扫一扫" <UnifiedQRButton
size="small" text="扫一扫"
onSuccess={(result) => { size="small"
console.log('统一扫码成功:', result); onSuccess={(result) => {
// 根据扫码类型给出不同的提示 console.log('统一扫码成功:', result);
if (result.type === 'verification') { // 根据扫码类型给出不同的提示
const businessType = result?.data?.businessType; if (result.type === 'verification') {
if (businessType === 'gift' && result?.data?.gift) { const businessType = result?.data?.businessType;
const gift = result.data.gift; 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({ Taro.showModal({
title: '核销成功', title: '核销成功',
content: `已成功核销${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}` content: '已成功核销'
}); });
return;
} }
if (businessType === 'ticket' && result?.data?.ticket) { }}
const ticket = result.data.ticket; onError={(error) => {
const qty = result.data.qty || 1; console.error('统一扫码失败:', error);
Taro.showModal({ }}
title: '核销成功', />
content: `已成功核销:${ticket.templateName || '水票'},本次使用${qty}次,剩余可用${ticket.availableQty ?? 0}` </Space>
}); )}
return;
}
Taro.showModal({
title: '核销成功',
content: '已成功核销'
});
}
}}
onError={(error) => {
console.error('统一扫码失败:', error);
}}
/>
</Space>
</View> </View>
<View className={'py-2'}> <View className={'py-2'}>
<View className={'flex justify-around mt-1'}> <View className={'flex justify-around mt-1'}>