docs: 更新优惠券相关文档- 新增优惠券API集成文档

- 新增优惠券卡片对齐修复文档
- 新增优惠券状态显示调试文档
- 新增优惠券组件警告修复文档- 更新用ShopInfo Hook字段迁移文档
- 更新Arguments关键字修复文档
This commit is contained in:
2025-08-15 01:52:36 +08:00
parent dc87f644c9
commit 1b24a611a8
50 changed files with 6530 additions and 595 deletions

View File

@@ -99,6 +99,34 @@ export interface CmsWebsite {
search?: boolean;
}
export interface AppInfo {
appId?: number;
appName?: string;
description?: string;
keywords?: string;
appCode?: string;
mpQrCode?: string;
title?: string;
logo?: string;
icon?: string;
domain?: string;
running?: number;
version?: number;
expirationTime?: string;
expired?: boolean;
expiredDays?: number;
soon?: number;
statusIcon?: string;
statusText?: string;
config?: Object;
serverTime?: Object;
topNavs?: CmsNavigation[];
bottomNavs?: CmsNavigation[];
setting?: Object;
createTime?: string;
}
/**
* 网站信息记录表搜索条件
*/

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request-legacy';
import type { ApiResult } from '@/api/index';
import type { ApiResult } from '@/api';
import type {
LoginParam,
LoginResult,
@@ -70,71 +70,3 @@ export async function sendSmsCaptcha(data: LoginParam) {
}
return Promise.reject(new Error(res.message));
}
export async function getOpenId(data: any){
const res = request.post<ApiResult<LoginResult>>(
SERVER_API_URL + '/wx-login/getOpenId',
data
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function loginByOpenId(data:any){
const res = request.post<ApiResult<LoginResult>>(
SERVER_API_URL + '/wx-login/loginByOpenId',
data
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 登录
*/
export async function remoteLogin(data: LoginParam) {
const res = await request.post<ApiResult<LoginResult>>(
'https://open.gxwebsoft.com/api/login',
data
);
if (res.code === 0) {
// setToken(res.data.data?.access_token, data.remember);
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取企业微信登录链接
*/
export async function getWxWorkQrConnect(data: any) {
const res = await request.post<ApiResult<unknown>>(
'/wx-work',
data
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function loginMpWxMobile(data: {
refereeId: number;
gradeId: number;
code: any;
sceneType: string;
comments: string;
encryptedData: any;
tenantId: string;
iv: any;
notVerifyPhone: boolean
}) {
const res = request.post<ApiResult<unknown>>(SERVER_API_URL + '/wx-login/loginByMpWxPhone', data);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -99,3 +99,42 @@ export async function getShopUserCoupon(id: number) {
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的可用优惠券
*/
export async function getMyAvailableCoupons() {
const res = await request.get<ApiResult<ShopUserCoupon[]>>(
'/shop/shop-user-coupon/my/available'
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的已使用优惠券
*/
export async function getMyUsedCoupons() {
const res = await request.get<ApiResult<ShopUserCoupon[]>>(
'/shop/shop-user-coupon/my/used'
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的已过期优惠券
*/
export async function getMyExpiredCoupons() {
const res = await request.get<ApiResult<ShopUserCoupon[]>>(
'/shop/shop-user-coupon/my/expired'
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -32,8 +32,16 @@ export interface ShopUserCoupon {
endTime?: string;
// 使用状态(0未使用 1已使用 2已过期)
status?: number;
// 状态文本描述
statusText?: string;
// 是否过期, 0否, 1是
isExpire?: number;
// 是否即将过期(后端计算)
isExpiringSoon?: boolean;
// 剩余天数(后端计算)
daysRemaining?: number;
// 剩余小时数(后端计算)
hoursRemaining?: number;
// 使用时间
useTime?: string;
// 使用订单ID
@@ -63,5 +71,13 @@ export interface ShopUserCouponParam extends PageParam {
isExpire?: number;
sortBy?: string;
sortOrder?: string;
// 仅查询有效的优惠券
validOnly?: boolean;
// 仅查询已过期的优惠券
expired?: boolean;
// 查询即将过期的优惠券
expiringSoon?: boolean;
// 当前时间(用于测试)
currentTime?: string;
keywords?: string;
}

View File

@@ -1,77 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { UserCoupon, UserCouponParam } from './model';
/**
* 分页查询用户优惠券
*/
export async function pageUserCoupon(params: UserCouponParam) {
const res = await request.get<ApiResult<PageResult<UserCoupon>>>(
'/sys/user-coupon/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询用户优惠券列表
*/
export async function listUserCoupon(params?: UserCouponParam) {
const res = await request.get<ApiResult<UserCoupon[]>>(
'/sys/user-coupon',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取用户优惠券统计
*/
export async function getUserCouponCount(userId: number) {
const res = await request.get<ApiResult<{
total: number;
unused: number;
used: number;
expired: number;
}>>(
'/sys/user-coupon/count',
{ userId }
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询用户优惠券
*/
export async function getUserCoupon(id: number) {
const res = await request.get<ApiResult<UserCoupon>>(
'/sys/user-coupon/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 使用优惠券
*/
export async function useCoupon(couponId: number, orderId: number) {
const res = await request.put<ApiResult<unknown>>(
'/sys/user-coupon/use',
{ couponId, orderId }
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,45 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 用户优惠券
*/
export interface UserCoupon {
// 优惠券ID
couponId?: number;
// 用户ID
userId?: number;
// 优惠券名称
name?: string;
// 优惠券类型 1-满减券 2-折扣券 3-免费券
type?: number;
// 优惠券金额/折扣
value?: string;
// 使用门槛金额
minAmount?: string;
// 有效期开始时间
startTime?: string;
// 有效期结束时间
endTime?: string;
// 使用状态 0-未使用 1-已使用 2-已过期
status?: number;
// 使用时间
useTime?: string;
// 关联订单ID
orderId?: number;
// 备注
comments?: string;
// 创建时间
createTime?: string;
// 更新时间
updateTime?: string;
}
/**
* 用户优惠券搜索条件
*/
export interface UserCouponParam extends PageParam {
userId?: number;
type?: number;
status?: number;
name?: string;
}

View File

@@ -7,7 +7,7 @@ import {loginByOpenId} from "@/api/layout";
import {TenantId} from "@/config/app";
import {saveStorageByLoginUser} from "@/utils/server";
function App(props) {
function App(props: { children: any; }) {
const reload = () => {
Taro.login({
success: (res) => {

View File

@@ -117,12 +117,16 @@
.coupon-right {
flex: 1;
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 16px;
.coupon-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.coupon-title {
font-size: 32px;
@@ -143,6 +147,7 @@
display: flex;
justify-content: flex-end;
align-items: center;
flex-shrink: 0;
.coupon-btn {
min-width: 120px;

View File

@@ -4,20 +4,32 @@ import { Button } from '@nutui/nutui-react-taro'
import './CouponCard.scss'
export interface CouponCardProps {
/** 优惠券ID */
id?: string
/** 优惠券金额 */
amount: number
/** 最低消费金额 */
minAmount?: number
/** 优惠券类型1-满减券 2-折扣券 3-免费券 */
type?: 1 | 2 | 3
/** 优惠券类型10-满减券 20-折扣券 30-免费券 */
type?: 10 | 20 | 30
/** 优惠券状态0-未使用 1-已使用 2-已过期 */
status?: 0 | 1 | 2
/** 状态文本描述(后端返回) */
statusText?: string
/** 优惠券标题 */
title?: string
/** 优惠券描述 */
description?: string
/** 有效期开始时间 */
startTime?: string
/** 有效期结束时间 */
endTime?: string
/** 是否即将过期(后端计算) */
isExpiringSoon?: boolean
/** 剩余天数(后端计算) */
daysRemaining?: number
/** 剩余小时数(后端计算) */
hoursRemaining?: number
/** 是否显示领取按钮 */
showReceiveBtn?: boolean
/** 是否显示使用按钮 */
@@ -33,11 +45,15 @@ export interface CouponCardProps {
const CouponCard: React.FC<CouponCardProps> = ({
amount,
minAmount,
type = 1,
type = 10,
status = 0,
statusText,
title,
startTime,
endTime,
isExpiringSoon,
daysRemaining,
hoursRemaining,
showReceiveBtn = false,
showUseBtn = false,
onReceive,
@@ -49,14 +65,13 @@ const CouponCard: React.FC<CouponCardProps> = ({
return `theme-${theme}`
}
// 格式化优惠券金额显示
// @ts-ignore
const formatAmount = () => {
switch (type) {
case 1: // 满减券
case 10: // 满减券
return `¥${amount}`
case 2: // 折扣券
case 20: // 折扣券
return `${amount}`
case 3: // 免费券
case 30: // 免费券
return '免费'
default:
return `¥${amount}`
@@ -65,21 +80,27 @@ const CouponCard: React.FC<CouponCardProps> = ({
// 获取优惠券状态文本
const getStatusText = () => {
// 优先使用后端返回的状态文本
if (statusText) {
return statusText
}
// 兜底逻辑
switch (status) {
case 0:
return '未使用'
return '用'
case 1:
return '已使用'
case 2:
return '已过期'
default:
return '未使用'
return '用'
}
}
// 获取使用条件文本
const getConditionText = () => {
if (type === 3) return '免费使用' // 免费券
if (type === 30) return '免费使用' // 免费券
if (minAmount && minAmount > 0) {
return `${minAmount}元可用`
}
@@ -88,15 +109,40 @@ const CouponCard: React.FC<CouponCardProps> = ({
// 格式化有效期显示
const formatValidityPeriod = () => {
if (!startTime || !endTime) return ''
// 第一优先级:使用后端返回的状态文本
if (statusText) {
return statusText
}
// 第二优先级:根据状态码显示
if (status === 2) {
return '已过期'
}
if (status === 1) {
return '已使用'
}
// 第三优先级:使用后端计算的剩余时间
if (isExpiringSoon && daysRemaining !== undefined) {
if (daysRemaining <= 0 && hoursRemaining !== undefined) {
return `${hoursRemaining}小时后过期`
}
return `${daysRemaining}天后过期`
}
// 兜底逻辑:使用前端计算
if (!endTime) return '可用'
const start = new Date(startTime)
const end = new Date(endTime)
const now = new Date()
// 如果还未开始
if (now < start) {
return `${start.getMonth() + 1}.${start.getDate()} 开始生效`
if (startTime) {
const start = new Date(startTime)
// 如果还未开始
if (now < start) {
return `${start.getMonth() + 1}.${start.getDate()} 开始生效`
}
}
// 计算剩余天数
@@ -112,25 +158,6 @@ const CouponCard: React.FC<CouponCardProps> = ({
}
}
// 格式化日期
const formatDate = (dateStr?: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return `${date.getMonth() + 1}.${date.getDate()}`
}
// 获取有效期文本
const getValidityText = () => {
if (startTime && endTime) {
return `${formatDate(startTime)}-${formatDate(endTime)}`
}
return ''
}
console.log(getValidityText)
const themeClass = getThemeClass()
return (
@@ -138,7 +165,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
{/* 左侧金额区域 */}
<View className={`coupon-left ${themeClass}`}>
<View className="amount-wrapper">
{type !== 3 && <Text className="currency">¥</Text>}
{type !== 30 && <Text className="currency">¥</Text>}
<Text className="amount">{formatAmount()}</Text>
</View>
<View className="condition">
@@ -157,7 +184,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
<View className="coupon-right">
<View className="coupon-info">
<View className="coupon-title">
{title || (type === 1 ? '满减券' : type === 2 ? '折扣券' : '免费券')}
{title || (type === 10 ? '满减券' : type === 20 ? '折扣券' : '免费券')}
</View>
<View className="coupon-validity">
{formatValidityPeriod()}

View File

@@ -73,12 +73,12 @@ const SpecSelector: React.FC<SpecSelectorProps> = ({
}, [selectedSpecs, skus, specGroups]);
// 选择规格值
const handleSpecSelect = (specName: string, specValue: string) => {
setSelectedSpecs(prev => ({
...prev,
[specName]: specValue
}));
};
// const handleSpecSelect = (specName: string, specValue: string) => {
// setSelectedSpecs(prev => ({
// ...prev,
// [specName]: specValue
// }));
// };
// 确认选择
const handleConfirm = () => {
@@ -89,21 +89,21 @@ const SpecSelector: React.FC<SpecSelectorProps> = ({
};
// 检查规格值是否可选是否有对应的SKU且有库存
const isSpecValueAvailable = (specName: string, specValue: string) => {
const testSpecs = { ...selectedSpecs, [specName]: specValue };
// 如果还有其他规格未选择,则认为可选
if (Object.keys(testSpecs).length < specGroups.length) {
return true;
}
// 构建规格值字符串
const sortedSpecNames = specGroups.map(g => g.specName).sort();
const specValues = sortedSpecNames.map(name => testSpecs[name]).join('|');
const sku = skus.find(s => s.sku === specValues);
return sku && sku.stock && sku.stock > 0 && sku.status === 0;
};
// const isSpecValueAvailable = (specName: string, specValue: string) => {
// const testSpecs = { ...selectedSpecs, [specName]: specValue };
//
// // 如果还有其他规格未选择,则认为可选
// if (Object.keys(testSpecs).length < specGroups.length) {
// return true;
// }
//
// // 构建规格值字符串
// const sortedSpecNames = specGroups.map(g => g.specName).sort();
// const specValues = sortedSpecNames.map(name => testSpecs[name]).join('|');
//
// const sku = skus.find(s => s.sku === specValues);
// return sku && sku.stock && sku.stock > 0 && sku.status === 0;
// };
return (
<Popup

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

View File

@@ -97,7 +97,8 @@ export const useUser = () => {
} catch (error) {
console.error('获取用户信息失败:', error);
// 如果获取失败可能是token过期清除登录状态
if (error.message?.includes('401') || error.message?.includes('未授权')) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage?.includes('401') || errorMessage?.includes('未授权')) {
logoutUser();
}
return null;

View File

@@ -2,21 +2,23 @@ import {useEffect, useState} from "react";
import Taro from '@tarojs/taro';
import {Button, Space} from '@nutui/nutui-react-taro'
import {TriangleDown} from '@nutui/icons-react-taro'
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
import {getShopInfo, getUserInfo, getWxOpenId} from "@/api/layout";
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
import {getUserInfo, getWxOpenId} from "@/api/layout";
import {TenantId} from "@/config/app";
import {getOrganization} from "@/api/system/organization";
import {myUserVerify} from "@/api/system/userVerify";
import {CmsWebsite} from "@/api/cms/cmsWebsite/model";
import {User} from "@/api/system/user/model";
import { useShopInfo } from '@/hooks/useShopInfo';
import MySearch from "./MySearch";
import './Header.scss';
const Header = (props: any) => {
const [userInfo, setUserInfo] = useState<User>()
// 使用新的useShopInfo Hook
const {
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
const [IsLogin, setIsLogin] = useState<boolean>(true)
const [config, setConfig] = useState<CmsWebsite>()
const [showBasic, setShowBasic] = useState(false)
const [statusBarHeight, setStatusBarHeight] = useState<number>()
const reload = async () => {
@@ -25,16 +27,11 @@ const Header = (props: any) => {
setStatusBarHeight(res.statusBarHeight)
},
})
// 获取站点信息
getShopInfo().then((data) => {
setConfig(data);
console.log(userInfo)
})
// 注意商店信息现在通过useShopInfo自动管理不需要手动获取
// 获取用户信息
getUserInfo().then((data) => {
if (data) {
setIsLogin(true);
setUserInfo(data)
console.log('用户信息>>>', data.phone)
// 保存userId
Taro.setStorageSync('UserId', data.userId)
@@ -87,7 +84,7 @@ const Header = (props: any) => {
}
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}) => {
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function () {
@@ -157,9 +154,9 @@ const Header = (props: any) => {
<Space>
<Avatar
size="22"
src={config?.websiteLogo}
src={getWebsiteLogo()}
/>
<span style={{color: '#000'}}>{config?.websiteName}</span>
<span style={{color: '#000'}}>{getWebsiteName()}</span>
</Space>
</Button>
<TriangleDown size={9}/>
@@ -168,23 +165,13 @@ const Header = (props: any) => {
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
<Avatar
size="22"
src={config?.websiteLogo}
src={getWebsiteLogo()}
/>
<span className={'text-white'}>{config?.websiteName}</span>
<span className={'text-white'}>{getWebsiteName()}</span>
<TriangleDown className={'text-white'} size={9}/>
</div>
)}>
</NavBar>
<Popup
visible={showBasic}
position="bottom"
style={{width: '100%', height: '100%'}}
onClose={() => {
setShowBasic(false)
}}
>
<div style={{padding: '12px 0', fontWeight: 'bold', textAlign: 'center'}}></div>
</Popup>
</>
)
}

View File

@@ -0,0 +1,205 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro';
import {Button, Space} from '@nutui/nutui-react-taro'
import {TriangleDown} from '@nutui/icons-react-taro'
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
import {getUserInfo, getWxOpenId} from "@/api/layout";
import {TenantId} from "@/config/app";
import {getOrganization} from "@/api/system/organization";
import {myUserVerify} from "@/api/system/userVerify";
import {User} from "@/api/system/user/model";
import { useShopInfo } from '@/hooks/useShopInfo';
import { useUser } from '@/hooks/useUser';
import MySearch from "./MySearch";
import './Header.scss';
const Header = (props: any) => {
// 使用新的hooks
const {
shopInfo,
loading: shopLoading,
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
const {
user,
isLoggedIn,
loading: userLoading
} = useUser();
const [showBasic, setShowBasic] = useState(false)
const [statusBarHeight, setStatusBarHeight] = useState<number>()
const reload = async () => {
Taro.getSystemInfo({
success: (res) => {
setStatusBarHeight(res.statusBarHeight)
},
})
// 注意商店信息现在通过useShopInfo自动管理不需要手动获取
// 用户信息现在通过useUser自动管理不需要手动获取
// 如果需要获取openId可以在用户登录后处理
if (user && !user.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
console.log('OpenId获取成功');
})
}
})
}
// 检查用户认证状态
if (user?.userId) {
// 获取组织信息
getOrganization({userId: user.userId}).then((data) => {
console.log('组织信息>>>', data)
}).catch(() => {
console.log('获取组织信息失败')
});
// 检查用户认证
myUserVerify({userId: user.userId}).then((data) => {
console.log('认证信息>>>', data)
}).catch(() => {
console.log('获取认证信息失败')
});
}
}
// 获取手机号授权
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function () {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: 0,
sceneType: 'save_referee',
tenantId: TenantId
},
success: function (res) {
if (res.data.code == 1) {
Taro.showToast({
title: res.data.message,
icon: 'error',
duration: 2000
})
return false;
}
// 登录成功
Taro.setStorageSync('access_token', res.data.data.access_token)
Taro.setStorageSync('UserId', res.data.data.user.userId)
// 重新加载小程序
Taro.reLaunch({
url: '/pages/index/index'
})
}
})
} else {
console.log('登录失败!')
}
}
})
}
useEffect(() => {
reload().then()
}, [])
// 显示加载状态
if (shopLoading || userLoading) {
return (
<div className={'fixed top-0 header-bg'} style={{
height: !props.stickyStatus ? '180px' : '148px',
}}>
<div style={{padding: '20px', textAlign: 'center', color: '#fff'}}>
...
</div>
</div>
);
}
return (
<>
<div className={'fixed top-0 header-bg'} style={{
height: !props.stickyStatus ? '180px' : '148px',
}}>
<MySearch/>
</div>
<NavBar
style={{marginTop: `${statusBarHeight}px`, marginBottom: '0px', backgroundColor: 'transparent'}}
onBackClick={() => {
}}
left={
!isLoggedIn ? (
<div style={{display: 'flex', alignItems: 'center'}}>
<Button style={{color: '#000'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Space>
<Avatar
size="22"
src={getWebsiteLogo()}
/>
<span style={{color: '#000'}}>{getWebsiteName()}</span>
</Space>
</Button>
<TriangleDown size={9}/>
</div>
) : (
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
<Avatar
size="22"
src={getWebsiteLogo()}
/>
<span className={'text-white'}>{getWebsiteName()}</span>
<TriangleDown className={'text-white'} size={9}/>
</div>
)}>
</NavBar>
<Popup
visible={showBasic}
position="bottom"
style={{width: '100%', height: '100%'}}
onClose={() => {
setShowBasic(false)
}}
>
<div style={{padding: '20px'}}>
<h3></h3>
<div>: {getWebsiteName()}</div>
<div>Logo: <img src={getWebsiteLogo()} alt="logo" style={{width: '50px', height: '50px'}} /></div>
<h3></h3>
<div>: {isLoggedIn ? '已登录' : '未登录'}</div>
{user && (
<>
<div>ID: {user.userId}</div>
<div>: {user.phone}</div>
<div>: {user.nickname}</div>
</>
)}
<button
onClick={() => setShowBasic(false)}
style={{marginTop: '20px', padding: '10px 20px'}}
>
</button>
</div>
</Popup>
</>
)
}
export default Header;

View File

@@ -0,0 +1,189 @@
import Header from './Header';
import BestSellers from './BestSellers';
import Taro from '@tarojs/taro';
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
import {useEffect, useState} from "react";
import {Sticky} from '@nutui/nutui-react-taro'
import { useShopInfo } from '@/hooks/useShopInfo';
import { useUser } from '@/hooks/useUser';
import Menu from "./Menu";
import Banner from "./Banner";
import './index.scss'
const Home = () => {
const [stickyStatus, setStickyStatus] = useState(false);
// 使用新的hooks
const {
shopInfo,
loading: shopLoading,
error: shopError,
getWebsiteName,
getWebsiteLogo,
refreshShopInfo
} = useShopInfo();
const {
user,
isLoggedIn,
loading: userLoading
} = useUser();
const onSticky = (args: any) => {
setStickyStatus(args[0].isFixed);
};
const showAuthModal = () => {
Taro.showModal({
title: '授权提示',
content: '需要获取您的用户信息',
confirmText: '去授权',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户点击确认,打开授权设置页面
Taro.openSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.userInfo']) {
console.log('用户已授权');
} else {
console.log('用户拒绝授权');
}
}
});
}
}
});
};
// 分享给好友
useShareAppMessage(() => {
return {
title: `${getWebsiteName()} - 精选商城`,
path: '/pages/index/index',
imageUrl: getWebsiteLogo(),
success: function (res: any) {
console.log('分享成功', res);
Taro.showToast({
title: '分享成功',
icon: 'success',
duration: 2000
});
},
fail: function (res: any) {
console.log('分享失败', res);
Taro.showToast({
title: '分享失败',
icon: 'none',
duration: 2000
});
}
};
});
// 分享到朋友圈
useShareTimeline(() => {
return {
title: `${getWebsiteName()} - 精选商城`,
imageUrl: getWebsiteLogo(),
success: function (res: any) {
console.log('分享到朋友圈成功', res);
},
fail: function (res: any) {
console.log('分享到朋友圈失败', res);
}
};
});
useEffect(() => {
// 设置页面标题
if (shopInfo?.appName) {
Taro.setNavigationBarTitle({
title: shopInfo.appName
});
}
}, [shopInfo]);
useEffect(() => {
// 检查用户授权状态
Taro.getSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
console.log('用户已经授权过,可以直接获取用户信息');
} else {
console.log('用户未授权,需要弹出授权窗口');
showAuthModal();
}
}
});
// 获取用户基本信息(头像、昵称等)
Taro.getUserInfo({
success: (res) => {
const avatar = res.userInfo.avatarUrl;
console.log('用户头像:', avatar);
},
fail: (err) => {
console.log('获取用户信息失败:', err);
}
});
}, []);
// 处理错误状态
if (shopError) {
return (
<div style={{padding: '20px', textAlign: 'center'}}>
<div>: {shopError}</div>
<button
onClick={refreshShopInfo}
style={{marginTop: '10px', padding: '10px 20px'}}
>
</button>
</div>
);
}
// 显示加载状态
if (shopLoading) {
return (
<div style={{padding: '20px', textAlign: 'center'}}>
<div>...</div>
</div>
);
}
return (
<>
<Sticky threshold={0} onChange={(args) => onSticky(args)}>
<Header stickyStatus={stickyStatus}/>
</Sticky>
<div className={'flex flex-col mt-12'}>
<Menu/>
<Banner/>
<BestSellers/>
{/* 调试信息面板 - 仅在开发环境显示 */}
{process.env.NODE_ENV === 'development' && (
<div style={{
position: 'fixed',
bottom: '10px',
right: '10px',
background: 'rgba(0,0,0,0.8)',
color: 'white',
padding: '10px',
borderRadius: '5px',
fontSize: '12px',
maxWidth: '200px'
}}>
<div>: {getWebsiteName()}</div>
<div>: {isLoggedIn ? (user?.nickname || '已登录') : '未登录'}</div>
<div>: {userLoading ? '用户加载中' : '已完成'}</div>
</div>
)}
</div>
</>
)
}
export default Home;

View File

@@ -26,11 +26,11 @@ function Home() {
return {
title: '网宿小店 - 网宿软件',
path: `/pages/index/index`,
success: function (res) {
console.log('分享成功', res);
success: function () {
console.log('分享成功');
},
fail: function (res) {
console.log('分享失败', res);
fail: function () {
console.log('分享失败');
}
};
});
@@ -72,7 +72,7 @@ function Home() {
});
};
const onSticky = (item) => {
const onSticky = (item: IArguments) => {
if(item){
setStickyStatus(!stickyStatus)
}

View File

@@ -6,7 +6,7 @@ import {useEffect, useState} from "react";
import {User} from "@/api/system/user/model";
import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {getUserCouponCount} from "@/api/user/coupon";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {getUserPointsStats} from "@/api/user/points";
import {useUser} from "@/hooks/useUser";
@@ -37,9 +37,9 @@ function UserCard() {
const loadUserStats = (userId: number) => {
// 加载优惠券数量
getUserCouponCount(userId)
.then((res: any) => {
setCouponCount(res.unused || 0)
getMyAvailableCoupons()
.then((coupons: any) => {
setCouponCount(coupons?.length || 0)
})
.catch((error: any) => {
console.error('Coupon count error:', error)
@@ -132,7 +132,7 @@ function UserCard() {
};
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}) => {
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function () {

View File

@@ -83,6 +83,15 @@
overflow-y: auto;
}
&__loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: #999;
font-size: 14px;
}
&__current {
padding: 16px;
background: #f8f9fa;

View File

@@ -26,6 +26,16 @@ import {PaymentHandler, PaymentType, buildSingleGoodsOrder} from "@/utils/paymen
import OrderConfirmSkeleton from "@/components/OrderConfirmSkeleton";
import CouponList from "@/components/CouponList";
import {CouponCardProps} from "@/components/CouponCard";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {
transformCouponData,
calculateCouponDiscount,
isCouponUsable,
getCouponUnusableReason,
sortCoupons,
filterUsableCoupons,
filterUnusableCoupons
} from "@/utils/couponUtils";
const OrderConfirm = () => {
@@ -53,38 +63,8 @@ const OrderConfirm = () => {
// 优惠券相关状态
const [selectedCoupon, setSelectedCoupon] = useState<CouponCardProps | null>(null)
const [couponVisible, setCouponVisible] = useState<boolean>(false)
const [availableCoupons] = useState<CouponCardProps[]>([
{
amount: 5,
minAmount: 20,
type: 1,
status: 0,
title: '满20减5',
startTime: '2024-01-01',
endTime: '2024-12-31',
theme: 'red'
},
{
amount: 10,
minAmount: 50,
type: 1,
status: 0,
title: '满50减10',
startTime: '2024-01-01',
endTime: '2024-12-31',
theme: 'orange'
},
{
amount: 20,
minAmount: 100,
type: 1,
status: 0,
title: '满100减20',
startTime: '2024-01-01',
endTime: '2024-12-31',
theme: 'blue'
}
])
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
const [couponLoading, setCouponLoading] = useState<boolean>(false)
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.goodsId;
@@ -99,22 +79,7 @@ const OrderConfirm = () => {
const getCouponDiscount = () => {
if (!selectedCoupon || !goods) return 0
const total = getGoodsTotal()
// 检查是否满足使用条件
if (selectedCoupon.minAmount && total < selectedCoupon.minAmount) {
return 0
}
switch (selectedCoupon.type) {
case 1: // 满减券
return selectedCoupon.amount
case 2: // 折扣券
return total * (1 - selectedCoupon.amount / 10)
case 3: // 免费券
return total
default:
return 0
}
return calculateCouponDiscount(selectedCoupon, total)
}
// 计算实付金额
@@ -133,17 +98,35 @@ const OrderConfirm = () => {
// 处理数量变化
const handleQuantityChange = (value: string | number) => {
const newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value
setQuantity(Math.max(1, Math.min(newQuantity, goods?.stock || 999)))
const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999))
setQuantity(finalQuantity)
// 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用
if (availableCoupons.length > 0) {
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
setAvailableCoupons(sortedCoupons)
// 检查当前选中的优惠券是否还可用
if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) {
setSelectedCoupon(null)
Taro.showToast({
title: '当前优惠券不满足使用条件,已自动取消',
icon: 'none'
})
}
}
}
// 处理优惠券选择
const handleCouponSelect = (coupon: CouponCardProps) => {
const total = getGoodsTotal()
// 检查是否满足使用条件
if (coupon.minAmount && total < coupon.minAmount) {
// 检查是否可用
if (!isCouponUsable(coupon, total)) {
const reason = getCouponUnusableReason(coupon, total)
Taro.showToast({
title: `需满${coupon.minAmount}元才能使用此优惠券`,
title: reason || '优惠券不可用',
icon: 'none'
})
return
@@ -166,6 +149,45 @@ const OrderConfirm = () => {
})
}
// 加载用户优惠券
const loadUserCoupons = async () => {
try {
setCouponLoading(true)
// 使用新的API获取可用优惠券
const res = await getMyAvailableCoupons()
if (res && res.length > 0) {
// 转换数据格式
const transformedCoupons = res.map(transformCouponData)
// 按优惠金额排序
const total = getGoodsTotal()
const sortedCoupons = sortCoupons(transformedCoupons, total)
setAvailableCoupons(sortedCoupons)
console.log('加载优惠券成功:', {
originalData: res,
transformedData: transformedCoupons,
sortedData: sortedCoupons
})
} else {
setAvailableCoupons([])
console.log('暂无可用优惠券')
}
} catch (error) {
console.error('加载优惠券失败:', error)
setAvailableCoupons([])
Taro.showToast({
title: '加载优惠券失败',
icon: 'none'
})
} finally {
setCouponLoading(false)
}
}
/**
* 统一支付入口
*/
@@ -208,7 +230,7 @@ const OrderConfirm = () => {
comments: goods.name,
deliveryType: 0,
buyerRemarks: orderRemark,
couponId: selectedCoupon ? selectedCoupon.amount : undefined
couponId: selectedCoupon ? selectedCoupon.id : undefined
}
);
@@ -268,6 +290,11 @@ const OrderConfirm = () => {
})))
setPayment(paymentRes[0])
}
// 加载优惠券(在商品信息加载完成后)
if (goodsRes) {
await loadUserCoupons()
}
} catch (err) {
console.error('加载数据失败:', err)
setError('加载数据失败,请重试')
@@ -456,35 +483,54 @@ const OrderConfirm = () => {
</View>
<View className="coupon-popup__content">
{selectedCoupon && (
<View className="coupon-popup__current">
<Text className="coupon-popup__current-title font-medium">使</Text>
<View className="coupon-popup__current-item">
<Text>{selectedCoupon.title} -{selectedCoupon.amount}</Text>
<Button size="small" onClick={handleCouponCancel}>使</Button>
</View>
{couponLoading ? (
<View className="coupon-popup__loading">
<Text>...</Text>
</View>
) : (
<>
{selectedCoupon && (
<View className="coupon-popup__current">
<Text className="coupon-popup__current-title font-medium">使</Text>
<View className="coupon-popup__current-item">
<Text>{selectedCoupon.title} -{calculateCouponDiscount(selectedCoupon, getGoodsTotal()).toFixed(2)}</Text>
<Button size="small" onClick={handleCouponCancel}>使</Button>
</View>
</View>
)}
{(() => {
const total = getGoodsTotal()
const usableCoupons = filterUsableCoupons(availableCoupons, total)
const unusableCoupons = filterUnusableCoupons(availableCoupons, total)
return (
<>
<CouponList
title={`可用优惠券 (${usableCoupons.length})`}
coupons={usableCoupons}
layout="vertical"
onCouponClick={handleCouponSelect}
showEmpty={usableCoupons.length === 0}
emptyText="暂无可用优惠券"
/>
{unusableCoupons.length > 0 && (
<CouponList
title={`不可用优惠券 (${unusableCoupons.length})`}
coupons={unusableCoupons.map(coupon => ({
...coupon,
status: 2 as const
}))}
layout="vertical"
showEmpty={false}
/>
)}
</>
)
})()}
</>
)}
<CouponList
title="可用优惠券"
coupons={availableCoupons.filter(coupon => {
const total = getGoodsTotal()
return !coupon.minAmount || total >= coupon.minAmount
})}
layout="vertical"
onCouponClick={handleCouponSelect}
/>
<CouponList
title="不可用优惠券"
coupons={availableCoupons.filter(coupon => {
const total = getGoodsTotal()
return coupon.minAmount && total < coupon.minAmount
}).map(coupon => ({...coupon, status: 2 as const}))}
layout="vertical"
showEmpty={false}
/>
</View>
</View>
</Popup>

View File

@@ -1,4 +1,4 @@
import {useEffect, useState} from 'react'
import {SetStateAction, useEffect, useState} from 'react'
import {useRouter} from '@tarojs/taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
@@ -46,7 +46,7 @@ const SearchPage = () => {
try {
let history = Taro.getStorageSync('search_history') || []
// 去重并添加到开头
history = history.filter(item => item !== keyword)
history = history.filter((item: string) => item !== keyword)
history.unshift(keyword)
// 只保留最近10条
history = history.slice(0, 10)
@@ -57,9 +57,9 @@ const SearchPage = () => {
}
}
const handleKeywords = (keywords) => {
const handleKeywords = (keywords: SetStateAction<string>) => {
setKeywords(keywords)
handleSearch(keywords).then()
handleSearch(typeof keywords === "string" ? keywords : '').then()
}
// 搜索商品

View File

@@ -1,8 +1,8 @@
import {useState, useEffect, CSSProperties} from 'react'
import Taro from '@tarojs/taro'
import {Cell, InfiniteLoading, Tabs, TabPane, Tag, Empty, ConfigProvider} from '@nutui/nutui-react-taro'
import {pageUserCoupon, getUserCouponCount} from "@/api/user/coupon";
import {UserCoupon as UserCouponType} from "@/api/user/coupon/model";
import {pageShopUserCoupon as pageUserCoupon, getMyAvailableCoupons, getMyUsedCoupons, getMyExpiredCoupons} from "@/api/shop/shopUserCoupon";
import {ShopUserCoupon as UserCouponType} from "@/api/shop/shopUserCoupon/model";
import {View} from '@tarojs/components'
const InfiniteUlStyle: CSSProperties = {
@@ -71,17 +71,26 @@ const UserCoupon = () => {
})
}
const loadCouponCount = () => {
const loadCouponCount = async () => {
const userId = Taro.getStorageSync('UserId')
if (!userId) return
getUserCouponCount(parseInt(userId))
.then((res: any) => {
setCouponCount(res)
})
.catch((error: any) => {
console.error('Coupon count error:', error)
try {
// 并行获取各种状态的优惠券数量
const [availableCoupons, usedCoupons, expiredCoupons] = await Promise.all([
getMyAvailableCoupons().catch(() => []),
getMyUsedCoupons().catch(() => []),
getMyExpiredCoupons().catch(() => [])
])
setCouponCount({
unused: availableCoupons.length || 0,
used: usedCoupons.length || 0,
expired: expiredCoupons.length || 0
})
} catch (error) {
console.error('Coupon count error:', error)
}
}
const onTabChange = (index: string) => {
@@ -97,9 +106,9 @@ const UserCoupon = () => {
const getCouponTypeText = (type?: number) => {
switch (type) {
case 1: return '满减券'
case 2: return '折扣券'
case 3: return '免费券'
case 10: return '满减券'
case 20: return '折扣券'
case 30: return '免费券'
default: return '优惠券'
}
}

View File

@@ -1,10 +1,20 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Empty, ConfigProvider, SearchBar, InfiniteLoading, Loading, PullToRefresh, Tabs, TabPane} from '@nutui/nutui-react-taro'
import {
Button,
Empty,
ConfigProvider,
SearchBar,
InfiniteLoading,
Loading,
PullToRefresh,
Tabs,
TabPane
} from '@nutui/nutui-react-taro'
import {Plus, Filter} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ShopUserCoupon} from "@/api/shop/shopUserCoupon/model";
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
import {pageShopUserCoupon, getMyAvailableCoupons, getMyUsedCoupons, getMyExpiredCoupons} from "@/api/shop/shopUserCoupon";
import CouponList from "@/components/CouponList";
import CouponStats from "@/components/CouponStats";
import CouponGuide from "@/components/CouponGuide";
@@ -12,6 +22,7 @@ import CouponFilter from "@/components/CouponFilter";
import CouponExpireNotice, {ExpiringSoon} from "@/components/CouponExpireNotice";
import {CouponCardProps} from "@/components/CouponCard";
import dayjs from "dayjs";
import {transformCouponData} from "@/utils/couponUtils";
const CouponManage = () => {
const [list, setList] = useState<ShopUserCoupon[]>([])
@@ -20,7 +31,7 @@ const CouponManage = () => {
const [searchValue, setSearchValue] = useState('')
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
console.log('total = ',total)
console.log('total = ', total)
const [activeTab, setActiveTab] = useState('0') // 0-可用 1-已使用 2-已过期
const [stats, setStats] = useState({
available: 0,
@@ -39,77 +50,9 @@ const CouponManage = () => {
})
// 获取优惠券状态过滤条件
const getStatusFilter = () => {
switch (activeTab) {
case '0': // 可用
return { status: 0, isExpire: 0 }
case '1': // 已使用
return { status: 1 }
case '2': // 已过期
return { isExpire: 1 }
default:
return {}
}
}
const reload = async (isRefresh = false) => {
if (isRefresh) {
setPage(1)
setList([])
setHasMore(true)
}
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const statusFilter = getStatusFilter()
const res = await pageShopUserCoupon({
page: currentPage,
limit: 10,
keywords: searchValue,
...statusFilter,
// 应用筛选条件
...(filters.type.length > 0 && { type: filters.type[0] }),
...(filters.minAmount && { minAmount: filters.minAmount }),
sortBy: filters.sortBy,
sortOrder: filters.sortOrder
})
if (res && res.list) {
const newList = isRefresh ? res.list : [...list, ...res.list]
console.log('优惠券数据加载成功:', {
isRefresh,
currentPage,
statusFilter,
responseData: res,
newListLength: newList.length,
activeTab
})
setList(newList)
setTotal(res.count || 0)
// 判断是否还有更多数据
setHasMore(res.list.length === 10) // 如果返回的数据等于limit说明可能还有更多
if (!isRefresh) {
setPage(currentPage + 1)
} else {
setPage(2) // 刷新后下一页是第2页
}
} else {
console.log('优惠券数据为空:', res)
setHasMore(false)
setTotal(0)
}
} catch (error) {
console.error('获取优惠券失败:', error)
Taro.showToast({
title: '获取优惠券失败',
icon: 'error'
});
} finally {
setLoading(false)
}
// 直接调用reloadWithTab使用当前的activeTab
await reloadWithTab(activeTab, isRefresh)
}
// 搜索功能
@@ -126,84 +69,140 @@ const CouponManage = () => {
// Tab切换
const handleTabChange = (value: string | number) => {
const tabValue = String(value)
console.log('Tab切换:', { from: activeTab, to: tabValue })
console.log('Tab切换:', {from: activeTab, to: tabValue})
setActiveTab(tabValue)
setPage(1)
setList([])
setHasMore(true)
// 延迟执行reload确保状态更新完成
setTimeout(() => {
reload(true)
}, 100)
// 直接调用reload传入新的tab值
reloadWithTab(tabValue)
}
// 转换优惠券数据为CouponCard组件所需格式
const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps => {
console.log('转换优惠券数据:', coupon)
// 判断优惠券状态
let status: 0 | 1 | 2 = 0 // 默认未使用
if (coupon.isExpire === 1) {
status = 2 // 已过期
} else if (coupon.status === 1) {
status = 1 // 已使用
// 根据指定tab加载数据
const reloadWithTab = async (tab: string, isRefresh = true) => {
if (isRefresh) {
setPage(1)
setList([])
setHasMore(true)
}
// 根据优惠券类型计算金额显示
let amount = 0
let type: 1 | 2 | 3 = 1
setLoading(true)
try {
let res: any = null
if (coupon.type === 10) { // 满减券
type = 1
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 2
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 3
amount = 0
// 根据tab选择对应的API
switch (tab) {
case '0': // 可用优惠券
res = await getMyAvailableCoupons()
break
case '1': // 已使用优惠券
res = await getMyUsedCoupons()
break
case '2': // 已过期优惠券
res = await getMyExpiredCoupons()
break
default:
res = await getMyAvailableCoupons()
}
console.log('使用Tab加载数据:', { tab, data: res })
if (res && res.length > 0) {
// 应用搜索过滤
let filteredList = res
if (searchValue) {
filteredList = res.filter((item: any) =>
item.name?.includes(searchValue) ||
item.description?.includes(searchValue)
)
}
// 应用其他筛选条件
if (filters.type.length > 0) {
filteredList = filteredList.filter((item: any) =>
filters.type.includes(item.type)
)
}
if (filters.minAmount) {
filteredList = filteredList.filter((item: any) =>
parseFloat(item.minPrice || '0') >= filters.minAmount!
)
}
// 排序
filteredList.sort((a: any, b: any) => {
const aValue = getValueForSort(a, filters.sortBy)
const bValue = getValueForSort(b, filters.sortBy)
if (filters.sortOrder === 'asc') {
return aValue - bValue
} else {
return bValue - aValue
}
})
setList(filteredList)
setTotal(filteredList.length)
setHasMore(false) // 一次性加载所有数据,不需要分页
} else {
setList([])
setTotal(0)
setHasMore(false)
}
} catch (error) {
console.error('获取优惠券失败:', error)
Taro.showToast({
title: '获取优惠券失败',
icon: 'error'
});
setList([])
setTotal(0)
setHasMore(false)
} finally {
setLoading(false)
}
}
// 获取排序值的辅助函数
const getValueForSort = (item: any, sortBy: string) => {
switch (sortBy) {
case 'amount':
return parseFloat(item.reducePrice || item.discount || '0')
case 'expireTime':
return new Date(item.endTime || '').getTime()
case 'createTime':
default:
return new Date(item.createTime || '').getTime()
}
}
// 转换优惠券数据并添加使用按钮
const transformCouponDataWithAction = (coupon: ShopUserCoupon): CouponCardProps => {
console.log('原始优惠券数据:', coupon)
// 使用统一的转换函数
const transformedCoupon = transformCouponData(coupon)
console.log('转换后的优惠券数据:', transformedCoupon)
// 添加使用按钮和点击事件
const result = {
amount,
type,
status,
minAmount: parseFloat(coupon.minPrice || '0'),
title: coupon.name || '优惠券',
startTime: coupon.startTime,
endTime: coupon.endTime,
showUseBtn: status === 0, // 只有未使用的券显示使用按钮
onUse: () => handleUseCoupon(coupon),
theme: getThemeByType(coupon.type)
...transformedCoupon,
showUseBtn: transformedCoupon.status === 0, // 只有未使用的券显示使用按钮
onUse: () => handleUseCoupon(coupon)
}
console.log('转换后的数据:', result)
console.log('最终优惠券数据:', result)
return result
}
// 根据优惠券类型获取主题色
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
switch (type) {
case 10: return 'red' // 满减券
case 20: return 'orange' // 折扣券
case 30: return 'green' // 免费券
default: return 'blue'
}
}
// 使用优惠券
const handleUseCoupon = (coupon: ShopUserCoupon) => {
Taro.showModal({
title: '使用优惠券',
content: `确定要使用"${coupon.name}"吗?`,
success: (res) => {
if (res.confirm) {
// 这里可以跳转到商品页面或购物车页面
Taro.navigateTo({
url: '/pages/index/index'
})
}
}
const handleUseCoupon = (_: ShopUserCoupon) => {
// 这里可以跳转到商品页面或购物车页面
Taro.navigateTo({
url: '/shop/category/index?id=4326'
})
}
@@ -229,18 +228,24 @@ const CouponManage = () => {
try {
// 并行获取各状态的优惠券数量
const [availableRes, usedRes, expiredRes] = await Promise.all([
pageShopUserCoupon({ page: 1, limit: 1, status: 0, isExpire: 0 }),
pageShopUserCoupon({ page: 1, limit: 1, status: 1 }),
pageShopUserCoupon({ page: 1, limit: 1, isExpire: 1 })
getMyAvailableCoupons(),
getMyUsedCoupons(),
getMyExpiredCoupons()
])
setStats({
available: availableRes?.count || 0,
used: usedRes?.count || 0,
expired: expiredRes?.count || 0
available: availableRes?.length || 0,
used: usedRes?.length || 0,
expired: expiredRes?.length || 0
})
} catch (error) {
console.error('获取优惠券统计失败:', error)
// 设置默认值
setStats({
available: 0,
used: 0,
expired: 0
})
}
}
@@ -265,7 +270,7 @@ const CouponManage = () => {
try {
// 获取即将过期的优惠券3天内过期
const res = await pageShopUserCoupon({
page: 1,
page: page,
limit: 50,
status: 0, // 未使用
isExpire: 0 // 未过期
@@ -283,7 +288,7 @@ const CouponManage = () => {
name: coupon.name || '',
type: coupon.type || 10,
amount: coupon.type === 10 ? coupon.reducePrice || '0' :
coupon.type === 20 ? coupon.discount?.toString() || '0' : '0',
coupon.type === 20 ? coupon.discount?.toString() || '0' : '0',
minAmount: coupon.minPrice,
endTime: coupon.endTime || '',
daysLeft
@@ -348,7 +353,7 @@ const CouponManage = () => {
<Button
size="small"
type="primary"
icon={<Plus />}
icon={<Plus/>}
onClick={() => Taro.navigateTo({url: '/user/coupon/receive'})}
>
@@ -356,7 +361,7 @@ const CouponManage = () => {
<Button
size="small"
fill="outline"
icon={<Filter />}
icon={<Filter/>}
onClick={() => setShowFilter(true)}
>
@@ -382,9 +387,9 @@ const CouponManage = () => {
{/* Tab切换 */}
<View className="bg-white">
<Tabs value={activeTab} onChange={handleTabChange}>
<TabPane title="可用" value="0" />
<TabPane title="已使用" value="1" />
<TabPane title="已过期" value="2" />
<TabPane title="可用" value="0"/>
<TabPane title="已使用" value="1"/>
<TabPane title="已过期" value="2"/>
</Tabs>
</View>
@@ -393,50 +398,42 @@ const CouponManage = () => {
onRefresh={handleRefresh}
headHeight={60}
>
<View style={{ height: 'calc(100vh - 200px)', overflowY: 'auto' }} id="coupon-scroll">
{/* 调试信息 */}
<View className="p-2 bg-yellow-100 text-xs text-gray-600">
调试信息: list.length={list.length}, loading={loading.toString()}, activeTab={activeTab}
</View>
{list.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 300px)'}}>
<Empty
description={
activeTab === '0' ? "暂无可用优惠券" :
activeTab === '1' ? "暂无已使用优惠券" :
"暂无已过期优惠券"
}
style={{backgroundColor: 'transparent'}}
/>
</View>
) : (
<InfiniteLoading
target="coupon-scroll"
hasMore={hasMore}
onLoadMore={loadMore}
loadingText={
<View className="flex justify-center items-center py-4">
<Loading />
<View className="ml-2">...</View>
</View>
}
loadMoreText={
<View className="text-center py-4 text-gray-500">
{list.length === 0 ? "暂无数据" : "没有更多了"}
</View>
}
>
<CouponList
coupons={list.map(transformCouponData)}
onCouponClick={handleCouponClick}
showEmpty={false}
/>
{/* 调试:显示转换后的数据 */}
<View className="p-2 bg-blue-100 text-xs text-gray-600">
: {JSON.stringify(list.map(transformCouponData).slice(0, 2), null, 2)}
<View style={{height: 'calc(100vh - 200px)', overflowY: 'auto'}} id="coupon-scroll">
{list.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 300px)'}}>
<Empty
description={
activeTab === '0' ? "暂无可用优惠券" :
activeTab === '1' ? "暂无已使用优惠券" :
"暂无已过期优惠券"
}
style={{backgroundColor: 'transparent'}}
/>
</View>
</InfiniteLoading>
)}
) : (
<InfiniteLoading
target="coupon-scroll"
hasMore={hasMore}
onLoadMore={loadMore}
loadingText={
<View className="flex justify-center items-center py-4">
<Loading/>
<View className="ml-2">...</View>
</View>
}
loadMoreText={
<View className="text-center py-4 text-gray-500">
{list.length === 0 ? "暂无数据" : "没有更多了"}
</View>
}
>
<CouponList
coupons={list.map(transformCouponDataWithAction)}
onCouponClick={handleCouponClick}
showEmpty={false}
/>
</InfiniteLoading>
)}
</View>
</PullToRefresh>

View File

@@ -76,16 +76,16 @@ const CouponReceive = () => {
// 转换优惠券数据为CouponCard组件所需格式
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
let amount = 0
let type: 1 | 2 | 3 = 1
let type: 10 | 20 | 30 = 10 // 使用新的类型值
if (coupon.type === 10) { // 满减券
type = 1
type = 10
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 2
type = 20
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 3
type = 30
amount = 0
}

View File

@@ -17,7 +17,7 @@ const GiftCardManage = () => {
const [searchValue, setSearchValue] = useState('')
const [page, setPage] = useState(1)
// const [total, setTotal] = useState(0)
const [activeTab, setActiveTab] = useState('0') // 0-可用 1-已使用 2-已过期
const [activeTab, setActiveTab] = useState<string | number>('0') // 0-可用 1-已使用 2-已过期
const [stats, setStats] = useState({
available: 0,
used: 0,
@@ -34,7 +34,7 @@ const GiftCardManage = () => {
// 获取礼品卡状态过滤条件
const getStatusFilter = () => {
switch (activeTab) {
switch (String(activeTab)) {
case '0': // 可用
return { useStatus: 0 }
case '1': // 已使用
@@ -108,7 +108,7 @@ const GiftCardManage = () => {
}
// Tab切换
const handleTabChange = (value: string) => {
const handleTabChange = (value: string | number) => {
setActiveTab(value)
setPage(1)
setList([])

View File

@@ -1,7 +1,7 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Empty, ConfigProvider, SearchBar, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
import {Gift, Search} from '@nutui/icons-react-taro'
import {Gift} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
import {pageShopCoupon} from "@/api/shop/shopCoupon";
@@ -31,7 +31,7 @@ const CouponReceive = () => {
page: currentPage,
limit: 10,
keywords: searchValue,
enabled: '1', // 启用状态
enabled: 1, // 启用状态
isExpire: 0 // 未过期
})
@@ -76,16 +76,16 @@ const CouponReceive = () => {
// 转换优惠券数据为CouponCard组件所需格式
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
let amount = 0
let type: 1 | 2 | 3 = 1
let type: 10 | 20 | 30 = 10 // 使用新的类型值
if (coupon.type === 10) { // 满减券
type = 1
type = 10
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 2
type = 20
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 3
type = 30
amount = 0
}
@@ -114,16 +114,16 @@ const CouponReceive = () => {
}
// 领取优惠券
const handleReceiveCoupon = async (coupon: ShopCoupon) => {
const handleReceiveCoupon = async (_: ShopCoupon) => {
try {
// 这里应该调用领取优惠券的API
// await receiveCoupon(coupon.id)
Taro.showToast({
title: '领取成功',
icon: 'success'
})
// 刷新列表
reload(true)
} catch (error) {
@@ -136,7 +136,7 @@ const CouponReceive = () => {
}
// 优惠券点击事件
const handleCouponClick = (coupon: CouponCardProps, index: number) => {
const handleCouponClick = (_: CouponCardProps, index: number) => {
const originalCoupon = list[index]
if (originalCoupon) {
// 显示优惠券详情
@@ -172,7 +172,6 @@ const CouponReceive = () => {
value={searchValue}
onChange={setSearchValue}
onSearch={handleSearch}
leftIcon={<Search />}
/>
</View>

200
src/utils/couponUtils.ts Normal file
View File

@@ -0,0 +1,200 @@
import { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model'
import { CouponCardProps } from '@/components/CouponCard'
/**
* 将后端优惠券数据转换为前端组件所需格式
*/
export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps => {
// 解析金额
let amount = 0
if (coupon.type === 10) {
// 满减券使用reducePrice
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) {
// 折扣券使用discount
amount = coupon.discount || 0
} else if (coupon.type === 30) {
// 免费券金额为0
amount = 0
}
// 解析最低消费金额
const minAmount = parseFloat(coupon.minPrice || '0')
// 确定主题颜色
const getTheme = (type?: number): CouponCardProps['theme'] => {
switch (type) {
case 10: return 'red' // 满减券-红色
case 20: return 'orange' // 折扣券-橙色
case 30: return 'green' // 免费券-绿色
default: return 'blue'
}
}
return {
id: coupon.id,
amount,
minAmount: minAmount > 0 ? minAmount : undefined,
type: coupon.type as 10 | 20 | 30,
status: coupon.status as 0 | 1 | 2,
statusText: coupon.statusText,
title: coupon.name || coupon.description || '优惠券',
description: coupon.description,
startTime: coupon.startTime,
endTime: coupon.endTime,
isExpiringSoon: coupon.isExpiringSoon,
daysRemaining: coupon.daysRemaining,
hoursRemaining: coupon.hoursRemaining,
theme: getTheme(coupon.type)
}
}
/**
* 计算优惠券折扣金额
*/
export const calculateCouponDiscount = (
coupon: CouponCardProps,
totalAmount: number
): number => {
// 检查是否满足使用条件
if (coupon.minAmount && totalAmount < coupon.minAmount) {
return 0
}
// 检查优惠券状态
if (coupon.status !== 0) {
return 0
}
switch (coupon.type) {
case 10: // 满减券
return coupon.amount
case 20: // 折扣券
return totalAmount * (1 - coupon.amount / 10)
case 30: // 免费券
return totalAmount
default:
return 0
}
}
/**
* 检查优惠券是否可用
*/
export const isCouponUsable = (
coupon: CouponCardProps,
totalAmount: number
): boolean => {
// 状态检查
if (coupon.status !== 0) {
return false
}
// 金额条件检查
if (coupon.minAmount && totalAmount < coupon.minAmount) {
return false
}
return true
}
/**
* 获取优惠券不可用原因
*/
export const getCouponUnusableReason = (
coupon: CouponCardProps,
totalAmount: number
): string => {
if (coupon.status === 1) {
return '优惠券已使用'
}
if (coupon.status === 2) {
return '优惠券已过期'
}
if (coupon.minAmount && totalAmount < coupon.minAmount) {
return `需满${coupon.minAmount}元才能使用`
}
return ''
}
/**
* 格式化优惠券标题
*/
export const formatCouponTitle = (coupon: CouponCardProps): string => {
if (coupon.title) {
return coupon.title
}
switch (coupon.type) {
case 10: // 满减券
if (coupon.minAmount && coupon.minAmount > 0) {
return `${coupon.minAmount}${coupon.amount}`
}
return `立减${coupon.amount}`
case 20: // 折扣券
if (coupon.minAmount && coupon.minAmount > 0) {
return `${coupon.minAmount}${coupon.amount}`
}
return `${coupon.amount}折优惠`
case 30: // 免费券
return '免费券'
default:
return '优惠券'
}
}
/**
* 排序优惠券列表
* 按照优惠金额从大到小排序,同等优惠金额按过期时间排序
*/
export const sortCoupons = (
coupons: CouponCardProps[],
totalAmount: number
): CouponCardProps[] => {
return [...coupons].sort((a, b) => {
// 先按可用性排序
const aUsable = isCouponUsable(a, totalAmount)
const bUsable = isCouponUsable(b, totalAmount)
if (aUsable && !bUsable) return -1
if (!aUsable && bUsable) return 1
// 都可用或都不可用时,按优惠金额排序
const aDiscount = calculateCouponDiscount(a, totalAmount)
const bDiscount = calculateCouponDiscount(b, totalAmount)
if (aDiscount !== bDiscount) {
return bDiscount - aDiscount // 优惠金额大的在前
}
// 优惠金额相同时,按过期时间排序(即将过期的在前)
if (a.endTime && b.endTime) {
return new Date(a.endTime).getTime() - new Date(b.endTime).getTime()
}
return 0
})
}
/**
* 过滤可用优惠券
*/
export const filterUsableCoupons = (
coupons: CouponCardProps[],
totalAmount: number
): CouponCardProps[] => {
return coupons.filter(coupon => isCouponUsable(coupon, totalAmount))
}
/**
* 过滤不可用优惠券
*/
export const filterUnusableCoupons = (
coupons: CouponCardProps[],
totalAmount: number
): CouponCardProps[] => {
return coupons.filter(coupon => !isCouponUsable(coupon, totalAmount))
}

View File

@@ -166,7 +166,7 @@ export function buildSingleGoodsOrder(
options?: {
comments?: string;
deliveryType?: number;
couponId?: number;
couponId?: any;
selfTakeMerchantId?: number;
skuId?: number;
specInfo?: string;