docs: 更新优惠券相关文档- 新增优惠券API集成文档
- 新增优惠券卡片对齐修复文档 - 新增优惠券状态显示调试文档 - 新增优惠券组件警告修复文档- 更新用ShopInfo Hook字段迁移文档 - 更新Arguments关键字修复文档
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网站信息记录表搜索条件
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
323
src/hooks/useShopInfo.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import {useState, useEffect, useCallback} from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {AppInfo} from '@/api/cms/cmsWebsite/model';
|
||||
import {getShopInfo} from '@/api/layout';
|
||||
|
||||
// 本地存储键名
|
||||
const SHOP_INFO_STORAGE_KEY = 'shop_info';
|
||||
const SHOP_INFO_CACHE_TIME_KEY = 'shop_info_cache_time';
|
||||
|
||||
// 缓存有效期(毫秒)- 默认30分钟
|
||||
const CACHE_DURATION = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 商店信息Hook
|
||||
* 提供商店信息的获取、缓存和管理功能
|
||||
*/
|
||||
export const useShopInfo = () => {
|
||||
const [shopInfo, setShopInfo] = useState<AppInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 从本地存储加载商店信息
|
||||
const loadShopInfoFromStorage = useCallback(() => {
|
||||
try {
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
const cacheTime = Taro.getStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
|
||||
if (cachedData && cacheTime) {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - cacheTime;
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (timeDiff < CACHE_DURATION) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
setLoading(false);
|
||||
return true; // 返回true表示使用了缓存
|
||||
} else {
|
||||
// 缓存过期,清除旧数据
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商店信息缓存失败:', error);
|
||||
}
|
||||
return false; // 返回false表示没有使用缓存
|
||||
}, []);
|
||||
|
||||
// 保存商店信息到本地存储
|
||||
const saveShopInfoToStorage = useCallback((data: AppInfo) => {
|
||||
try {
|
||||
Taro.setStorageSync(SHOP_INFO_STORAGE_KEY, data);
|
||||
Taro.setStorageSync(SHOP_INFO_CACHE_TIME_KEY, Date.now());
|
||||
} catch (error) {
|
||||
console.error('保存商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从服务器获取商店信息
|
||||
const fetchShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('获取商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
|
||||
// 如果网络请求失败,尝试使用缓存数据(即使过期)
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
if (cachedData) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
console.warn('网络请求失败,使用缓存数据');
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 刷新商店信息
|
||||
const refreshShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('刷新商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 清除缓存
|
||||
const clearCache = useCallback(() => {
|
||||
try {
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
setShopInfo(null);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error('清除商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取应用名称
|
||||
const getAppName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站名称(兼容旧方法名)
|
||||
const getWebsiteName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用Logo
|
||||
const getAppLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站Logo(兼容旧方法名)
|
||||
const getWebsiteLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用图标
|
||||
const getAppIcon = useCallback(() => {
|
||||
return shopInfo?.icon || shopInfo?.logo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取深色模式Logo(AppInfo中无此字段,使用普通Logo)
|
||||
const getDarkLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用域名
|
||||
const getDomain = useCallback(() => {
|
||||
return shopInfo?.domain || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用描述
|
||||
const getDescription = useCallback(() => {
|
||||
return shopInfo?.description || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用关键词
|
||||
const getKeywords = useCallback(() => {
|
||||
return shopInfo?.keywords || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用标题
|
||||
const getTitle = useCallback(() => {
|
||||
return shopInfo?.title || shopInfo?.appName || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取小程序二维码
|
||||
const getMpQrCode = useCallback(() => {
|
||||
return shopInfo?.mpQrCode || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取联系电话(AppInfo中无此字段,从config中获取)
|
||||
const getPhone = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.phone || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取邮箱(AppInfo中无此字段,从config中获取)
|
||||
const getEmail = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.email || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取地址(AppInfo中无此字段,从config中获取)
|
||||
const getAddress = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.address || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取ICP备案号(AppInfo中无此字段,从config中获取)
|
||||
const getIcpNo = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.icpNo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用状态
|
||||
const getStatus = useCallback(() => {
|
||||
return {
|
||||
running: shopInfo?.running || 0,
|
||||
statusText: shopInfo?.statusText || '',
|
||||
statusIcon: shopInfo?.statusIcon || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用配置
|
||||
const getConfig = useCallback(() => {
|
||||
return shopInfo?.config || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用设置
|
||||
const getSetting = useCallback(() => {
|
||||
return shopInfo?.setting || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取服务器时间
|
||||
const getServerTime = useCallback(() => {
|
||||
return shopInfo?.serverTime || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取导航菜单
|
||||
const getNavigation = useCallback(() => {
|
||||
return {
|
||||
topNavs: shopInfo?.topNavs || [],
|
||||
bottomNavs: shopInfo?.bottomNavs || []
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否支持搜索(从config中获取)
|
||||
const isSearchEnabled = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.search === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用版本信息
|
||||
const getVersionInfo = useCallback(() => {
|
||||
return {
|
||||
version: shopInfo?.version || 10,
|
||||
expirationTime: shopInfo?.expirationTime || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查应用是否过期
|
||||
const isExpired = useCallback(() => {
|
||||
return shopInfo?.expired === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取过期天数
|
||||
const getExpiredDays = useCallback(() => {
|
||||
return shopInfo?.expiredDays || 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否即将过期
|
||||
const isSoonExpired = useCallback(() => {
|
||||
return (shopInfo?.soon || 0) > 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 初始化时加载商店信息
|
||||
useEffect(() => {
|
||||
const initShopInfo = async () => {
|
||||
// 先尝试从缓存加载
|
||||
const hasCache = loadShopInfoFromStorage();
|
||||
|
||||
// 如果没有缓存或需要刷新,则从服务器获取
|
||||
if (!hasCache) {
|
||||
await fetchShopInfo();
|
||||
}
|
||||
};
|
||||
|
||||
initShopInfo();
|
||||
}, []); // 空依赖数组,只在组件挂载时执行一次
|
||||
|
||||
return {
|
||||
// 状态
|
||||
shopInfo,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 方法
|
||||
fetchShopInfo,
|
||||
refreshShopInfo,
|
||||
clearCache,
|
||||
|
||||
// 新的工具方法(基于AppInfo字段)
|
||||
getAppName,
|
||||
getAppLogo,
|
||||
getAppIcon,
|
||||
getDescription,
|
||||
getKeywords,
|
||||
getTitle,
|
||||
getMpQrCode,
|
||||
getDomain,
|
||||
getConfig,
|
||||
getSetting,
|
||||
getServerTime,
|
||||
getNavigation,
|
||||
getStatus,
|
||||
getVersionInfo,
|
||||
isExpired,
|
||||
getExpiredDays,
|
||||
isSoonExpired,
|
||||
|
||||
// 兼容旧方法名
|
||||
getWebsiteName,
|
||||
getWebsiteLogo,
|
||||
getDarkLogo,
|
||||
getPhone,
|
||||
getEmail,
|
||||
getAddress,
|
||||
getIcpNo,
|
||||
isSearchEnabled
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
205
src/pages/index/HeaderWithHook.tsx
Normal file
205
src/pages/index/HeaderWithHook.tsx
Normal 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;
|
||||
189
src/pages/index/IndexWithHook.tsx
Normal file
189
src/pages/index/IndexWithHook.tsx
Normal 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;
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
// 搜索商品
|
||||
|
||||
@@ -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 '优惠券'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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([])
|
||||
|
||||
@@ -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
200
src/utils/couponUtils.ts
Normal 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))
|
||||
}
|
||||
@@ -166,7 +166,7 @@ export function buildSingleGoodsOrder(
|
||||
options?: {
|
||||
comments?: string;
|
||||
deliveryType?: number;
|
||||
couponId?: number;
|
||||
couponId?: any;
|
||||
selfTakeMerchantId?: number;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
|
||||
Reference in New Issue
Block a user