feat(registration): 优化经销商注册流程并增加地址定位功能

- 修改导航栏标题从“邀请注册”为“注册成为会员”
- 修复重复提交问题并移除不必要的submitting状态
- 增加昵称和头像的必填验证提示
- 添加用户角色缺失时的默认角色写入机制
- 集成地图选点功能,支持经纬度获取和地址解析
- 实现微信地址导入功能,自动填充基本信息
- 增加定位权限检查和错误处理机制
- 添加.gitignore规则忽略备份文件夹src__bak
- 移除已废弃的银行卡和客户管理页面代码
- 优化表单验证规则和错误提示信息
- 实现经销商注册成功后自动跳转到“我的”页面
- 添加用户信息缓存刷新机制确保角色信息同步
```
This commit is contained in:
2026-03-01 12:35:41 +08:00
parent 945351be91
commit eee4644d06
296 changed files with 28845 additions and 6664 deletions

66
src/hooks/useAdminMode.ts Normal file
View File

@@ -0,0 +1,66 @@
import { useState, useCallback, useEffect } from 'react';
import Taro from '@tarojs/taro';
/**
* 管理员模式Hook
* 用于管理管理员用户的模式切换(普通用户模式 vs 管理员模式)
*/
export function useAdminMode() {
const [isAdminMode, setIsAdminMode] = useState<boolean>(false);
// 从本地存储加载管理员模式状态
useEffect(() => {
try {
const savedMode = Taro.getStorageSync('admin_mode');
if (savedMode !== undefined) {
setIsAdminMode(savedMode === 'true' || savedMode === true);
}
} catch (error) {
console.warn('Failed to load admin mode from storage:', error);
}
}, []);
// 切换管理员模式
const toggleAdminMode = useCallback(() => {
const newMode = !isAdminMode;
setIsAdminMode(newMode);
try {
// 保存到本地存储
Taro.setStorageSync('admin_mode', newMode);
// 显示切换提示
Taro.showToast({
title: newMode ? '已切换到管理员模式' : '已切换到普通用户模式',
icon: 'success',
duration: 1500
});
} catch (error) {
console.error('Failed to save admin mode to storage:', error);
}
}, [isAdminMode]);
// 设置管理员模式
const setAdminMode = useCallback((mode: boolean) => {
if (mode !== isAdminMode) {
setIsAdminMode(mode);
try {
Taro.setStorageSync('admin_mode', mode);
} catch (error) {
console.error('Failed to save admin mode to storage:', error);
}
}
}, [isAdminMode]);
// 重置为普通用户模式
const resetToUserMode = useCallback(() => {
setAdminMode(false);
}, [setAdminMode]);
return {
isAdminMode,
toggleAdminMode,
setAdminMode,
resetToUserMode
};
}

50
src/hooks/useConfig.ts Normal file
View File

@@ -0,0 +1,50 @@
import { useEffect, useState } from 'react';
import Taro from '@tarojs/taro';
import { configWebsiteField } from '@/api/cms/cmsWebsiteField';
import { Config } from '@/api/cms/cmsWebsiteField/model';
/**
* 自定义Hook用于获取和管理网站配置数据
* @returns {Object} 包含配置数据和加载状态的对象
*/
export const useConfig = () => {
const [config, setConfig] = useState<Config | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchConfig = async () => {
try {
setLoading(true);
const data = await configWebsiteField();
setConfig(data);
Taro.setStorageSync('config', data);
// 设置主题
if (data.theme && !Taro.getStorageSync('user_theme')) {
Taro.setStorageSync('user_theme', data.theme);
}
} catch (err) {
setError(err instanceof Error ? err : new Error('获取配置失败'));
console.error('获取网站配置失败:', err);
} finally {
setLoading(false);
}
};
fetchConfig();
}, []);
return { config, loading, error, refetch: () => {
setLoading(true);
setError(null);
configWebsiteField().then(data => {
setConfig(data);
Taro.setStorageSync('config', data);
setLoading(false);
}).catch(err => {
setError(err instanceof Error ? err : new Error('获取配置失败'));
setLoading(false);
});
}};
};

View File

@@ -1,5 +1,5 @@
import {useState, useEffect, useCallback} from 'react'
import Taro from '@tarojs/taro'
import Taro, { useDidShow } from '@tarojs/taro'
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
@@ -22,17 +22,20 @@ export interface UseDealerUserReturn {
*/
export const useDealerUser = (): UseDealerUserReturn => {
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const rawUserId = Taro.getStorageSync('UserId')
const userId = Number(rawUserId)
const hasUser = Number.isFinite(userId) && userId > 0
const userId = Taro.getStorageSync('UserId');
// If user is logged in, start in loading state to avoid "click too fast" mis-routing.
const [loading, setLoading] = useState<boolean>(hasUser)
const [error, setError] = useState<string | null>(null)
// 获取经销商用户数据
const fetchDealerData = useCallback(async () => {
if (!userId) {
console.log('🔍 用户未登录,提前返回')
if (!hasUser) {
setDealerUser(null)
setLoading(false)
return
}
@@ -42,6 +45,7 @@ export const useDealerUser = (): UseDealerUserReturn => {
// 查询当前用户的经销商信息
const dealer = await getShopDealerUser(userId)
if (dealer) {
setDealerUser(dealer)
} else {
@@ -54,7 +58,7 @@ export const useDealerUser = (): UseDealerUserReturn => {
} finally {
setLoading(false)
}
}, [userId])
}, [hasUser, userId])
// 刷新数据
const refresh = useCallback(async () => {
@@ -63,13 +67,31 @@ export const useDealerUser = (): UseDealerUserReturn => {
// 初始化加载数据
useEffect(() => {
if (userId) {
console.log('🔍 调用 fetchDealerData')
fetchDealerData().then()
if (hasUser) {
fetchDealerData()
} else {
console.log('🔍 用户ID不存在不调用 fetchDealerData')
setDealerUser(null)
setError(null)
setLoading(false)
}
}, [fetchDealerData, userId])
}, [fetchDealerData, hasUser])
// 页面返回/切换到前台时刷新一次,避免“注册成为经销商后,页面不更新”
useDidShow(() => {
fetchDealerData()
})
// 允许业务侧通过事件主动触发刷新(例如:注册成功后触发)
useEffect(() => {
const handler = () => {
fetchDealerData()
}
// 事件名尽量语义化;后续可在注册成功处 trigger
Taro.eventCenter.on('dealerUser:changed', handler)
return () => {
Taro.eventCenter.off('dealerUser:changed', handler)
}
}, [fetchDealerData])
return {
dealerUser,

View File

@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { UserOrderStats } from '@/api/user';
import { getUserOrderStats, UserOrderStats } from '@/api/user';
import Taro from '@tarojs/taro';
import {pageShopOrder} from "@/api/shop/shopOrder";
/**
* 订单统计Hook
@@ -28,20 +27,20 @@ export const useOrderStats = () => {
setLoading(true);
setError(null);
// TODO 读取订单数量
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
if(!Taro.getStorageSync('UserId')){
return false;
}
// 聚合接口:一次请求返回各状态数量(后台按用户做了缓存)
const stats = await getUserOrderStats();
setOrderStats({
pending: pending?.count || 0,
paid: paid?.count || 0,
shipped: shipped?.count || 0,
completed: completed?.count || 0,
refund: refund?.count || 0,
total: total?.count || 0
pending: stats?.pending || 0,
paid: stats?.paid || 0,
shipped: stats?.shipped || 0,
completed: stats?.completed || 0,
refund: stats?.refund || 0,
total: stats?.total || 0
})
if (showToast) {

View File

@@ -13,19 +13,30 @@ export interface CountdownTime {
totalMinutes: number; // 总剩余分钟数
}
export interface UsePaymentCountdownParams {
/** 订单创建时间(用于兼容:当 expirationTime 缺失时按 createTime + timeoutHours 计算) */
createTime?: string;
/** 订单过期时间(推荐直接传后端返回的 expirationTime */
expirationTime?: string;
/** 支付状态 */
payStatus?: boolean;
/** 是否实时更新详情页用true列表页用false */
realTime?: boolean;
/** 超时小时数默认24小时仅在 expirationTime 缺失时生效) */
timeoutHours?: number;
}
/**
* 支付倒计时Hook
* @param createTime 订单创建时间
* @param payStatus 支付状态
* @param realTime 是否实时更新详情页用true列表页用false
* @param timeoutHours 超时小时数默认24小时
* 优先使用 expirationTime当 expirationTime 缺失时回退到 createTime + timeoutHours。
*/
export const usePaymentCountdown = (
createTime?: string,
payStatus?: boolean,
realTime: boolean = false,
timeoutHours: number = 24
): CountdownTime => {
export const usePaymentCountdown = ({
createTime,
expirationTime,
payStatus,
realTime = false,
timeoutHours = 24
}: UsePaymentCountdownParams): CountdownTime => {
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
hours: 0,
minutes: 0,
@@ -37,7 +48,7 @@ export const usePaymentCountdown = (
// 计算剩余时间的函数
const calculateTimeLeft = useMemo(() => {
return (): CountdownTime => {
if (!createTime || payStatus) {
if (payStatus || (!expirationTime && !createTime)) {
return {
hours: 0,
minutes: 0,
@@ -47,8 +58,27 @@ export const usePaymentCountdown = (
};
}
const createTimeObj = dayjs(createTime);
const expireTime = createTimeObj.add(timeoutHours, 'hour');
// 优先使用后端过期时间;如果无法解析,再回退到 createTime + timeoutHours
const expireTimeFromExpiration = expirationTime ? dayjs(expirationTime) : null;
const expireTimeFromCreate =
createTime ? dayjs(createTime).add(timeoutHours, 'hour') : null;
const expireTime =
expireTimeFromExpiration?.isValid()
? expireTimeFromExpiration
: expireTimeFromCreate?.isValid()
? expireTimeFromCreate
: null;
if (!expireTime) {
return {
hours: 0,
minutes: 0,
seconds: 0,
isExpired: true,
totalMinutes: 0
};
}
const now = dayjs();
const diff = expireTime.diff(now);
@@ -76,10 +106,10 @@ export const usePaymentCountdown = (
totalMinutes
};
};
}, [createTime, payStatus, timeoutHours]);
}, [createTime, expirationTime, payStatus, timeoutHours]);
useEffect(() => {
if (!createTime || payStatus) {
if (payStatus || (!expirationTime && !createTime)) {
setTimeLeft({
hours: 0,
minutes: 0,
@@ -111,7 +141,7 @@ export const usePaymentCountdown = (
}, 1000);
return () => clearInterval(timer);
}, [createTime, payStatus, realTime, calculateTimeLeft]);
}, [createTime, expirationTime, payStatus, realTime, calculateTimeLeft]);
return timeLeft;
};

228
src/hooks/useQRLogin.ts Normal file
View File

@@ -0,0 +1,228 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import Taro from '@tarojs/taro';
import {
confirmWechatQRLogin,
parseQRContent,
type ConfirmLoginResult
} from '@/api/passport/qr-login';
/**
* 扫码登录状态
*/
export enum ScanLoginState {
IDLE = 'idle', // 空闲状态
SCANNING = 'scanning', // 正在扫码
CONFIRMING = 'confirming', // 正在确认登录
SUCCESS = 'success', // 登录成功
ERROR = 'error' // 登录失败
}
/**
* 扫码登录Hook
*/
export function useQRLogin() {
const [state, setState] = useState<ScanLoginState>(ScanLoginState.IDLE);
const [error, setError] = useState<string>('');
const [result, setResult] = useState<ConfirmLoginResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
// 用于取消操作的引用
const cancelRef = useRef<boolean>(false);
/**
* 重置状态
*/
const reset = useCallback(() => {
setState(ScanLoginState.IDLE);
setError('');
setResult(null);
setIsLoading(false);
cancelRef.current = false;
}, []);
/**
* 开始扫码登录
*/
const startScan = useCallback(async () => {
try {
reset();
setState(ScanLoginState.SCANNING);
// 检查用户是否已登录
const userId = Taro.getStorageSync('UserId');
if (!userId) {
throw new Error('请先登录小程序');
}
// 调用扫码API
const scanResult = await new Promise<string>((resolve, reject) => {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
if (res.result) {
resolve(res.result);
} else {
reject(new Error('扫码结果为空'));
}
},
fail: (err) => {
reject(new Error(err.errMsg || '扫码失败'));
}
});
});
// 检查是否被取消
if (cancelRef.current) {
return;
}
// 解析二维码内容
const token = parseQRContent(scanResult);
console.log('解析二维码内容2:',token)
if (!token) {
throw new Error('无效的登录二维码');
}
// 确认登录
setState(ScanLoginState.CONFIRMING);
setIsLoading(true);
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
console.log(confirmResult,'confirmResult>>>>')
if (cancelRef.current) {
return;
}
if (confirmResult.success) {
setState(ScanLoginState.SUCCESS);
setResult(confirmResult);
// 显示成功提示
Taro.showToast({
title: '登录确认成功',
icon: 'success',
duration: 2000
});
} else {
throw new Error(confirmResult.message || '登录确认失败');
}
} catch (err: any) {
if (!cancelRef.current) {
setState(ScanLoginState.ERROR);
const errorMessage = err.message || '扫码登录失败';
setError(errorMessage);
// 显示错误提示
Taro.showToast({
title: errorMessage,
icon: 'error',
duration: 3000
});
}
} finally {
setIsLoading(false);
}
}, [reset]);
/**
* 取消扫码登录
*/
const cancel = useCallback(() => {
cancelRef.current = true;
reset();
}, [reset]);
/**
* 处理扫码结果(用于已有扫码结果的情况)
*/
const handleScanResult = useCallback(async (qrContent: string) => {
try {
reset();
setState(ScanLoginState.CONFIRMING);
setIsLoading(true);
// 检查用户是否已登录
const userId = Taro.getStorageSync('UserId');
if (!userId) {
throw new Error('请先登录小程序');
}
// 解析二维码内容
const token = parseQRContent(qrContent);
if (!token) {
throw new Error('无效的登录二维码');
}
// 确认登录
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
if (confirmResult.success) {
setState(ScanLoginState.SUCCESS);
setResult(confirmResult);
// 显示成功提示
Taro.showToast({
title: '登录确认成功',
icon: 'success',
duration: 2000
});
} else {
throw new Error(confirmResult.message || '登录确认失败');
}
} catch (err: any) {
setState(ScanLoginState.ERROR);
const errorMessage = err.message || '登录确认失败';
setError(errorMessage);
// 显示错误提示
Taro.showToast({
title: errorMessage,
icon: 'error',
duration: 3000
});
} finally {
setIsLoading(false);
}
}, [reset]);
/**
* 检查是否可以进行扫码登录
*/
const canScan = useCallback(() => {
const userId = Taro.getStorageSync('UserId');
const accessToken = Taro.getStorageSync('access_token');
return !!(userId && accessToken);
}, []);
// 组件卸载时取消操作
useEffect(() => {
return () => {
cancelRef.current = true;
};
}, []);
return {
// 状态
state,
error,
result,
isLoading,
// 方法
startScan,
cancel,
reset,
handleScanResult,
canScan,
// 便捷状态判断
isIdle: state === ScanLoginState.IDLE,
isScanning: state === ScanLoginState.SCANNING,
isConfirming: state === ScanLoginState.CONFIRMING,
isSuccess: state === ScanLoginState.SUCCESS,
isError: state === ScanLoginState.ERROR
};
}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
import { useState, useEffect, useCallback } from 'react'
import { gradientThemes, type GradientTheme, gradientUtils } from '@/styles/gradients'
import Taro from '@tarojs/taro'
export interface UseThemeReturn {
@@ -14,28 +14,42 @@ export interface UseThemeReturn {
* 提供主题切换和状态管理功能
*/
export const useTheme = (): UseThemeReturn => {
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
// 获取当前主题
const getCurrentTheme = (): GradientTheme => {
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
if (savedTheme === 'auto') {
// 自动主题根据用户ID生成
const userId = Taro.getStorageSync('userId') || '1'
return gradientUtils.getThemeByUserId(userId)
} else {
// 手动选择的主题
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
const getSavedThemeName = useCallback((): string => {
try {
return Taro.getStorageSync('user_theme') || 'nature'
} catch {
return 'nature'
}
}
}, [])
const getStoredUserId = useCallback((): number => {
try {
const raw = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId')
const asNumber = typeof raw === 'number' ? raw : parseInt(String(raw || '1'), 10)
return Number.isFinite(asNumber) ? asNumber : 1
} catch {
return 1
}
}, [])
const resolveTheme = useCallback(
(themeName: string): GradientTheme => {
if (themeName === 'auto') {
return gradientUtils.getThemeByUserId(getStoredUserId())
}
return gradientThemes.find(t => t.name === themeName) || gradientUtils.getThemeByName('nature') || gradientThemes[0]
},
[getStoredUserId]
)
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(() => getSavedThemeName() === 'auto')
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(() => resolveTheme(getSavedThemeName()))
// 初始化主题
useEffect(() => {
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
const savedTheme = getSavedThemeName()
setIsAutoTheme(savedTheme === 'auto')
setCurrentTheme(getCurrentTheme())
setCurrentTheme(resolveTheme(savedTheme))
}, [])
// 设置主题
@@ -43,7 +57,7 @@ export const useTheme = (): UseThemeReturn => {
try {
Taro.setStorageSync('user_theme', themeName)
setIsAutoTheme(themeName === 'auto')
setCurrentTheme(getCurrentTheme())
setCurrentTheme(resolveTheme(themeName))
} catch (error) {
console.error('保存主题失败:', error)
}
@@ -51,7 +65,7 @@ export const useTheme = (): UseThemeReturn => {
// 刷新主题(用于自动主题模式下用户信息变更时)
const refreshTheme = () => {
setCurrentTheme(getCurrentTheme())
setCurrentTheme(resolveTheme(getSavedThemeName()))
}
return {

View File

@@ -5,6 +5,7 @@ import {
parseQRContent
} from '@/api/passport/qr-login';
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
import { useUser } from "@/hooks/useUser";
import { isValidJSON } from "@/utils/jsonUtils";
import dayjs from 'dayjs';
@@ -29,6 +30,15 @@ export enum ScanType {
UNKNOWN = 'unknown' // 未知类型
}
type VerificationBusinessType = 'gift' | 'ticket';
interface TicketVerificationPayload {
userTicketId: number;
qty?: number;
userId?: number;
t?: number;
}
/**
* 统一扫码结果
*/
@@ -73,7 +83,11 @@ export function useUnifiedQRScan() {
// 1. 检查是否为JSON格式核销二维码
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
return ScanType.VERIFICATION;
}
// Allow plaintext (non-encrypted) ticket verification payload for debugging/internal use.
if (json.userTicketId) {
return ScanType.VERIFICATION;
}
}
@@ -130,35 +144,79 @@ export function useUnifiedQRScan() {
throw new Error('您没有核销权限');
}
let code = '';
let businessType: VerificationBusinessType = 'gift';
let decryptedOrRaw = '';
// 判断是否为加密的JSON格式
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
// 解密获取核销码
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
businessType = json.businessType;
// 解密获取核销内容
const decryptedData = await decryptQrData({
token: json.token,
encryptedData: json.data
});
if (decryptedData) {
code = decryptedData.toString();
decryptedOrRaw = decryptedData.toString();
} else {
throw new Error('解密失败');
}
} else if (json.userTicketId) {
businessType = 'ticket';
decryptedOrRaw = scanResult.trim();
}
} else {
// 直接使用扫码结果作为核销
code = scanResult.trim();
// 直接使用扫码结果作为核销内容
decryptedOrRaw = scanResult.trim();
}
if (!code) {
if (!decryptedOrRaw) {
throw new Error('无法获取有效的核销码');
}
// 验证核销码
const gift = await getShopGiftByCode(code);
if (businessType === 'ticket') {
if (!isValidJSON(decryptedOrRaw)) {
throw new Error('水票核销信息格式错误');
}
const payload = JSON.parse(decryptedOrRaw) as TicketVerificationPayload;
const userTicketId = Number(payload.userTicketId);
const qty = Math.max(1, Number(payload.qty || 1));
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
throw new Error('水票核销信息无效');
}
const ticket = await getGltUserTicket(userTicketId);
if (!ticket) throw new Error('水票不存在');
if (ticket.status === 1) throw new Error('该水票已冻结');
const available = Number(ticket.availableQty || 0);
const used = Number(ticket.usedQty || 0);
if (available < qty) throw new Error('水票可用次数不足');
await updateGltUserTicket({
...ticket,
availableQty: available - qty,
usedQty: used + qty
});
return {
type: ScanType.VERIFICATION,
data: {
businessType: 'ticket',
ticket: {
...ticket,
availableQty: available - qty,
usedQty: used + qty
},
qty
},
message: `核销成功(已使用${qty}次)`
};
}
// 验证礼品卡核销码
const gift = await getShopGiftByCode(decryptedOrRaw);
if (!gift) {
throw new Error('核销码无效');
@@ -187,7 +245,7 @@ export function useUnifiedQRScan() {
return {
type: ScanType.VERIFICATION,
data: gift,
data: { businessType: 'gift', gift },
message: '核销成功'
};
}, [isAdmin]);
@@ -213,7 +271,14 @@ export function useUnifiedQRScan() {
}
},
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 || '扫码失败'));
}
});
});
@@ -265,6 +330,11 @@ export function useUnifiedQRScan() {
return result;
} catch (err: any) {
// User cancelled scanning (e.g. `scanCode:fail cancel`).
if (cancelRef.current) {
reset();
return null;
}
if (!cancelRef.current) {
setState(UnifiedScanState.ERROR);
const errorMessage = err.message || '处理失败';

View File

@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro';
import { User } from '@/api/system/user/model';
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
import { TenantId } from '@/config/app';
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
import { handleInviteRelation } from '@/utils/invite';
// 用户Hook
export const useUser = () => {
@@ -44,15 +44,10 @@ export const useUser = () => {
reject(new Error('自动登录失败'));
}
}).catch(_ => {
// 首次注册,跳转到邀请注册页面
const pages = Taro.getCurrentPages();
const currentPage = pages[pages.length - 1];
const inviteParams = getStoredInviteParams()
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
return Taro.navigateTo({
url: '/dealer/apply/add'
});
}
// 登录失败(通常是新用户尚未注册/未绑定手机号等)。
// 这里不做任何“自动跳转”,避免用户点击「我的」时被强制带到分销/申请页,体验割裂。
// 需要登录的页面请使用 utils/auth 的 ensureLoggedIn / goToRegister 做显式跳转。
reject(new Error('autoLoginByOpenId failed'));
});
},
fail: reject
@@ -60,7 +55,11 @@ export const useUser = () => {
});
return res;
} catch (error) {
console.error('自动登录失败:', error);
const msg = error instanceof Error ? error.message : String(error);
// 新用户首次进入、未绑定手机号等场景属于“预期失败”,避免刷屏报错。
if (msg !== 'autoLoginByOpenId failed') {
console.error('自动登录失败:', error);
}
return null;
}
};
@@ -280,11 +279,14 @@ export const useUser = () => {
// 检查用户是否是管理员
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 = () => {
return user?.isSuperAdmin === true;
const v: any = (user as any)?.isSuperAdmin;
return v === true || v === 1 || v === '1';
};
// 获取用户余额

View File

@@ -1,12 +1,10 @@
import { useState, useEffect, useCallback } from 'react'
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
import {pageShopGift} from "@/api/shop/shopGift";
import Taro from '@tarojs/taro'
import {getUserInfo} from "@/api/layout";
import {useUser} from "@/hooks/useUser";
import Taro from '@tarojs/taro'
import { getUserCardStats } from '@/api/user'
interface UserData {
balance: number
balance: string
points: number
coupons: number
giftCards: number
@@ -24,7 +22,7 @@ interface UseUserDataReturn {
loading: boolean
error: string | null
refresh: () => Promise<void>
updateBalance: (newBalance: number) => void
updateBalance: (newBalance: string) => void
updatePoints: (newPoints: number) => void
}
@@ -35,30 +33,22 @@ export const useUserData = (): UseUserDataReturn => {
// 获取用户数据
const fetchUserData = useCallback(async () => {
// 检查用户ID是否存在更直接的登录状态检查
const userId = Taro.getStorageSync('UserId')
if (!userId) {
setLoading(false)
setData(null)
return
}
try {
setLoading(true)
setError(null)
// 并发请求所有数据
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
getUserInfo(),
pageShopUserCoupon({ page: 1, limit: 1, userId, status: 0}),
pageShopGift({ page: 1, limit: 1, userId, status: 0})
])
if(!Taro.getStorageSync('UserId')){
return;
}
// 聚合接口:一次请求返回余额/积分/优惠券/礼品卡统计(后端可按用户做缓存)
const stats = await getUserCardStats()
const newData: UserData = {
balance: userDataRes?.balance || 0.00,
points: userDataRes?.points || 0,
coupons: couponsRes?.count || 0,
giftCards: giftCardsRes?.count || 0,
balance: stats?.balance || '0.00',
points: stats?.points || 0,
coupons: stats?.coupons || 0,
giftCards: stats?.giftCards || 0,
orders: {
pending: 0,
paid: 0,
@@ -82,7 +72,7 @@ export const useUserData = (): UseUserDataReturn => {
}, [fetchUserData])
// 更新余额(本地更新,避免频繁请求)
const updateBalance = useCallback((newBalance: number) => {
const updateBalance = useCallback((newBalance: string) => {
setData(prev => prev ? { ...prev, balance: newBalance } : null)
}, [])