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:
2026-02-08 12:15:31 +08:00
commit ec252beb4b
548 changed files with 76314 additions and 0 deletions

161
src/hooks/useCart.ts Normal file
View 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
};
};

View 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
}
}

View 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
View 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;

View 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
View 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]);
// 获取深色模式LogoAppInfo中无此字段使用普通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
View 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
}
}

View 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
View 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
View 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
}
}