Browse Source

优化下单流程

demo
科技小王子 2 months ago
parent
commit
a8a84f8b39
  1. 8
      src/api/shop/shopGoodsSku/index.ts
  2. 2
      src/api/shop/shopOrder/model/index.ts
  3. 16
      src/hooks/useCart.ts
  4. 45
      src/pages/order/components/OrderList.tsx
  5. 2
      src/pages/order/order.scss
  6. 9
      src/pages/order/order.tsx
  7. 110
      src/shop/goodsDetail/index.tsx
  8. 6
      src/shop/orderConfirm/index.tsx
  9. 8
      src/user/address/add.tsx
  10. 6
      src/utils/payment.ts

8
src/api/shop/shopGoodsSku/index.ts

@ -20,9 +20,7 @@ export async function generateGoodsSku(data: ShopGoodsSpec) {
export async function pageShopGoodsSku(params: ShopGoodsSkuParam) { export async function pageShopGoodsSku(params: ShopGoodsSkuParam) {
const res = await request.get<ApiResult<PageResult<ShopGoodsSku>>>( const res = await request.get<ApiResult<PageResult<ShopGoodsSku>>>(
'/shop/shop-goods-sku/page', '/shop/shop-goods-sku/page',
{
params
}
params
); );
if (res.code === 0) { if (res.code === 0) {
return res.data; return res.data;
@ -36,9 +34,7 @@ export async function pageShopGoodsSku(params: ShopGoodsSkuParam) {
export async function listShopGoodsSku(params?: ShopGoodsSkuParam) { export async function listShopGoodsSku(params?: ShopGoodsSkuParam) {
const res = await request.get<ApiResult<ShopGoodsSku[]>>( const res = await request.get<ApiResult<ShopGoodsSku[]>>(
'/shop/shop-goods-sku', '/shop/shop-goods-sku',
{
params
}
params
); );
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
return res.data; return res.data;

2
src/api/shop/shopOrder/model/index.ts

@ -89,7 +89,7 @@ export interface ShopOrder {
// 代付支付方式,0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付 // 代付支付方式,0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付
friendPayType?: number; friendPayType?: number;
// 0未付款,1已付款 // 0未付款,1已付款
payStatus?: number;
payStatus?: boolean;
// 0未使用,1已完成,2已取消,3取消中,4退款申请中,5退款被拒绝,6退款成功,7客户端申请退款 // 0未使用,1已完成,2已取消,3取消中,4退款申请中,5退款被拒绝,6退款成功,7客户端申请退款
orderStatus?: number; orderStatus?: number;
// 发货状态(10未发货 20已发货 30部分发货) // 发货状态(10未发货 20已发货 30部分发货)

16
src/hooks/useCart.ts

@ -9,6 +9,8 @@ export interface CartItem {
image: string; image: string;
quantity: number; quantity: number;
addTime: number; addTime: number;
skuId?: number;
specInfo?: string;
} }
// 购物车Hook // 购物车Hook
@ -51,9 +53,15 @@ export const useCart = () => {
name: string; name: string;
price: string; price: string;
image: string; image: string;
skuId?: number;
specInfo?: string;
}, quantity: number = 1) => { }, quantity: number = 1) => {
const newItems = [...cartItems]; const newItems = [...cartItems];
const existingItemIndex = newItems.findIndex(item => item.goodsId === goods.goodsId);
// 如果有SKU,需要根据goodsId和skuId来判断是否为同一商品
const existingItemIndex = newItems.findIndex(item =>
item.goodsId === goods.goodsId &&
(goods.skuId ? item.skuId === goods.skuId : !item.skuId)
);
if (existingItemIndex >= 0) { if (existingItemIndex >= 0) {
// 如果商品已存在,增加数量 // 如果商品已存在,增加数量
@ -66,7 +74,9 @@ export const useCart = () => {
price: goods.price, price: goods.price,
image: goods.image, image: goods.image,
quantity, quantity,
addTime: Date.now()
addTime: Date.now(),
skuId: goods.skuId,
specInfo: goods.specInfo
}; };
newItems.push(newItem); newItems.push(newItem);
} }
@ -98,7 +108,7 @@ export const useCart = () => {
return; return;
} }
const newItems = cartItems.map(item =>
const newItems = cartItems.map(item =>
item.goodsId === goodsId ? { ...item, quantity } : item item.goodsId === goodsId ? { ...item, quantity } : item
); );
setCartItems(newItems); setCartItems(newItems);

45
src/pages/order/components/OrderList.tsx

@ -1,4 +1,4 @@
import {Avatar, Cell, Space, Tabs, Button, TabPane, Image, Toast} from '@nutui/nutui-react-taro'
import {Avatar, Cell, Space, Tabs, Button, TabPane, Image} from '@nutui/nutui-react-taro'
import {useEffect, useState, CSSProperties} from "react"; import {useEffect, useState, CSSProperties} from "react";
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro' import {InfiniteLoading} from '@nutui/nutui-react-taro'
@ -10,12 +10,13 @@ import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import {copyText} from "@/utils/common"; import {copyText} from "@/utils/common";
const InfiniteUlStyle: CSSProperties = { const InfiniteUlStyle: CSSProperties = {
marginTop: '84px',
marginTop: '44px',
height: '82vh', height: '82vh',
width: '100%', width: '100%',
padding: '0', padding: '0',
overflowY: 'auto', overflowY: 'auto',
overflowX: 'hidden', overflowX: 'hidden',
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
} }
const tabs = [ const tabs = [
{ {
@ -61,12 +62,11 @@ function OrderList(props: OrderListProps) {
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
const [tapIndex, setTapIndex] = useState<string | number>('0') const [tapIndex, setTapIndex] = useState<string | number>('0')
console.log(props.statusBarHeight, 'ppp')
// 获取订单状态文本 // 获取订单状态文本
const getOrderStatusText = (order: ShopOrder) => { const getOrderStatusText = (order: ShopOrder) => {
if (order.payStatus === 0) return '待付款';
if (order.payStatus === 1 && order.deliveryStatus === 10) return '待发货';
console.log(order,'order')
if (!order.payStatus) return '待付款';
if (order.payStatus && order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) return '待收货'; if (order.deliveryStatus === 20) return '待收货';
if (order.deliveryStatus === 30) return '已收货'; if (order.deliveryStatus === 30) return '已收货';
if (order.orderStatus === 1) return '已完成'; if (order.orderStatus === 1) return '已完成';
@ -151,12 +151,15 @@ function OrderList(props: OrderListProps) {
deliveryStatus: 30, // 已收货 deliveryStatus: 30, // 已收货
orderStatus: 1 // 已完成 orderStatus: 1 // 已完成
}); });
Toast.show('确认收货成功');
Taro.showToast({
title: '确认收货成功',
});
reload(true); // 重新加载列表 reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新 props.onReload?.(); // 通知父组件刷新
} catch (error) { } catch (error) {
console.error('确认收货失败:', error);
Toast.show('确认收货失败');
Taro.showToast({
title: '确认收货失败',
});
} }
}; };
@ -167,12 +170,16 @@ function OrderList(props: OrderListProps) {
...order, ...order,
orderStatus: 2 // 已取消 orderStatus: 2 // 已取消
}); });
Toast.show('订单已取消');
Taro.showToast({
title: '订单已取消',
});
reload(true); // 重新加载列表 reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新 props.onReload?.(); // 通知父组件刷新
} catch (error) { } catch (error) {
console.error('取消订单失败:', error); console.error('取消订单失败:', error);
Toast.show('取消订单失败');
Taro.showToast({
title: '取消订单失败',
});
} }
}; };
@ -185,8 +192,8 @@ function OrderList(props: OrderListProps) {
<Tabs <Tabs
align={'left'} align={'left'}
className={'fixed left-0'} className={'fixed left-0'}
style={{ top: '84px'}}
tabStyle={{ backgroundColor: 'transparent'}}
style={{ top: '44px'}}
tabStyle={{ backgroundColor: '#ffffff'}}
value={tapIndex} value={tapIndex}
onChange={(paneKey) => { onChange={(paneKey) => {
setTapIndex(paneKey) setTapIndex(paneKey)
@ -225,9 +232,9 @@ function OrderList(props: OrderListProps) {
<Cell key={index} style={{padding: '16px'}} onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}> <Cell key={index} style={{padding: '16px'}} onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
<Space direction={'vertical'} className={'w-full flex flex-col'}> <Space direction={'vertical'} className={'w-full flex flex-col'}>
<div className={'order-no flex justify-between'}> <div className={'order-no flex justify-between'}>
<span className={'text-gray-700 font-bold text-sm'}
<span className={'text-gray-600 font-bold text-sm'}
onClick={(e) => {e.stopPropagation(); copyText(`${item.orderNo}`)}}>{item.orderNo}</span> onClick={(e) => {e.stopPropagation(); copyText(`${item.orderNo}`)}}>{item.orderNo}</span>
<span className={'text-orange-500'}>{getOrderStatusText(item)}</span>
<span className={'text-gray-600 font-medium'}>{getOrderStatusText(item)}</span>
</div> </div>
<div <div
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div> className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
@ -249,7 +256,7 @@ function OrderList(props: OrderListProps) {
{goods.spec && <div className={'text-gray-500 text-xs'}>{goods.spec}</div>} {goods.spec && <div className={'text-gray-500 text-xs'}>{goods.spec}</div>}
<div className={'text-gray-500 text-xs'}>{goods.totalNum}</div> <div className={'text-gray-500 text-xs'}>{goods.totalNum}</div>
</div> </div>
<div className={'text-red-500 text-sm'}>{goods.price}</div>
<div className={'text-sm'}>{goods.price}</div>
</div> </div>
)) ))
) : ( ) : (
@ -271,11 +278,11 @@ function OrderList(props: OrderListProps) {
{/* 操作按钮 */} {/* 操作按钮 */}
<Space className={'btn flex justify-end'}> <Space className={'btn flex justify-end'}>
{item.payStatus === 0 && (
<>
{item.payStatus && (
<Space>
<Button size={'small'} onClick={(e) => {e.stopPropagation(); cancelOrder(item)}}></Button> <Button size={'small'} onClick={(e) => {e.stopPropagation(); cancelOrder(item)}}></Button>
<Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); console.log('立即支付')}}></Button> <Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); console.log('立即支付')}}></Button>
</>
</Space>
)} )}
{item.deliveryStatus === 20 && ( {item.deliveryStatus === 20 && (
<Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); confirmReceive(item)}}></Button> <Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); confirmReceive(item)}}></Button>

2
src/pages/order/order.scss

@ -1,4 +1,4 @@
page { page {
background: linear-gradient(to bottom, #e9fff2, #f9fafb);
background: linear-gradient(to bottom, #f3f3f3, #f9fafb);
background-size: 100%; background-size: 100%;
} }

9
src/pages/order/order.tsx

@ -2,6 +2,7 @@ import {useState} from "react"; // 添加 useCallback 引入
import Taro, {useDidShow} from '@tarojs/taro' import Taro, {useDidShow} from '@tarojs/taro'
import {NavBar, Space, Empty, Button, ConfigProvider} from '@nutui/nutui-react-taro' import {NavBar, Space, Empty, Button, ConfigProvider} from '@nutui/nutui-react-taro'
import {Search} from '@nutui/icons-react-taro' import {Search} from '@nutui/icons-react-taro'
import { View } from '@tarojs/components';
import OrderList from "./components/OrderList"; import OrderList from "./components/OrderList";
import {ShopOrder} from "@/api/shop/shopOrder/model"; import {ShopOrder} from "@/api/shop/shopOrder/model";
import {pageShopOrder} from "@/api/shop/shopOrder"; import {pageShopOrder} from "@/api/shop/shopOrder";
@ -25,19 +26,15 @@ function Order() {
setStatusBarHeight(res.statusBarHeight) setStatusBarHeight(res.statusBarHeight)
}, },
}) })
// 设置导航栏背景色(含状态栏)
Taro.setNavigationBarColor({
backgroundColor: '#ffffff', // 状态栏+导航栏背景色
frontColor: 'black', // 状态栏文字颜色(仅支持 black/white)
});
reload().then() reload().then()
}); // 新增: 添加滚动事件监听 }); // 新增: 添加滚动事件监听
return ( return (
<> <>
<View style={{ height: `${statusBarHeight}px`, backgroundColor: '#ffffff'}}></View>
<NavBar <NavBar
fixed={true} fixed={true}
style={{marginTop: `${statusBarHeight}px`, backgroundColor: 'transparent'}}
style={{marginTop: `${statusBarHeight}px`, backgroundColor: '#ffffff'}}
left={ left={
<> <>
<div className={'flex justify-between items-center w-full'}> <div className={'flex justify-between items-center w-full'}>

110
src/shop/goodsDetail/index.tsx

@ -5,14 +5,25 @@ import Taro, {useShareAppMessage, useShareTimeline} from "@tarojs/taro";
import {RichText, View} from '@tarojs/components' import {RichText, View} from '@tarojs/components'
import {ShopGoods} from "@/api/shop/shopGoods/model"; import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods"; import {getShopGoods} from "@/api/shop/shopGoods";
import {listShopGoodsSpec} from "@/api/shop/shopGoodsSpec";
import {ShopGoodsSpec} from "@/api/shop/shopGoodsSpec/model";
import {listShopGoodsSku} from "@/api/shop/shopGoodsSku";
import {ShopGoodsSku} from "@/api/shop/shopGoodsSku/model";
import {Swiper} from '@nutui/nutui-react-taro' import {Swiper} from '@nutui/nutui-react-taro'
import navTo, {wxParse} from "@/utils/common"; import navTo, {wxParse} from "@/utils/common";
import SpecSelector from "@/components/SpecSelector";
import "./index.scss"; import "./index.scss";
import {useCart} from "@/hooks/useCart"; import {useCart} from "@/hooks/useCart";
const GoodsDetail = () => { const GoodsDetail = () => {
const [goods, setGoods] = useState<ShopGoods | null>(null); const [goods, setGoods] = useState<ShopGoods | null>(null);
const [files, setFiles] = useState<any[]>([]); const [files, setFiles] = useState<any[]>([]);
const [specs, setSpecs] = useState<ShopGoodsSpec[]>([]);
const [skus, setSkus] = useState<ShopGoodsSku[]>([]);
const [showSpecSelector, setShowSpecSelector] = useState(false);
const [specAction, setSpecAction] = useState<'cart' | 'buy'>('cart');
const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
const [loading, setLoading] = useState(false);
const router = Taro.getCurrentInstance().router; const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.id; const goodsId = router?.params?.id;
@ -31,6 +42,14 @@ const GoodsDetail = () => {
}); });
} }
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('cart');
setShowSpecSelector(true);
return;
}
// 没有规格,直接加入购物车
addToCart({ addToCart({
goodsId: goods.goodsId!, goodsId: goods.goodsId!,
name: goods.name || '', name: goods.name || '',
@ -39,8 +58,61 @@ const GoodsDetail = () => {
}); });
}; };
// 处理立即购买
const handleBuyNow = () => {
if (!goods) return;
if (!Taro.getStorageSync('UserId')) {
return Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
}
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('buy');
setShowSpecSelector(true);
return;
}
// 没有规格,直接购买
navTo(`/shop/orderConfirm/index?goodsId=${goods?.goodsId}`, true);
};
// 规格选择确认回调
const handleSpecConfirm = (sku: ShopGoodsSku, quantity: number, action: 'cart' | 'buy') => {
setSelectedSku(sku);
setShowSpecSelector(false);
if (action === 'cart') {
// 加入购物车
addToCart({
goodsId: goods!.goodsId!,
skuId: sku.id,
name: goods!.name || '',
price: sku.price || goods!.price || '0',
image: goods!.image || '',
specInfo: sku.sku, // sku字段包含规格信息
}, quantity);
} else {
// 立即购买
const orderData = {
goodsId: goods!.goodsId!,
skuId: sku.id,
quantity,
price: sku.price || goods!.price || '0'
};
navTo(`/shop/orderConfirm/index?orderData=${encodeURIComponent(JSON.stringify(orderData))}`, true);
}
};
useEffect(() => { useEffect(() => {
if (goodsId) { if (goodsId) {
setLoading(true);
// 加载商品详情
getShopGoods(Number(goodsId)) getShopGoods(Number(goodsId))
.then((res) => { .then((res) => {
// 处理富文本内容,去掉图片间距 // 处理富文本内容,去掉图片间距
@ -52,10 +124,30 @@ const GoodsDetail = () => {
const arr = JSON.parse(res.files); const arr = JSON.parse(res.files);
arr.length > 0 && setFiles(arr); arr.length > 0 && setFiles(arr);
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Failed to fetch goods detail:", error); console.error("Failed to fetch goods detail:", error);
})
.finally(() => {
setLoading(false);
});
// 加载商品规格
listShopGoodsSpec({ goodsId: Number(goodsId) } as any)
.then((data) => {
setSpecs(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods specs:", error);
});
// 加载商品SKU
listShopGoodsSku({ goodsId: Number(goodsId) } as any)
.then((data) => {
setSkus(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods skus:", error);
}); });
} }
}, [goodsId]); }, [goodsId]);
@ -94,7 +186,7 @@ const GoodsDetail = () => {
}; };
}); });
if (!goods) {
if (!goods || loading) {
return <div>...</div>; return <div>...</div>;
} }
@ -209,11 +301,23 @@ const GoodsDetail = () => {
onClick={() => handleAddToCart()}> onClick={() => handleAddToCart()}>
</div> </div>
<div className={'cart-buy pl-4 pr-5 text-sm'} <div className={'cart-buy pl-4 pr-5 text-sm'}
onClick={() => navTo(`/shop/orderConfirm/index?goodsId=${goods?.goodsId}`, true)}>
onClick={() => handleBuyNow()}>
</div> </div>
</div> </div>
</View> </View>
</div> </div>
{/* 规格选择器 */}
{showSpecSelector && (
<SpecSelector
goods={goods!}
specs={specs}
skus={skus}
action={specAction}
onConfirm={handleSpecConfirm}
onClose={() => setShowSpecSelector(false)}
/>
)}
</div> </div>
); );
}; };

6
src/shop/orderConfirm/index.tsx

@ -1,7 +1,7 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Image, Button, Cell, CellGroup, Input, Space, ActionSheet} from '@nutui/nutui-react-taro' import {Image, Button, Cell, CellGroup, Input, Space, ActionSheet} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro' import {Location, ArrowRight} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import Taro, {useDidShow} from '@tarojs/taro'
import {ShopGoods} from "@/api/shop/shopGoods/model"; import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods"; import {getShopGoods} from "@/api/shop/shopGoods";
import {View} from '@tarojs/components'; import {View} from '@tarojs/components';
@ -87,6 +87,10 @@ const OrderConfirm = () => {
await PaymentHandler.pay(orderData, paymentType); await PaymentHandler.pay(orderData, paymentType);
}; };
useDidShow(() => {
reload().then()
})
useEffect(() => { useEffect(() => {
if (goodsId) { if (goodsId) {
getShopGoods(Number(goodsId)).then(res => { getShopGoods(Number(goodsId)).then(res => {

8
src/user/address/add.tsx

@ -254,7 +254,13 @@ const AddUserAddress = () => {
width: '100%' width: '100%'
}} }}
> >
<Button nativeType="submit" block type="info">
<Button
nativeType="submit"
type="success"
size="large"
className={'w-full'}
block
>
使 使
</Button> </Button>
</div> </div>

6
src/utils/payment.ts

@ -168,13 +168,17 @@ export function buildSingleGoodsOrder(
deliveryType?: number; deliveryType?: number;
couponId?: number; couponId?: number;
selfTakeMerchantId?: number; selfTakeMerchantId?: number;
skuId?: number;
specInfo?: string;
} }
): OrderCreateRequest { ): OrderCreateRequest {
return { return {
goodsItems: [ goodsItems: [
{ {
goodsId, goodsId,
quantity
quantity,
skuId: options?.skuId,
specInfo: options?.specInfo
} }
], ],
addressId, addressId,

Loading…
Cancel
Save