feat(pages): 添加管理页面功能和配置
- 创建 .editorconfig 文件统一代码风格配置 - 配置 .eslintrc 使用 taro/react 规则集 - 完善 .gitignore 忽略编译产物和敏感文件 - 添加 admin/article/add 页面实现文章管理功能 - 添加 dealer/apply/add 页面实现经销商申请功能 - 添加 dealer/bank/add 页面实现银行卡管理功能 - 添加 dealer/customer/add 页面实现客户管理功能 - 添加 user/address/add 页面实现用户地址管理功能 - 添加 user/chat/message/add 页面实现消息功能 - 添加 user/gift/add 页面实现礼品管理功能 - 配置各页面导航栏标题和样式 - 实现表单验证和数据提交功能 - 集成图片上传和头像选择功能 - 添加日期选择和数据校验逻辑 - 实现编辑和新增模式切换 - 集成用户权限和角色管理功能
This commit is contained in:
161
src/hooks/useCart.ts
Normal file
161
src/hooks/useCart.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
// 购物车商品接口
|
||||
export interface CartItem {
|
||||
goodsId: number;
|
||||
name: string;
|
||||
price: string;
|
||||
image: string;
|
||||
quantity: number;
|
||||
addTime: number;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}
|
||||
|
||||
// 购物车Hook
|
||||
export const useCart = () => {
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||
const [cartCount, setCartCount] = useState(0);
|
||||
|
||||
// 从本地存储加载购物车数据
|
||||
const loadCartFromStorage = () => {
|
||||
try {
|
||||
const cartData = Taro.getStorageSync('cart_items');
|
||||
if (cartData) {
|
||||
const items = JSON.parse(cartData) as CartItem[];
|
||||
setCartItems(items);
|
||||
updateCartCount(items);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载购物车数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存购物车数据到本地存储
|
||||
const saveCartToStorage = (items: CartItem[]) => {
|
||||
try {
|
||||
Taro.setStorageSync('cart_items', JSON.stringify(items));
|
||||
} catch (error) {
|
||||
console.error('保存购物车数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新购物车数量
|
||||
const updateCartCount = (items: CartItem[]) => {
|
||||
const count = items.reduce((total, item) => total + item.quantity, 0);
|
||||
setCartCount(count);
|
||||
};
|
||||
|
||||
// 添加商品到购物车
|
||||
const addToCart = (goods: {
|
||||
goodsId: number;
|
||||
name: string;
|
||||
price: string;
|
||||
image: string;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}, quantity: number = 1) => {
|
||||
const newItems = [...cartItems];
|
||||
// 如果有SKU,需要根据goodsId和skuId来判断是否为同一商品
|
||||
const existingItemIndex = newItems.findIndex(item =>
|
||||
item.goodsId === goods.goodsId &&
|
||||
(goods.skuId ? item.skuId === goods.skuId : !item.skuId)
|
||||
);
|
||||
|
||||
if (existingItemIndex >= 0) {
|
||||
// 如果商品已存在,增加数量
|
||||
newItems[existingItemIndex].quantity += quantity;
|
||||
} else {
|
||||
// 如果商品不存在,添加新商品
|
||||
const newItem: CartItem = {
|
||||
goodsId: goods.goodsId,
|
||||
name: goods.name,
|
||||
price: goods.price,
|
||||
image: goods.image,
|
||||
quantity,
|
||||
addTime: Date.now(),
|
||||
skuId: goods.skuId,
|
||||
specInfo: goods.specInfo
|
||||
};
|
||||
newItems.push(newItem);
|
||||
}
|
||||
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '加入购物车成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
};
|
||||
|
||||
// 从购物车移除商品
|
||||
const removeFromCart = (goodsId: number) => {
|
||||
const newItems = cartItems.filter(item => item.goodsId !== goodsId);
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
};
|
||||
|
||||
// 更新商品数量
|
||||
const updateQuantity = (goodsId: number, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeFromCart(goodsId);
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = cartItems.map(item =>
|
||||
item.goodsId === goodsId ? { ...item, quantity } : item
|
||||
);
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
};
|
||||
|
||||
// 清空购物车
|
||||
const clearCart = () => {
|
||||
setCartItems([]);
|
||||
setCartCount(0);
|
||||
Taro.removeStorageSync('cart_items');
|
||||
};
|
||||
|
||||
// 获取购物车总价
|
||||
const getTotalPrice = () => {
|
||||
return cartItems.reduce((total, item) => {
|
||||
return total + (parseFloat(item.price) * item.quantity);
|
||||
}, 0).toFixed(2);
|
||||
};
|
||||
|
||||
// 检查商品是否在购物车中
|
||||
const isInCart = (goodsId: number) => {
|
||||
return cartItems.some(item => item.goodsId === goodsId);
|
||||
};
|
||||
|
||||
// 获取商品在购物车中的数量
|
||||
const getItemQuantity = (goodsId: number) => {
|
||||
const item = cartItems.find(item => item.goodsId === goodsId);
|
||||
return item ? item.quantity : 0;
|
||||
};
|
||||
|
||||
// 初始化时加载购物车数据
|
||||
useEffect(() => {
|
||||
loadCartFromStorage();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
cartItems,
|
||||
cartCount,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
updateQuantity,
|
||||
clearCart,
|
||||
getTotalPrice,
|
||||
isInCart,
|
||||
getItemQuantity,
|
||||
loadCartFromStorage
|
||||
};
|
||||
};
|
||||
81
src/hooks/useDealerApply.ts
Normal file
81
src/hooks/useDealerApply.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getShopDealerApply} from '@/api/shop/shopDealerApply'
|
||||
import type {ShopDealerApply} from '@/api/shop/shopDealerApply/model'
|
||||
|
||||
// Hook 返回值接口
|
||||
export interface UseDealerApplyReturn {
|
||||
// 经销商用户信息
|
||||
dealerApply: ShopDealerApply | null
|
||||
// 加载状态
|
||||
loading: boolean
|
||||
// 错误信息
|
||||
error: string | null
|
||||
// 刷新数据
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 经销商用户 Hook - 简化版本
|
||||
* 只查询经销商用户信息和判断是否存在
|
||||
*/
|
||||
export const useDealerApply = (): UseDealerApplyReturn => {
|
||||
const [dealerApply, setDealerApply] = useState<ShopDealerApply | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
// 获取经销商用户数据
|
||||
const fetchDealerData = useCallback(async () => {
|
||||
|
||||
if (!userId) {
|
||||
console.log('🔍 用户未登录,提前返回')
|
||||
setDealerApply(null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// 查询当前用户的经销商信息
|
||||
const dealer = await getShopDealerApply(userId)
|
||||
|
||||
if (dealer) {
|
||||
setDealerApply(dealer)
|
||||
} else {
|
||||
setDealerApply(null)
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||
setError(errorMessage)
|
||||
setDealerApply(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchDealerData()
|
||||
}, [fetchDealerData])
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
fetchDealerData()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
}
|
||||
}, [fetchDealerData, userId])
|
||||
|
||||
return {
|
||||
dealerApply,
|
||||
loading,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
80
src/hooks/useDealerUser.ts
Normal file
80
src/hooks/useDealerUser.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
|
||||
|
||||
// Hook 返回值接口
|
||||
export interface UseDealerUserReturn {
|
||||
// 经销商用户信息
|
||||
dealerUser: ShopDealerUser | null
|
||||
// 加载状态
|
||||
loading: boolean
|
||||
// 错误信息
|
||||
error: string | null
|
||||
// 刷新数据
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 经销商用户 Hook - 简化版本
|
||||
* 只查询经销商用户信息和判断是否存在
|
||||
*/
|
||||
export const useDealerUser = (): UseDealerUserReturn => {
|
||||
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
// 获取经销商用户数据
|
||||
const fetchDealerData = useCallback(async () => {
|
||||
|
||||
if (!userId) {
|
||||
console.log('🔍 用户未登录,提前返回')
|
||||
setDealerUser(null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// 查询当前用户的经销商信息
|
||||
const dealer = await getShopDealerUser(userId)
|
||||
if (dealer) {
|
||||
setDealerUser(dealer)
|
||||
} else {
|
||||
setDealerUser(null)
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||
setError(errorMessage)
|
||||
setDealerUser(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchDealerData()
|
||||
}, [fetchDealerData])
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
fetchDealerData().then()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
}
|
||||
}, [fetchDealerData, userId])
|
||||
|
||||
return {
|
||||
dealerUser,
|
||||
loading,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
117
src/hooks/useOrderStats.ts
Normal file
117
src/hooks/useOrderStats.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { UserOrderStats } from '@/api/user';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
|
||||
/**
|
||||
* 订单统计Hook
|
||||
* 用于管理用户订单各状态的数量统计
|
||||
*/
|
||||
export const useOrderStats = () => {
|
||||
const [orderStats, setOrderStats] = useState<UserOrderStats>({
|
||||
pending: 0, // 待付款
|
||||
paid: 0, // 待发货
|
||||
shipped: 0, // 待收货
|
||||
completed: 0, // 已完成
|
||||
refund: 0, // 退货/售后
|
||||
total: 0 // 总订单数
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* 获取订单统计数据
|
||||
*/
|
||||
const fetchOrderStats = useCallback(async (showToast = false) => {
|
||||
try {
|
||||
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')})
|
||||
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
|
||||
})
|
||||
|
||||
if (showToast) {
|
||||
Taro.showToast({
|
||||
title: '数据已更新',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.message || '获取订单统计失败';
|
||||
setError(errorMessage);
|
||||
|
||||
console.error('获取订单统计失败:', err);
|
||||
|
||||
if (showToast) {
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 刷新订单统计数据
|
||||
*/
|
||||
const refreshOrderStats = useCallback(() => {
|
||||
return fetchOrderStats(true);
|
||||
}, [fetchOrderStats]);
|
||||
|
||||
/**
|
||||
* 获取指定状态的订单数量
|
||||
*/
|
||||
const getOrderCount = useCallback((status: keyof UserOrderStats) => {
|
||||
return orderStats[status] || 0;
|
||||
}, [orderStats]);
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的订单
|
||||
*/
|
||||
const hasPendingOrders = useCallback(() => {
|
||||
return orderStats.pending > 0 || orderStats.paid > 0 || orderStats.shipped > 0;
|
||||
}, [orderStats]);
|
||||
|
||||
/**
|
||||
* 获取总的待处理订单数量
|
||||
*/
|
||||
const getTotalPendingCount = useCallback(() => {
|
||||
return orderStats.pending + orderStats.paid + orderStats.shipped;
|
||||
}, [orderStats]);
|
||||
|
||||
// 组件挂载时自动获取数据
|
||||
useEffect(() => {
|
||||
fetchOrderStats();
|
||||
}, [fetchOrderStats]);
|
||||
|
||||
return {
|
||||
orderStats,
|
||||
loading,
|
||||
error,
|
||||
fetchOrderStats,
|
||||
refreshOrderStats,
|
||||
getOrderCount,
|
||||
hasPendingOrders,
|
||||
getTotalPendingCount
|
||||
};
|
||||
};
|
||||
|
||||
export default useOrderStats;
|
||||
163
src/hooks/usePaymentCountdown.ts
Normal file
163
src/hooks/usePaymentCountdown.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
|
||||
// 扩展dayjs支持duration
|
||||
dayjs.extend(duration);
|
||||
|
||||
export interface CountdownTime {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
isExpired: boolean;
|
||||
totalMinutes: number; // 总剩余分钟数
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付倒计时Hook
|
||||
* @param createTime 订单创建时间
|
||||
* @param payStatus 支付状态
|
||||
* @param realTime 是否实时更新(详情页用true,列表页用false)
|
||||
* @param timeoutHours 超时小时数,默认24小时
|
||||
*/
|
||||
export const usePaymentCountdown = (
|
||||
createTime?: string,
|
||||
payStatus?: boolean,
|
||||
realTime: boolean = false,
|
||||
timeoutHours: number = 24
|
||||
): CountdownTime => {
|
||||
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
});
|
||||
|
||||
// 计算剩余时间的函数
|
||||
const calculateTimeLeft = useMemo(() => {
|
||||
return (): CountdownTime => {
|
||||
if (!createTime || payStatus) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
};
|
||||
}
|
||||
|
||||
const createTimeObj = dayjs(createTime);
|
||||
const expireTime = createTimeObj.add(timeoutHours, 'hour');
|
||||
const now = dayjs();
|
||||
const diff = expireTime.diff(now);
|
||||
|
||||
if (diff <= 0) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: true,
|
||||
totalMinutes: 0
|
||||
};
|
||||
}
|
||||
|
||||
const durationObj = dayjs.duration(diff);
|
||||
const hours = Math.floor(durationObj.asHours());
|
||||
const minutes = durationObj.minutes();
|
||||
const seconds = durationObj.seconds();
|
||||
const totalMinutes = Math.floor(durationObj.asMinutes());
|
||||
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
isExpired: false,
|
||||
totalMinutes
|
||||
};
|
||||
};
|
||||
}, [createTime, payStatus, timeoutHours]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!createTime || payStatus) {
|
||||
setTimeLeft({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即计算一次
|
||||
const initialTime = calculateTimeLeft();
|
||||
setTimeLeft(initialTime);
|
||||
|
||||
// 如果不需要实时更新,直接返回
|
||||
if (!realTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果需要实时更新,设置定时器
|
||||
const timer = setInterval(() => {
|
||||
const newTimeLeft = calculateTimeLeft();
|
||||
setTimeLeft(newTimeLeft);
|
||||
|
||||
// 如果已过期,清除定时器
|
||||
if (newTimeLeft.isExpired) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [createTime, payStatus, realTime, calculateTimeLeft]);
|
||||
|
||||
return timeLeft;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化倒计时文本
|
||||
* @param timeLeft 倒计时时间对象
|
||||
* @param showSeconds 是否显示秒数
|
||||
*/
|
||||
export const formatCountdownText = (
|
||||
timeLeft: CountdownTime,
|
||||
showSeconds: boolean = false
|
||||
): string => {
|
||||
if (timeLeft.isExpired) {
|
||||
return '已过期';
|
||||
}
|
||||
|
||||
if (timeLeft.hours > 0) {
|
||||
if (showSeconds) {
|
||||
return `${timeLeft.hours}小时${timeLeft.minutes}分${timeLeft.seconds}秒`;
|
||||
} else {
|
||||
return `${timeLeft.hours}小时${timeLeft.minutes}分钟`;
|
||||
}
|
||||
} else if (timeLeft.minutes > 0) {
|
||||
if (showSeconds) {
|
||||
return `${timeLeft.minutes}分${timeLeft.seconds}秒`;
|
||||
} else {
|
||||
return `${timeLeft.minutes}分钟`;
|
||||
}
|
||||
} else {
|
||||
return `${timeLeft.seconds}秒`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为紧急状态(剩余时间少于1小时)
|
||||
*/
|
||||
export const isUrgentCountdown = (timeLeft: CountdownTime): boolean => {
|
||||
return !timeLeft.isExpired && timeLeft.totalMinutes < 60;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为非常紧急状态(剩余时间少于10分钟)
|
||||
*/
|
||||
export const isCriticalCountdown = (timeLeft: CountdownTime): boolean => {
|
||||
return !timeLeft.isExpired && timeLeft.totalMinutes < 10;
|
||||
};
|
||||
|
||||
export default usePaymentCountdown;
|
||||
323
src/hooks/useShopInfo.ts
Normal file
323
src/hooks/useShopInfo.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import {useState, useEffect, useCallback} from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {AppInfo} from '@/api/cms/cmsWebsite/model';
|
||||
import {getShopInfo} from '@/api/layout';
|
||||
|
||||
// 本地存储键名
|
||||
const SHOP_INFO_STORAGE_KEY = 'shop_info';
|
||||
const SHOP_INFO_CACHE_TIME_KEY = 'shop_info_cache_time';
|
||||
|
||||
// 缓存有效期(毫秒)- 默认30分钟
|
||||
const CACHE_DURATION = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 商店信息Hook
|
||||
* 提供商店信息的获取、缓存和管理功能
|
||||
*/
|
||||
export const useShopInfo = () => {
|
||||
const [shopInfo, setShopInfo] = useState<AppInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 从本地存储加载商店信息
|
||||
const loadShopInfoFromStorage = useCallback(() => {
|
||||
try {
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
const cacheTime = Taro.getStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
|
||||
if (cachedData && cacheTime) {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - cacheTime;
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (timeDiff < CACHE_DURATION) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
setLoading(false);
|
||||
return true; // 返回true表示使用了缓存
|
||||
} else {
|
||||
// 缓存过期,清除旧数据
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商店信息缓存失败:', error);
|
||||
}
|
||||
return false; // 返回false表示没有使用缓存
|
||||
}, []);
|
||||
|
||||
// 保存商店信息到本地存储
|
||||
const saveShopInfoToStorage = useCallback((data: AppInfo) => {
|
||||
try {
|
||||
Taro.setStorageSync(SHOP_INFO_STORAGE_KEY, data);
|
||||
Taro.setStorageSync(SHOP_INFO_CACHE_TIME_KEY, Date.now());
|
||||
} catch (error) {
|
||||
console.error('保存商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从服务器获取商店信息
|
||||
const fetchShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('获取商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
|
||||
// 如果网络请求失败,尝试使用缓存数据(即使过期)
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
if (cachedData) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
console.warn('网络请求失败,使用缓存数据');
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 刷新商店信息
|
||||
const refreshShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('刷新商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 清除缓存
|
||||
const clearCache = useCallback(() => {
|
||||
try {
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
setShopInfo(null);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error('清除商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取应用名称
|
||||
const getAppName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站名称(兼容旧方法名)
|
||||
const getWebsiteName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用Logo
|
||||
const getAppLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站Logo(兼容旧方法名)
|
||||
const getWebsiteLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用图标
|
||||
const getAppIcon = useCallback(() => {
|
||||
return shopInfo?.icon || shopInfo?.logo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取深色模式Logo(AppInfo中无此字段,使用普通Logo)
|
||||
const getDarkLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用域名
|
||||
const getDomain = useCallback(() => {
|
||||
return shopInfo?.domain || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用描述
|
||||
const getDescription = useCallback(() => {
|
||||
return shopInfo?.description || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用关键词
|
||||
const getKeywords = useCallback(() => {
|
||||
return shopInfo?.keywords || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用标题
|
||||
const getTitle = useCallback(() => {
|
||||
return shopInfo?.title || shopInfo?.appName || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取小程序二维码
|
||||
const getMpQrCode = useCallback(() => {
|
||||
return shopInfo?.mpQrCode || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取联系电话(AppInfo中无此字段,从config中获取)
|
||||
const getPhone = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.phone || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取邮箱(AppInfo中无此字段,从config中获取)
|
||||
const getEmail = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.email || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取地址(AppInfo中无此字段,从config中获取)
|
||||
const getAddress = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.address || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取ICP备案号(AppInfo中无此字段,从config中获取)
|
||||
const getIcpNo = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.icpNo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用状态
|
||||
const getStatus = useCallback(() => {
|
||||
return {
|
||||
running: shopInfo?.running || 0,
|
||||
statusText: shopInfo?.statusText || '',
|
||||
statusIcon: shopInfo?.statusIcon || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用配置
|
||||
const getConfig = useCallback(() => {
|
||||
return shopInfo?.config || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用设置
|
||||
const getSetting = useCallback(() => {
|
||||
return shopInfo?.setting || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取服务器时间
|
||||
const getServerTime = useCallback(() => {
|
||||
return shopInfo?.serverTime || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取导航菜单
|
||||
const getNavigation = useCallback(() => {
|
||||
return {
|
||||
topNavs: shopInfo?.topNavs || [],
|
||||
bottomNavs: shopInfo?.bottomNavs || []
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否支持搜索(从config中获取)
|
||||
const isSearchEnabled = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.search === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用版本信息
|
||||
const getVersionInfo = useCallback(() => {
|
||||
return {
|
||||
version: shopInfo?.version || 10,
|
||||
expirationTime: shopInfo?.expirationTime || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查应用是否过期
|
||||
const isExpired = useCallback(() => {
|
||||
return shopInfo?.expired === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取过期天数
|
||||
const getExpiredDays = useCallback(() => {
|
||||
return shopInfo?.expiredDays || 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否即将过期
|
||||
const isSoonExpired = useCallback(() => {
|
||||
return (shopInfo?.soon || 0) > 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 初始化时加载商店信息
|
||||
useEffect(() => {
|
||||
const initShopInfo = async () => {
|
||||
// 先尝试从缓存加载
|
||||
const hasCache = loadShopInfoFromStorage();
|
||||
|
||||
// 如果没有缓存或需要刷新,则从服务器获取
|
||||
if (!hasCache) {
|
||||
await fetchShopInfo();
|
||||
}
|
||||
};
|
||||
|
||||
initShopInfo();
|
||||
}, []); // 空依赖数组,只在组件挂载时执行一次
|
||||
|
||||
return {
|
||||
// 状态
|
||||
shopInfo,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 方法
|
||||
fetchShopInfo,
|
||||
refreshShopInfo,
|
||||
clearCache,
|
||||
|
||||
// 新的工具方法(基于AppInfo字段)
|
||||
getAppName,
|
||||
getAppLogo,
|
||||
getAppIcon,
|
||||
getDescription,
|
||||
getKeywords,
|
||||
getTitle,
|
||||
getMpQrCode,
|
||||
getDomain,
|
||||
getConfig,
|
||||
getSetting,
|
||||
getServerTime,
|
||||
getNavigation,
|
||||
getStatus,
|
||||
getVersionInfo,
|
||||
isExpired,
|
||||
getExpiredDays,
|
||||
isSoonExpired,
|
||||
|
||||
// 兼容旧方法名
|
||||
getWebsiteName,
|
||||
getWebsiteLogo,
|
||||
getDarkLogo,
|
||||
getPhone,
|
||||
getEmail,
|
||||
getAddress,
|
||||
getIcpNo,
|
||||
isSearchEnabled
|
||||
};
|
||||
};
|
||||
95
src/hooks/useTheme.ts
Normal file
95
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface UseThemeReturn {
|
||||
currentTheme: GradientTheme
|
||||
setTheme: (themeName: string) => void
|
||||
isAutoTheme: boolean
|
||||
refreshTheme: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题管理Hook
|
||||
* 提供主题切换和状态管理功能
|
||||
*/
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主题
|
||||
useEffect(() => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
setIsAutoTheme(savedTheme === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
}, [])
|
||||
|
||||
// 设置主题
|
||||
const setTheme = (themeName: string) => {
|
||||
try {
|
||||
Taro.setStorageSync('user_theme', themeName)
|
||||
setIsAutoTheme(themeName === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
} catch (error) {
|
||||
console.error('保存主题失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新主题(用于自动主题模式下用户信息变更时)
|
||||
const refreshTheme = () => {
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
}
|
||||
|
||||
return {
|
||||
currentTheme,
|
||||
setTheme,
|
||||
isAutoTheme,
|
||||
refreshTheme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题的样式对象
|
||||
* 用于直接应用到组件样式中
|
||||
*/
|
||||
export const useThemeStyles = () => {
|
||||
const { currentTheme } = useTheme()
|
||||
|
||||
return {
|
||||
// 主要背景样式
|
||||
primaryBackground: {
|
||||
background: currentTheme.background,
|
||||
color: currentTheme.textColor
|
||||
},
|
||||
|
||||
// 按钮样式
|
||||
primaryButton: {
|
||||
background: currentTheme.background,
|
||||
border: 'none',
|
||||
color: currentTheme.textColor
|
||||
},
|
||||
|
||||
// 强调色
|
||||
accentColor: currentTheme.primary,
|
||||
|
||||
// 文字颜色
|
||||
textColor: currentTheme.textColor,
|
||||
|
||||
// 完整主题对象
|
||||
theme: currentTheme
|
||||
}
|
||||
}
|
||||
331
src/hooks/useUnifiedQRScan.ts
Normal file
331
src/hooks/useUnifiedQRScan.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent
|
||||
} from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 统一扫码状态
|
||||
*/
|
||||
export enum UnifiedScanState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
PROCESSING = 'processing', // 正在处理
|
||||
SUCCESS = 'success', // 处理成功
|
||||
ERROR = 'error' // 处理失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码类型
|
||||
*/
|
||||
export enum ScanType {
|
||||
LOGIN = 'login', // 登录二维码
|
||||
VERIFICATION = 'verification', // 核销二维码
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
export interface UnifiedScanResult {
|
||||
type: ScanType;
|
||||
data: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码Hook
|
||||
* 可以处理登录和核销两种类型的二维码
|
||||
*/
|
||||
export function useUnifiedQRScan() {
|
||||
const { isAdmin } = useUser();
|
||||
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<UnifiedScanResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [scanType, setScanType] = useState<ScanType>(ScanType.UNKNOWN);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(UnifiedScanState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
setScanType(ScanType.UNKNOWN);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 检测二维码类型
|
||||
*/
|
||||
const detectScanType = useCallback((scanResult: string): ScanType => {
|
||||
try {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否为登录二维码
|
||||
const loginToken = parseQRContent(scanResult);
|
||||
if (loginToken) {
|
||||
return ScanType.LOGIN;
|
||||
}
|
||||
|
||||
// 3. 检查是否为纯文本核销码(6位数字)
|
||||
if (/^\d{6}$/.test(scanResult.trim())) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
|
||||
return ScanType.UNKNOWN;
|
||||
} catch (error) {
|
||||
console.error('检测二维码类型失败:', error);
|
||||
return ScanType.UNKNOWN;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理登录二维码
|
||||
*/
|
||||
const handleLoginQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
const token = parseQRContent(scanResult);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
if (confirmResult.status === 'confirmed') {
|
||||
return {
|
||||
type: ScanType.LOGIN,
|
||||
data: confirmResult,
|
||||
message: '登录成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理核销二维码
|
||||
*/
|
||||
const handleVerificationQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
if (!isAdmin()) {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
}
|
||||
|
||||
if (gift.status === 1) {
|
||||
throw new Error('此礼品码已使用');
|
||||
}
|
||||
|
||||
if (gift.status === 2) {
|
||||
throw new Error('此礼品码已失效');
|
||||
}
|
||||
|
||||
if (gift.userId === 0) {
|
||||
throw new Error('此礼品码未认领');
|
||||
}
|
||||
|
||||
// 执行核销
|
||||
await updateShopGift({
|
||||
...gift,
|
||||
status: 1,
|
||||
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
|
||||
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
|
||||
/**
|
||||
* 开始扫码
|
||||
*/
|
||||
const startScan = useCallback(async (): Promise<UnifiedScanResult | null> => {
|
||||
try {
|
||||
reset();
|
||||
setState(UnifiedScanState.SCANNING);
|
||||
|
||||
// 调用扫码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 null;
|
||||
}
|
||||
|
||||
// 检测二维码类型
|
||||
const type = detectScanType(scanResult);
|
||||
setScanType(type);
|
||||
|
||||
if (type === ScanType.UNKNOWN) {
|
||||
throw new Error('不支持的二维码类型');
|
||||
}
|
||||
|
||||
// 开始处理
|
||||
setState(UnifiedScanState.PROCESSING);
|
||||
setIsLoading(true);
|
||||
|
||||
let result: UnifiedScanResult;
|
||||
|
||||
switch (type) {
|
||||
case ScanType.LOGIN:
|
||||
result = await handleLoginQR(scanResult);
|
||||
break;
|
||||
case ScanType.VERIFICATION:
|
||||
result = await handleVerificationQR(scanResult);
|
||||
break;
|
||||
default:
|
||||
throw new Error('未知的扫码类型');
|
||||
}
|
||||
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(UnifiedScanState.SUCCESS);
|
||||
setResult(result);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: result.message,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(UnifiedScanState.ERROR);
|
||||
const errorMessage = err.message || '处理失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]);
|
||||
|
||||
/**
|
||||
* 取消扫码
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [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,
|
||||
scanType,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === UnifiedScanState.IDLE,
|
||||
isScanning: state === UnifiedScanState.SCANNING,
|
||||
isProcessing: state === UnifiedScanState.PROCESSING,
|
||||
isSuccess: state === UnifiedScanState.SUCCESS,
|
||||
isError: state === UnifiedScanState.ERROR
|
||||
};
|
||||
}
|
||||
335
src/hooks/useUser.ts
Normal file
335
src/hooks/useUser.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
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';
|
||||
|
||||
// 用户Hook
|
||||
export const useUser = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 自动登录(通过OpenID)
|
||||
const autoLoginByOpenId = async () => {
|
||||
try {
|
||||
const res = await new Promise<any>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (loginRes) => {
|
||||
loginByOpenId({
|
||||
code: loginRes.code,
|
||||
tenantId: TenantId
|
||||
}).then(async (data) => {
|
||||
if (data) {
|
||||
// 保存登录信息
|
||||
saveUserToStorage(data.access_token, data.user);
|
||||
setUser(data.user);
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// 处理邀请关系
|
||||
if (data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(data.user.userId);
|
||||
if (inviteSuccess) {
|
||||
console.log('自动登录时邀请关系建立成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动登录时处理邀请关系失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(data.user);
|
||||
} else {
|
||||
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'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('自动登录失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 从本地存储加载用户数据
|
||||
const loadUserFromStorage = async () => {
|
||||
try {
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
const userData = Taro.getStorageSync('User');
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const tenantId = Taro.getStorageSync('TenantId');
|
||||
|
||||
if (token && userData) {
|
||||
const userInfo = typeof userData === 'string' ? JSON.parse(userData) : userData;
|
||||
setUser(userInfo);
|
||||
setIsLoggedIn(true);
|
||||
} else if (token && userId) {
|
||||
// 如果有token和userId但没有完整用户信息,标记为已登录但需要获取用户信息
|
||||
setIsLoggedIn(true);
|
||||
setUser({ userId, tenantId } as User);
|
||||
} else {
|
||||
// 没有本地登录信息,尝试自动登录
|
||||
console.log('没有本地登录信息,尝试自动登录...');
|
||||
const autoLoginResult = await autoLoginByOpenId();
|
||||
if (!autoLoginResult) {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户数据失败:', error);
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存用户数据到本地存储
|
||||
const saveUserToStorage = (token: string, userInfo: User) => {
|
||||
try {
|
||||
Taro.setStorageSync('access_token', token);
|
||||
Taro.setStorageSync('User', userInfo);
|
||||
|
||||
// 确保关键字段不为空时才保存,避免覆盖现有数据
|
||||
if (userInfo.userId) {
|
||||
Taro.setStorageSync('UserId', userInfo.userId);
|
||||
}
|
||||
if (userInfo.tenantId) {
|
||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
||||
}
|
||||
if (userInfo.phone) {
|
||||
Taro.setStorageSync('Phone', userInfo.phone);
|
||||
}
|
||||
// 保存头像和昵称信息
|
||||
if (userInfo.avatar) {
|
||||
Taro.setStorageSync('Avatar', userInfo.avatar);
|
||||
}
|
||||
if (userInfo.nickname) {
|
||||
Taro.setStorageSync('Nickname', userInfo.nickname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存用户数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 登录用户
|
||||
const loginUser = (token: string, userInfo: User) => {
|
||||
setUser(userInfo);
|
||||
setIsLoggedIn(true);
|
||||
saveUserToStorage(token, userInfo);
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const logoutUser = () => {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
|
||||
// 清除本地存储
|
||||
try {
|
||||
Taro.removeStorageSync('access_token');
|
||||
Taro.removeStorageSync('User');
|
||||
Taro.removeStorageSync('UserId');
|
||||
Taro.removeStorageSync('TenantId');
|
||||
Taro.removeStorageSync('Phone');
|
||||
Taro.removeStorageSync('userInfo');
|
||||
} catch (error) {
|
||||
console.error('清除用户数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从服务器获取最新用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
if (!isLoggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
setUser(userInfo);
|
||||
|
||||
// 更新本地存储
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
if (token) {
|
||||
saveUserToStorage(token, userInfo);
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
// 如果获取失败,可能是token过期,清除登录状态
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage?.includes('401') || errorMessage?.includes('未授权')) {
|
||||
logoutUser();
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户信息
|
||||
const updateUser = async (userData: Partial<User>) => {
|
||||
if (!user) {
|
||||
throw new Error('用户未登录');
|
||||
}
|
||||
|
||||
try {
|
||||
// 先获取最新的用户信息,确保我们有完整的数据
|
||||
const latestUserInfo = await getUserInfo();
|
||||
|
||||
// 合并最新的用户信息和要更新的数据
|
||||
const updatedUser = { ...latestUserInfo, ...userData };
|
||||
|
||||
// 调用API更新用户信息
|
||||
await updateUserInfo(updatedUser);
|
||||
|
||||
// 更新本地状态
|
||||
setUser(updatedUser);
|
||||
|
||||
// 更新本地存储
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
if (token) {
|
||||
saveUserToStorage(token, updatedUser);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error);
|
||||
Taro.showToast({
|
||||
title: '更新失败',
|
||||
icon: 'error',
|
||||
duration: 1500
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否有特定权限
|
||||
const hasPermission = (permission: string) => {
|
||||
if (!user || !user.authorities) {
|
||||
return false;
|
||||
}
|
||||
return user.authorities.some(auth => auth.authority === permission);
|
||||
};
|
||||
|
||||
// 检查是否有特定角色
|
||||
const hasRole = (roleCode: string) => {
|
||||
if (!user || !user.roles) {
|
||||
return false;
|
||||
}
|
||||
return user.roles.some(role => role.roleCode === roleCode);
|
||||
};
|
||||
|
||||
// 获取用户头像URL
|
||||
const getAvatarUrl = () => {
|
||||
return user?.avatar || user?.avatarUrl || '';
|
||||
};
|
||||
|
||||
const getUserId = () => {
|
||||
return user?.userId;
|
||||
};
|
||||
|
||||
// 获取用户显示名称
|
||||
const getDisplayName = () => {
|
||||
return user?.nickname || user?.realName || user?.username || '未登录';
|
||||
};
|
||||
|
||||
// 获取用户显示的角色(同步版本)
|
||||
const getRoleName = () => {
|
||||
if(hasRole('superAdmin')){
|
||||
return '超级管理员';
|
||||
}
|
||||
if(hasRole('admin')){
|
||||
return '管理员';
|
||||
}
|
||||
if(hasRole('staff')){
|
||||
return '员工';
|
||||
}
|
||||
if(hasRole('vip')){
|
||||
return 'VIP会员';
|
||||
}
|
||||
return '注册用户';
|
||||
}
|
||||
|
||||
// 检查用户是否已实名认证
|
||||
const isCertified = () => {
|
||||
return user?.certification === true;
|
||||
};
|
||||
|
||||
// 检查用户是否是管理员
|
||||
const isAdmin = () => {
|
||||
return user?.isAdmin === true;
|
||||
};
|
||||
|
||||
const isSuperAdmin = () => {
|
||||
return user?.isSuperAdmin === true;
|
||||
};
|
||||
|
||||
// 获取用户余额
|
||||
const getBalance = () => {
|
||||
return user?.balance || 0;
|
||||
};
|
||||
|
||||
// 获取用户积分
|
||||
const getPoints = () => {
|
||||
return user?.points || 0;
|
||||
};
|
||||
|
||||
// 初始化时加载用户数据
|
||||
useEffect(() => {
|
||||
loadUserFromStorage().catch(error => {
|
||||
console.error('初始化用户数据失败:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
isLoggedIn,
|
||||
loading,
|
||||
|
||||
// 方法
|
||||
loginUser,
|
||||
logoutUser,
|
||||
fetchUserInfo,
|
||||
updateUser,
|
||||
loadUserFromStorage,
|
||||
autoLoginByOpenId,
|
||||
|
||||
// 工具方法
|
||||
hasPermission,
|
||||
hasRole,
|
||||
getAvatarUrl,
|
||||
getDisplayName,
|
||||
getRoleName,
|
||||
isCertified,
|
||||
isAdmin,
|
||||
getBalance,
|
||||
getPoints,
|
||||
getUserId,
|
||||
isSuperAdmin
|
||||
};
|
||||
};
|
||||
140
src/hooks/useUserData.ts
Normal file
140
src/hooks/useUserData.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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";
|
||||
|
||||
interface UserData {
|
||||
balance: number
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
orders: {
|
||||
pending: number
|
||||
paid: number
|
||||
shipped: number
|
||||
completed: number
|
||||
refund: number
|
||||
}
|
||||
}
|
||||
|
||||
interface UseUserDataReturn {
|
||||
data: UserData | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
refresh: () => Promise<void>
|
||||
updateBalance: (newBalance: number) => void
|
||||
updatePoints: (newPoints: number) => void
|
||||
}
|
||||
|
||||
export const useUserData = (): UseUserDataReturn => {
|
||||
const [data, setData] = useState<UserData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// 获取用户数据
|
||||
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})
|
||||
])
|
||||
|
||||
const newData: UserData = {
|
||||
balance: userDataRes?.balance || 0.00,
|
||||
points: userDataRes?.points || 0,
|
||||
coupons: couponsRes?.count || 0,
|
||||
giftCards: giftCardsRes?.count || 0,
|
||||
orders: {
|
||||
pending: 0,
|
||||
paid: 0,
|
||||
shipped: 0,
|
||||
completed: 0,
|
||||
refund: 0
|
||||
}
|
||||
}
|
||||
|
||||
setData(newData)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取用户数据失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchUserData()
|
||||
}, [fetchUserData])
|
||||
|
||||
// 更新余额(本地更新,避免频繁请求)
|
||||
const updateBalance = useCallback((newBalance: number) => {
|
||||
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
||||
}, [])
|
||||
|
||||
// 更新积分
|
||||
const updatePoints = useCallback((newPoints: number) => {
|
||||
setData(prev => prev ? { ...prev, points: newPoints } : null)
|
||||
}, [])
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
fetchUserData().then()
|
||||
}, [fetchUserData])
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
updateBalance,
|
||||
updatePoints
|
||||
}
|
||||
}
|
||||
|
||||
// 轻量级版本 - 只获取基础数据
|
||||
export const useUserBasicData = () => {
|
||||
const {user} = useUser()
|
||||
const [balance, setBalance] = useState<number>(0)
|
||||
const [points, setPoints] = useState<number>(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchBasicData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
setBalance(user?.balance || 0)
|
||||
setPoints(user?.points || 0)
|
||||
} catch (error) {
|
||||
console.error('获取基础数据失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchBasicData().then()
|
||||
}, [fetchBasicData])
|
||||
|
||||
return {
|
||||
balance,
|
||||
points,
|
||||
loading,
|
||||
refresh: fetchBasicData,
|
||||
updateBalance: setBalance,
|
||||
updatePoints: setPoints
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user