feat(doctor): 新增医生端功能模块
- 添加医生注册申请页面及表单逻辑- 实现医生银行卡信息管理功能 - 开发患者报备与签约管理界面 - 集成微信手机号获取与头像上传功能 - 添加表单验证与数据提交逻辑 - 实现页面配置文件与路由集成- 添加日期选择器与数据格式化工具 - 集成API接口调用与错误处理机制
This commit is contained in:
31
src/shop/category/components/Banner.tsx
Normal file
31
src/shop/category/components/Banner.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Swiper } from '@nutui/nutui-react-taro'
|
||||
import {CmsAd} from "@/api/cms/cmsAd/model";
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAd} from "@/api/cms/cmsAd";
|
||||
|
||||
const MyPage = () => {
|
||||
const [item, setItem] = useState<CmsAd>()
|
||||
const reload = () => {
|
||||
getCmsAd(439).then(data => {
|
||||
setItem(data)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
|
||||
{item?.imageList?.map((item) => (
|
||||
<Swiper.Item key={item}>
|
||||
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
|
||||
</Swiper.Item>
|
||||
))}
|
||||
</Swiper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MyPage
|
||||
0
src/shop/category/components/GoodsList.scss
Normal file
0
src/shop/category/components/GoodsList.scss
Normal file
60
src/shop/category/components/GoodsList.tsx
Normal file
60
src/shop/category/components/GoodsList.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model"
|
||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model"
|
||||
import './GoodsList.scss'
|
||||
|
||||
interface GoodsListProps {
|
||||
data: ShopGoods[]
|
||||
nav?: CmsNavigation
|
||||
}
|
||||
|
||||
const GoodsList = (props: GoodsListProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'py-3'} style={{paddingTop: '0'}}>
|
||||
<View className={'bg-gray-50'}>
|
||||
<View className={'grid grid-cols-2 gap-2 pb-2 p-2'}>
|
||||
{props.data?.map((item, index) => {
|
||||
return (
|
||||
<View key={index} className={'goods-waterfall-item bg-white'} style={{
|
||||
borderRadius: '0 0 6px 6px'
|
||||
}}>
|
||||
<View className={'goods-card'}>
|
||||
<Image
|
||||
src={item.image}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '6px 6px 0 0',
|
||||
height: '180px'
|
||||
}}
|
||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}
|
||||
/>
|
||||
<View className={'goods-info p-2 flex flex-col'}>
|
||||
<View className={'goods-title text-sm font-bold'}>{item.name}</View>
|
||||
<View className={'goods-meta'}>
|
||||
<Text className={'goods-comments text-gray-400 text-xs'}>{item.comments}</Text>
|
||||
</View>
|
||||
<View className={'goods-price-section flex justify-between'}>
|
||||
<View className={'goods-price'}>
|
||||
<Text className={'price-unit text-orange-600 font-bold text-lg'}>¥</Text>
|
||||
<Text className={'price-number text-orange-600 font-bold text-lg'}>{item.price}</Text>
|
||||
</View>
|
||||
<View className={'goods-actions'}>
|
||||
<Text className={'goods-sales text-gray-400 text-xs'}>已售 {item.sales}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default GoodsList
|
||||
4
src/shop/category/index.config.ts
Normal file
4
src/shop/category/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品分类',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
0
src/shop/category/index.scss
Normal file
0
src/shop/category/index.scss
Normal file
71
src/shop/category/index.tsx
Normal file
71
src/shop/category/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import GoodsList from './components/GoodsList'
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState} from "react"
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
import {pageShopGoods} from "@/api/shop/shopGoods"
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model"
|
||||
import {getCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
||||
|
||||
function Category() {
|
||||
const {params} = useRouter();
|
||||
const [categoryId, setCategoryId] = useState<number>(0)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [nav, setNav] = useState<CmsNavigation>()
|
||||
const [list, setList] = useState<ShopGoods[]>([])
|
||||
|
||||
const reload = async () => {
|
||||
// 1.加载远程数据
|
||||
const id = Number(params.id)
|
||||
const nav = await getCmsNavigation(id)
|
||||
const shopGoods = await pageShopGoods({categoryId: id})
|
||||
|
||||
// 2.处理业务逻辑
|
||||
setCategoryId(id)
|
||||
setNav(nav)
|
||||
setList(shopGoods?.list || [])
|
||||
|
||||
// 3.设置标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: `${nav?.categoryName}`
|
||||
})
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, []);
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_WebSoft Inc.`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
fail: function () {
|
||||
console.log('分享失败');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading className={'px-2 text-center'}>加载中</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col'}>
|
||||
<GoodsList data={list} nav={nav}/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Category
|
||||
5
src/shop/goodsDetail/index.config.ts
Normal file
5
src/shop/goodsDetail/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品详情',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
25
src/shop/goodsDetail/index.scss
Normal file
25
src/shop/goodsDetail/index.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
.cart-icon{
|
||||
background: linear-gradient(to bottom, #bbe094, #4ee265);
|
||||
border-radius: 100px 0 0 100px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
/* 去掉 RichText 中图片的间距 */
|
||||
rich-text img {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 在全局样式或组件样式文件中 */
|
||||
.no-margin {
|
||||
margin: 0 !important; /* 使用 !important 来确保覆盖默认样式 */
|
||||
}
|
||||
|
||||
/* 文本截断样式 */
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
399
src/shop/goodsDetail/index.tsx
Normal file
399
src/shop/goodsDetail/index.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Image, Badge, Popup, CellGroup, Cell} from "@nutui/nutui-react-taro";
|
||||
import {ArrowLeft, Headphones, Share, Cart, ArrowRight} from "@nutui/icons-react-taro";
|
||||
import Taro, {useShareAppMessage} from "@tarojs/taro";
|
||||
import {RichText, View, Text} from '@tarojs/components'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
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 navTo, {wxParse} from "@/utils/common";
|
||||
import SpecSelector from "@/components/SpecSelector";
|
||||
import "./index.scss";
|
||||
import {useCart} from "@/hooks/useCart";
|
||||
import {useConfig} from "@/hooks/useConfig";
|
||||
|
||||
const GoodsDetail = () => {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
|
||||
const [windowWidth, setWindowWidth] = useState<number>(390)
|
||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||
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 [showBottom, setShowBottom] = useState(false)
|
||||
const [bottomItem, setBottomItem] = useState<any>({
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
// const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const goodsId = router?.params?.id;
|
||||
|
||||
// 使用购物车Hook
|
||||
const {cartCount, addToCart} = useCart()
|
||||
const {config} = useConfig()
|
||||
|
||||
// 处理加入购物车
|
||||
const handleAddToCart = () => {
|
||||
if (!goods) return;
|
||||
|
||||
if (!Taro.getStorageSync('UserId')) {
|
||||
return Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有规格,显示规格选择器
|
||||
if (specs.length > 0) {
|
||||
setSpecAction('cart');
|
||||
setShowSpecSelector(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有规格,直接加入购物车
|
||||
addToCart({
|
||||
goodsId: goods.goodsId!,
|
||||
name: goods.name || '',
|
||||
price: goods.price || '0',
|
||||
image: goods.image || ''
|
||||
});
|
||||
};
|
||||
|
||||
// 处理立即购买
|
||||
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 if (action === 'buy') {
|
||||
// 立即购买
|
||||
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);
|
||||
} else {
|
||||
// 默认情况:如果action未定义,默认为立即购买
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const openBottom = (title: string, content: string) => {
|
||||
setBottomItem({
|
||||
title,
|
||||
content
|
||||
})
|
||||
setShowBottom(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setWindowWidth(res.windowWidth)
|
||||
setStatusBarHeight(Number(res.statusBarHeight) + 5)
|
||||
},
|
||||
});
|
||||
if (goodsId) {
|
||||
setLoading(true);
|
||||
|
||||
// 加载商品详情
|
||||
getShopGoods(Number(goodsId))
|
||||
.then((res) => {
|
||||
// 处理富文本内容,去掉图片间距
|
||||
if (res.content) {
|
||||
res.content = wxParse(res.content);
|
||||
}
|
||||
setGoods(res);
|
||||
if (res.files) {
|
||||
const arr = JSON.parse(res.files);
|
||||
arr.length > 0 && setFiles(arr);
|
||||
}
|
||||
})
|
||||
.catch((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]);
|
||||
|
||||
// 分享给好友
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: goods?.name || '精选商品',
|
||||
path: `/shop/goodsDetail/index?id=${goodsId}`,
|
||||
imageUrl: goods?.image, // 分享图片
|
||||
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
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (!goods || loading) {
|
||||
return <View>加载中...</View>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={"py-0"}>
|
||||
<View
|
||||
className={
|
||||
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-70"
|
||||
}
|
||||
style={{
|
||||
borderRadius: "100%",
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
top: statusBarHeight + 'px',
|
||||
left: "10px",
|
||||
}}
|
||||
onClick={() => Taro.navigateBack()}
|
||||
>
|
||||
<ArrowLeft size={14}/>
|
||||
</View>
|
||||
<View className={
|
||||
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-90"
|
||||
}
|
||||
style={{
|
||||
borderRadius: "100%",
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
top: statusBarHeight + 'px',
|
||||
right: "110px",
|
||||
}}
|
||||
onClick={() => Taro.switchTab({url: `/pages/cart/cart`})}>
|
||||
<Badge value={cartCount} top="-2" right="2">
|
||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
||||
<Cart size={16}/>
|
||||
</View>
|
||||
</Badge>
|
||||
</View>
|
||||
{
|
||||
files.length > 0 && (
|
||||
<Swiper defaultValue={0} indicator height={windowWidth}>
|
||||
{files.map((item) => (
|
||||
<Swiper.Item key={item}>
|
||||
<Image width={windowWidth} height={windowWidth} src={item.url} mode={'scaleToFill'} lazyLoad={false}/>
|
||||
</Swiper.Item>
|
||||
))}
|
||||
</Swiper>
|
||||
)
|
||||
}
|
||||
{
|
||||
files.length == 0 && (
|
||||
<Image
|
||||
src={goods.image}
|
||||
mode={"scaleToFill"}
|
||||
radius="10px 10px 0 0"
|
||||
height="300"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<View
|
||||
className={"flex flex-col justify-between items-center"}
|
||||
>
|
||||
<View
|
||||
className={
|
||||
"flex flex-col bg-white w-full"
|
||||
}
|
||||
>
|
||||
<View className={"flex flex-col p-3 rounded-lg"}>
|
||||
<>
|
||||
<View className={'flex justify-between'}>
|
||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
||||
<span className={'text-xs'}>¥</span>
|
||||
<span className={'font-bold text-2xl'}>{goods.price}</span>
|
||||
</View>
|
||||
<span className={"text-gray-400 text-xs"}>已售 {goods.sales}</span>
|
||||
</View>
|
||||
<View className={'flex justify-between items-center'}>
|
||||
<View className={'goods-info'}>
|
||||
<View className={"car-no text-lg"}>
|
||||
{goods.name}
|
||||
</View>
|
||||
<View className={"flex justify-between text-xs py-1"}>
|
||||
<span className={"text-orange-500"}>
|
||||
{goods.comments}
|
||||
</span>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<button
|
||||
className={'flex flex-col justify-center items-center bg-white text-gray-500 px-1 gap-1 text-nowrap whitespace-nowrap'}
|
||||
open-type="share">
|
||||
<Share size={20}/>
|
||||
<span className={'text-xs'}>分享</span>
|
||||
</button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
</View>
|
||||
<View className={'w-full'}>
|
||||
{
|
||||
config?.deliveryText && (
|
||||
<CellGroup>
|
||||
<Cell title={'配送'} extra={
|
||||
<View className="flex items-center">
|
||||
<Text className={'truncate max-w-56 inline-block'}>{config?.deliveryText || '14:30下单,明天配送'}</Text>
|
||||
<ArrowRight color="#cccccc" size={15}/>
|
||||
</View>
|
||||
} onClick={() => openBottom('配送', `${config?.deliveryText}`)}/>
|
||||
<Cell title={'保障'} extra={
|
||||
<View className="flex items-center">
|
||||
<Text className={'truncate max-w-56 inline-block'}>{config?.guaranteeText || '支持7天无理由退货'}</Text>
|
||||
<ArrowRight color="#cccccc" size={15}/>
|
||||
</View>
|
||||
} onClick={() => openBottom('保障', `${config?.guaranteeText}`)}/>
|
||||
</CellGroup>
|
||||
)
|
||||
}
|
||||
{config?.openComments == '1' && (
|
||||
<CellGroup>
|
||||
<Cell title={'用户评价 (0)'} extra={
|
||||
<>
|
||||
<Text>查看全部</Text>
|
||||
<ArrowRight color="#cccccc" size={15}/>
|
||||
</>
|
||||
} onClick={() => navTo(`/shop/comments/index`)}/>
|
||||
<Cell className={'flex h-32 bg-white p-4'}>
|
||||
暂无评价
|
||||
</Cell>
|
||||
</CellGroup>
|
||||
)}
|
||||
</View>
|
||||
<View className={'w-full'}>
|
||||
<View className={'p-4 bg-white leading-7 '}>
|
||||
<RichText nodes={goods.content || '内容详情'}/>
|
||||
</View>
|
||||
<View className={'h-24'}></View>
|
||||
</View>
|
||||
</View>
|
||||
{/*底部弹窗*/}
|
||||
<Popup
|
||||
visible={showBottom}
|
||||
position="bottom"
|
||||
onClose={() => {
|
||||
setShowBottom(false)
|
||||
}}
|
||||
lockScroll
|
||||
>
|
||||
<View className={'flex flex-col p-4'}>
|
||||
<Text className={'font-bold text-sm'}>{bottomItem.title}</Text>
|
||||
<Text className={'text-gray-500 my-2'}>{bottomItem.content}</Text>
|
||||
</View>
|
||||
</Popup>
|
||||
{/*底部购买按钮*/}
|
||||
<View className={'fixed bg-white w-full bottom-0 left-0 pt-4 pb-6'}>
|
||||
<View className={'btn-bar flex justify-between items-center'}>
|
||||
<View className={'flex justify-center items-center mx-4'}>
|
||||
<button open-type="contact" className={'flex items-center text-sm py-2'}>
|
||||
<Headphones size={16} style={{marginRight: '4px'}}/>咨询
|
||||
</button>
|
||||
</View>
|
||||
<View className={'buy-btn mx-4'}>
|
||||
<View className={'cart-add px-4 text-sm'}
|
||||
onClick={() => handleAddToCart()}>加入购物车
|
||||
</View>
|
||||
<View className={'cart-buy pl-4 pr-5 text-sm'}
|
||||
onClick={() => handleBuyNow()}>立即购买
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 规格选择器 */}
|
||||
{showSpecSelector && (
|
||||
<SpecSelector
|
||||
goods={goods!}
|
||||
specs={specs}
|
||||
skus={skus}
|
||||
action={specAction}
|
||||
onConfirm={handleSpecConfirm}
|
||||
onClose={() => setShowSpecSelector(false)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoodsDetail;
|
||||
4
src/shop/orderConfirm/index.config.ts
Normal file
4
src/shop/orderConfirm/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单确认',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
116
src/shop/orderConfirm/index.scss
Normal file
116
src/shop/orderConfirm/index.scss
Normal file
@@ -0,0 +1,116 @@
|
||||
.order-confirm-page {
|
||||
padding-bottom: 100px; // 留出底部固定按钮的空间
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
|
||||
.error-text {
|
||||
color: #999;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.total-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.address-bottom-line{
|
||||
width: 100%;
|
||||
border-radius: 12rpx 12rpx 0 0;
|
||||
background: #fff;
|
||||
padding: 26rpx 49rpx 0 34rpx;
|
||||
position: relative;
|
||||
&:before {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
background: repeating-linear-gradient(-45deg, #ff6c6c, #ff6c6c 20%, transparent 0, transparent 25%, #1989fa 0,
|
||||
#1989fa 45%, transparent 0, transparent 50%);
|
||||
background-size: 120px;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// 优惠券弹窗样式
|
||||
.coupon-popup {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__current {
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&-title {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8rpx 12rpx;
|
||||
background: #fff;
|
||||
border-radius: 6rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
850
src/shop/orderConfirm/index.tsx
Normal file
850
src/shop/orderConfirm/index.tsx
Normal file
@@ -0,0 +1,850 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {
|
||||
Image,
|
||||
Button,
|
||||
Cell,
|
||||
CellGroup,
|
||||
Input,
|
||||
Space,
|
||||
ActionSheet,
|
||||
Popup,
|
||||
InputNumber,
|
||||
ConfigProvider
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {Location, ArrowRight} from '@nutui/icons-react-taro'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {getShopGoods} from "@/api/shop/shopGoods";
|
||||
import {View, Text} from '@tarojs/components';
|
||||
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
|
||||
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
|
||||
import './index.scss'
|
||||
import Gap from "@/components/Gap";
|
||||
import {selectPayment} from "@/api/system/payment";
|
||||
import {Payment} from "@/api/system/payment/model";
|
||||
import {PaymentHandler, PaymentType, buildSingleGoodsOrder} from "@/utils/payment";
|
||||
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";
|
||||
import navTo from "@/utils/common";
|
||||
|
||||
|
||||
const OrderConfirm = () => {
|
||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||
const [address, setAddress] = useState<ShopUserAddress>()
|
||||
const [payments, setPayments] = useState<any[]>([])
|
||||
const [payment, setPayment] = useState<Payment>()
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false)
|
||||
const [quantity, setQuantity] = useState<number>(1)
|
||||
const [orderRemark, setOrderRemark] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string>('')
|
||||
const [payLoading, setPayLoading] = useState<boolean>(false)
|
||||
|
||||
// InputNumber 主题配置
|
||||
const customTheme = {
|
||||
nutuiInputnumberButtonWidth: '28px',
|
||||
nutuiInputnumberButtonHeight: '28px',
|
||||
nutuiInputnumberInputWidth: '40px',
|
||||
nutuiInputnumberInputHeight: '28px',
|
||||
nutuiInputnumberInputBorderRadius: '4px',
|
||||
nutuiInputnumberButtonBorderRadius: '4px',
|
||||
}
|
||||
|
||||
// 优惠券相关状态
|
||||
const [selectedCoupon, setSelectedCoupon] = useState<CouponCardProps | null>(null)
|
||||
const [couponVisible, setCouponVisible] = useState<boolean>(false)
|
||||
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
|
||||
const [couponLoading, setCouponLoading] = useState<boolean>(false)
|
||||
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const goodsId = router?.params?.goodsId;
|
||||
|
||||
// 计算商品总价
|
||||
const getGoodsTotal = () => {
|
||||
if (!goods) return 0
|
||||
const price = parseFloat(goods.price || '0')
|
||||
const total = price * quantity
|
||||
|
||||
// 🔍 详细日志,用于排查数值精度问题
|
||||
console.log('💵 商品总价计算:', {
|
||||
goodsPrice: goods.price,
|
||||
goodsPriceType: typeof goods.price,
|
||||
parsedPrice: price,
|
||||
quantity: quantity,
|
||||
total: total,
|
||||
totalFixed2: total.toFixed(2),
|
||||
totalString: total.toString()
|
||||
})
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// 计算优惠券折扣
|
||||
const getCouponDiscount = () => {
|
||||
if (!selectedCoupon || !goods) return 0
|
||||
const total = getGoodsTotal()
|
||||
return calculateCouponDiscount(selectedCoupon, total)
|
||||
}
|
||||
|
||||
// 计算实付金额
|
||||
const getFinalPrice = () => {
|
||||
const total = getGoodsTotal()
|
||||
const discount = getCouponDiscount()
|
||||
return Math.max(0, total - discount)
|
||||
}
|
||||
|
||||
|
||||
const handleSelect = (item: any) => {
|
||||
setPayment(payments.find(payment => payment.name === item.name))
|
||||
setIsVisible(false)
|
||||
}
|
||||
|
||||
// 处理数量变化
|
||||
const handleQuantityChange = (value: string | number) => {
|
||||
const newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value
|
||||
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)
|
||||
const usableCoupons = filterUsableCoupons(sortedCoupons, newTotal)
|
||||
setAvailableCoupons(sortedCoupons)
|
||||
|
||||
// 检查当前选中的优惠券是否还可用
|
||||
if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) {
|
||||
setSelectedCoupon(null)
|
||||
Taro.showToast({
|
||||
title: '当前优惠券不满足使用条件,已自动取消',
|
||||
icon: 'none'
|
||||
})
|
||||
|
||||
// 🎯 自动推荐新的最优优惠券
|
||||
if (usableCoupons.length > 0) {
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const discount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||
|
||||
if (discount > 0) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
Taro.showToast({
|
||||
title: `已为您重新推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||
icon: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (!selectedCoupon && usableCoupons.length > 0) {
|
||||
// 🔔 如果没有选中优惠券但有可用的,推荐最优的
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const discount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||
|
||||
if (discount > 0) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
Taro.showToast({
|
||||
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||
icon: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
} else if (selectedCoupon && usableCoupons.length > 0) {
|
||||
// 🔍 检查是否有更好的优惠券
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const currentDiscount = calculateCouponDiscount(selectedCoupon, newTotal)
|
||||
const bestDiscount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||
|
||||
// 如果有更好的优惠券(优惠超过0.01元)
|
||||
if (bestDiscount > currentDiscount + 0.01 && bestCoupon.id !== selectedCoupon.id) {
|
||||
Taro.showModal({
|
||||
title: '发现更优惠的优惠券',
|
||||
content: `有更好的优惠券可用,额外节省¥${(bestDiscount - currentDiscount).toFixed(2)},是否更换?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
Taro.showToast({
|
||||
title: '优惠券已更换',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理优惠券选择
|
||||
const handleCouponSelect = (coupon: CouponCardProps) => {
|
||||
const total = getGoodsTotal()
|
||||
|
||||
// 🔍 详细日志记录,用于排查问题
|
||||
console.log('🎫 手动选择优惠券详细信息:', {
|
||||
coupon: {
|
||||
id: coupon.id,
|
||||
title: coupon.title,
|
||||
type: coupon.type,
|
||||
amount: coupon.amount,
|
||||
minAmount: coupon.minAmount,
|
||||
status: coupon.status
|
||||
},
|
||||
orderInfo: {
|
||||
goodsPrice: goods?.price,
|
||||
quantity: quantity,
|
||||
total: total,
|
||||
totalFixed: total.toFixed(2)
|
||||
},
|
||||
validation: {
|
||||
isUsable: isCouponUsable(coupon, total),
|
||||
discount: calculateCouponDiscount(coupon, total),
|
||||
reason: getCouponUnusableReason(coupon, total)
|
||||
}
|
||||
})
|
||||
|
||||
// 检查是否可用
|
||||
if (!isCouponUsable(coupon, total)) {
|
||||
const reason = getCouponUnusableReason(coupon, total)
|
||||
|
||||
// 🚨 记录手动选择失败的详细信息
|
||||
console.error('🚨 手动选择优惠券失败:', {
|
||||
reason,
|
||||
coupon,
|
||||
total,
|
||||
minAmount: coupon.minAmount,
|
||||
comparison: {
|
||||
totalVsMinAmount: `${total} < ${coupon.minAmount}`,
|
||||
result: total < (coupon.minAmount || 0)
|
||||
}
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: reason || '优惠券不可用',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedCoupon(coupon)
|
||||
setCouponVisible(false)
|
||||
Taro.showToast({
|
||||
title: '优惠券选择成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 取消选择优惠券
|
||||
const handleCouponCancel = () => {
|
||||
setSelectedCoupon(null)
|
||||
Taro.showToast({
|
||||
title: '已取消使用优惠券',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 加载用户优惠券
|
||||
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)
|
||||
const usableCoupons = filterUsableCoupons(sortedCoupons, total)
|
||||
|
||||
setAvailableCoupons(sortedCoupons)
|
||||
|
||||
// 🎯 智能推荐:自动应用最优惠的可用优惠券
|
||||
if (usableCoupons.length > 0 && !selectedCoupon) {
|
||||
const bestCoupon = usableCoupons[0] // 已经按优惠金额排序,第一个就是最优的
|
||||
const discount = calculateCouponDiscount(bestCoupon, total)
|
||||
|
||||
// 🔍 详细日志记录自动推荐的信息
|
||||
console.log('🤖 自动推荐优惠券详细信息:', {
|
||||
coupon: {
|
||||
id: bestCoupon.id,
|
||||
title: bestCoupon.title,
|
||||
type: bestCoupon.type,
|
||||
amount: bestCoupon.amount,
|
||||
minAmount: bestCoupon.minAmount,
|
||||
status: bestCoupon.status
|
||||
},
|
||||
orderInfo: {
|
||||
goodsPrice: goods?.price,
|
||||
quantity: quantity,
|
||||
total: total,
|
||||
totalFixed: total.toFixed(2)
|
||||
},
|
||||
validation: {
|
||||
isUsable: isCouponUsable(bestCoupon, total),
|
||||
discount: discount,
|
||||
reason: getCouponUnusableReason(bestCoupon, total)
|
||||
}
|
||||
})
|
||||
|
||||
if (discount > 0) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
|
||||
// 显示智能推荐提示
|
||||
Taro.showToast({
|
||||
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||
icon: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 🔔 优惠券提示:如果有可用优惠券,显示提示
|
||||
if (usableCoupons.length > 0) {
|
||||
console.log(`发现${usableCoupons.length}张可用优惠券,已为您推荐最优惠券`)
|
||||
}
|
||||
|
||||
console.log('加载优惠券成功:', {
|
||||
originalData: res,
|
||||
transformedData: transformedCoupons,
|
||||
sortedData: sortedCoupons,
|
||||
usableCoupons: usableCoupons,
|
||||
recommendedCoupon: usableCoupons[0] || null
|
||||
})
|
||||
} else {
|
||||
setAvailableCoupons([])
|
||||
console.log('暂无可用优惠券')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载优惠券失败:', error)
|
||||
setAvailableCoupons([])
|
||||
Taro.showToast({
|
||||
title: '加载优惠券失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
setCouponLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一支付入口
|
||||
*/
|
||||
const onPay = async (goods: ShopGoods) => {
|
||||
try {
|
||||
setPayLoading(true)
|
||||
|
||||
// 基础校验
|
||||
if (!address) {
|
||||
Taro.showToast({
|
||||
title: '请选择收货地址',
|
||||
icon: 'error'
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payment) {
|
||||
Taro.showToast({
|
||||
title: '请选择支付方式',
|
||||
icon: 'error'
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
// 库存校验
|
||||
if (goods.stock !== undefined && quantity > goods.stock) {
|
||||
Taro.showToast({
|
||||
title: '商品库存不足',
|
||||
icon: 'error'
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
// 优惠券校验
|
||||
if (selectedCoupon) {
|
||||
const total = getGoodsTotal()
|
||||
if (!isCouponUsable(selectedCoupon, total)) {
|
||||
const reason = getCouponUnusableReason(selectedCoupon, total)
|
||||
Taro.showToast({
|
||||
title: reason || '优惠券不可用',
|
||||
icon: 'error'
|
||||
})
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 🔔 支付前最后一次检查:提醒用户是否有可用优惠券
|
||||
const total = getGoodsTotal()
|
||||
const usableCoupons = filterUsableCoupons(availableCoupons, total)
|
||||
|
||||
if (usableCoupons.length > 0) {
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const discount = calculateCouponDiscount(bestCoupon, total)
|
||||
|
||||
if (discount > 0) {
|
||||
// 用模态框提醒用户
|
||||
const confirmResult = await new Promise<boolean>((resolve) => {
|
||||
Taro.showModal({
|
||||
title: '发现可用优惠券',
|
||||
content: `您有优惠券可使用,可省¥${discount.toFixed(2)},是否使用?`,
|
||||
success: (res) => resolve(res.confirm),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
|
||||
if (confirmResult) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
// 🔄 使用优惠券后需要重新构建订单数据,这里直接递归调用支付函数
|
||||
// 但要确保传递最新的优惠券信息
|
||||
const currentPaymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
|
||||
const updatedOrderData = buildSingleGoodsOrder(
|
||||
goods.goodsId!,
|
||||
quantity,
|
||||
address.id,
|
||||
{
|
||||
comments: goods.name,
|
||||
deliveryType: 0,
|
||||
buyerRemarks: orderRemark,
|
||||
couponId: parseInt(String(bestCoupon.id), 10)
|
||||
}
|
||||
);
|
||||
|
||||
console.log('🎯 使用推荐优惠券的订单数据:', updatedOrderData);
|
||||
|
||||
// 执行支付
|
||||
await PaymentHandler.pay(updatedOrderData, currentPaymentType);
|
||||
return; // 提前返回,避免重复执行支付
|
||||
} else {
|
||||
// 用户选择不使用优惠券,继续支付
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建订单数据
|
||||
const orderData = buildSingleGoodsOrder(
|
||||
goods.goodsId!,
|
||||
quantity,
|
||||
address.id,
|
||||
{
|
||||
comments: goods.name,
|
||||
deliveryType: 0,
|
||||
buyerRemarks: orderRemark,
|
||||
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
|
||||
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined
|
||||
}
|
||||
);
|
||||
|
||||
// 根据支付方式选择支付类型
|
||||
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
|
||||
|
||||
// 🔍 支付前的详细信息记录
|
||||
console.log('💰 开始支付 - 详细信息:', {
|
||||
orderData,
|
||||
paymentType,
|
||||
selectedCoupon: selectedCoupon ? {
|
||||
id: selectedCoupon.id,
|
||||
title: selectedCoupon.title,
|
||||
type: selectedCoupon.type,
|
||||
amount: selectedCoupon.amount,
|
||||
minAmount: selectedCoupon.minAmount,
|
||||
discount: getCouponDiscount()
|
||||
} : null,
|
||||
priceCalculation: {
|
||||
goodsPrice: goods?.price,
|
||||
quantity: quantity,
|
||||
goodsTotal: getGoodsTotal(),
|
||||
couponDiscount: getCouponDiscount(),
|
||||
finalPrice: getFinalPrice()
|
||||
},
|
||||
couponValidation: selectedCoupon ? {
|
||||
isUsable: isCouponUsable(selectedCoupon, getGoodsTotal()),
|
||||
reason: getCouponUnusableReason(selectedCoupon, getGoodsTotal())
|
||||
} : null
|
||||
});
|
||||
|
||||
// 执行支付 - 移除这里的成功提示,让PaymentHandler统一处理
|
||||
await PaymentHandler.pay(orderData, paymentType);
|
||||
|
||||
// ✅ 移除双重成功提示 - PaymentHandler会处理成功提示
|
||||
// Taro.showToast({
|
||||
// title: '支付成功',
|
||||
// icon: 'success'
|
||||
// })
|
||||
} catch (error: any) {
|
||||
return navTo('/user/order/order?statusFilter=0', true)
|
||||
// console.error('支付失败:', error)
|
||||
|
||||
// 只处理PaymentHandler未处理的错误
|
||||
// if (!error.handled) {
|
||||
// let errorMessage = '支付失败,请重试';
|
||||
//
|
||||
// // 根据错误类型提供具体提示
|
||||
// if (error.message?.includes('余额不足')) {
|
||||
// errorMessage = '账户余额不足,请充值后重试';
|
||||
// } else if (error.message?.includes('优惠券')) {
|
||||
// errorMessage = '优惠券使用失败,请重新选择';
|
||||
// } else if (error.message?.includes('库存')) {
|
||||
// errorMessage = '商品库存不足,请减少购买数量';
|
||||
// } else if (error.message?.includes('地址')) {
|
||||
// errorMessage = '收货地址信息有误,请重新选择';
|
||||
// } else if (error.message) {
|
||||
// errorMessage = error.message;
|
||||
// }
|
||||
// Taro.showToast({
|
||||
// title: errorMessage,
|
||||
// icon: 'error'
|
||||
// })
|
||||
// console.log('跳去未付款的订单列表页面')
|
||||
// }
|
||||
} finally {
|
||||
setPayLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
// 统一的数据加载函数
|
||||
const loadAllData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
// 分别加载数据,避免类型推断问题
|
||||
let goodsRes: ShopGoods | null = null
|
||||
if (goodsId) {
|
||||
goodsRes = await getShopGoods(Number(goodsId))
|
||||
}
|
||||
|
||||
const [addressRes, paymentRes] = await Promise.all([
|
||||
listShopUserAddress({isDefault: true}),
|
||||
selectPayment({})
|
||||
])
|
||||
|
||||
// 设置商品信息
|
||||
if (goodsRes) {
|
||||
setGoods(goodsRes)
|
||||
}
|
||||
|
||||
// 设置默认收货地址
|
||||
if (addressRes && addressRes.length > 0) {
|
||||
setAddress(addressRes[0])
|
||||
}
|
||||
|
||||
// 设置支付方式
|
||||
if (paymentRes && paymentRes.length > 0) {
|
||||
setPayments(paymentRes.map((d) => ({
|
||||
type: d.type,
|
||||
name: d.name
|
||||
})))
|
||||
setPayment(paymentRes[0])
|
||||
}
|
||||
|
||||
// 加载优惠券(在商品信息加载完成后)
|
||||
if (goodsRes) {
|
||||
await loadUserCoupons()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载数据失败:', err)
|
||||
setError('加载数据失败,请重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
loadAllData()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadAllData()
|
||||
}, [goodsId]);
|
||||
|
||||
// 重新加载数据
|
||||
const handleRetry = () => {
|
||||
loadAllData()
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (error) {
|
||||
return (
|
||||
<View className="order-confirm-page">
|
||||
<View className="error-state">
|
||||
<Text className="error-text">{error}</Text>
|
||||
<Button onClick={handleRetry}>重新加载</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
if (loading || !goods) {
|
||||
return <OrderConfirmSkeleton/>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'order-confirm-page'}>
|
||||
<CellGroup>
|
||||
{
|
||||
address && (
|
||||
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
||||
<Space>
|
||||
<Location className={'text-gray-500'}/>
|
||||
<View className={'flex flex-col w-full justify-between items-start'}>
|
||||
<Space className={'flex flex-row w-full'}>
|
||||
<View className={'flex-wrap text-nowrap whitespace-nowrap text-gray-500'}>送至</View>
|
||||
<View className={'font-medium text-sm flex items-center w-full'}>
|
||||
<View
|
||||
style={{width: '64%'}}>{address.province} {address.city} {address.region} {address.address}</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
</Space>
|
||||
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
|
||||
</View>
|
||||
</Space>
|
||||
</Cell>
|
||||
)
|
||||
}
|
||||
{!address && (
|
||||
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
||||
<Space>
|
||||
<Location/>
|
||||
添加收货地址
|
||||
</Space>
|
||||
</Cell>
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell key={goods.goodsId}>
|
||||
<View className={'flex w-full justify-between gap-3'}>
|
||||
<View>
|
||||
<Image src={goods.image} mode={'aspectFill'} style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
}} lazyLoad={false}/>
|
||||
</View>
|
||||
<View className={'flex flex-col w-full ml-2'} style={{width: '100%'}}>
|
||||
<Text className={'font-medium w-full'}>{goods.name}</Text>
|
||||
<Text className={'number text-gray-400 text-sm py-2'}>80g/袋</Text>
|
||||
<View className={'flex justify-between items-center'}>
|
||||
<Text className={'text-red-500'}>¥{goods.price}</Text>
|
||||
<View className={'flex flex-col items-end gap-1'}>
|
||||
<ConfigProvider theme={customTheme}>
|
||||
<InputNumber
|
||||
value={quantity}
|
||||
min={1}
|
||||
max={goods.stock || 999}
|
||||
disabled={goods.canBuyNumber != 0}
|
||||
onChange={handleQuantityChange}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
{goods.stock !== undefined && (
|
||||
<Text className={'text-xs text-gray-400'}>
|
||||
库存 {goods.stock} 件
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title={'支付方式'}
|
||||
extra={(
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<View className={'text-gray-900'}>{payment?.name}</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)}
|
||||
onClick={() => setIsVisible(true)}
|
||||
/>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title={`商品总价(共${quantity}件)`}
|
||||
extra={<View className={'font-medium'}>¥{getGoodsTotal().toFixed(2)}</View>}
|
||||
/>
|
||||
<Cell
|
||||
title={'优惠券'}
|
||||
extra={(
|
||||
<View className={'flex justify-between items-center'}>
|
||||
<View className={'text-red-500 text-sm mr-1'}>
|
||||
{selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'}
|
||||
</View>
|
||||
{(() => {
|
||||
const usableCoupons = filterUsableCoupons(availableCoupons, getGoodsTotal())
|
||||
if (usableCoupons.length > 0 && !selectedCoupon) {
|
||||
return (
|
||||
<View className={'flex items-center'}>
|
||||
<View className={'bg-red-500 text-white text-xs px-2 py-1 rounded mr-2'}>
|
||||
{usableCoupons.length}张可用
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)
|
||||
} else if (usableCoupons.length > 0) {
|
||||
return (
|
||||
<View className={'flex items-center'}>
|
||||
<View className={'bg-green-500 text-white text-xs px-2 py-1 rounded mr-2'}>
|
||||
已选择
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
return <ArrowRight className={'text-gray-400'} size={14}/>
|
||||
}
|
||||
})()
|
||||
}
|
||||
</View>
|
||||
)}
|
||||
onClick={() => setCouponVisible(true)}
|
||||
/>
|
||||
<Cell title={'配送费'} extra={'¥0.00'}/>
|
||||
<Cell extra={(
|
||||
<View className={'flex items-end gap-2'}>
|
||||
<Text>已优惠</Text>
|
||||
<Text className={'text-red-500 text-sm'}>¥{getCouponDiscount().toFixed(2)}</Text>
|
||||
<Text className={'ml-2'}>实付</Text>
|
||||
<Text className={'text-gray-700 font-bold'} style={{fontSize: '18px'}}>¥{getFinalPrice().toFixed(2)}</Text>
|
||||
</View>
|
||||
)}/>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell title={'订单备注'} extra={(
|
||||
<Input
|
||||
placeholder={'选填,请先和商家协商一致'}
|
||||
style={{padding: '0'}}
|
||||
value={orderRemark}
|
||||
onChange={(value) => setOrderRemark(value)}
|
||||
maxLength={100}
|
||||
/>
|
||||
)}/>
|
||||
</CellGroup>
|
||||
|
||||
{/* 支付方式选择 */}
|
||||
<ActionSheet
|
||||
visible={isVisible}
|
||||
options={payments}
|
||||
onSelect={handleSelect}
|
||||
onCancel={() => setIsVisible(false)}
|
||||
/>
|
||||
|
||||
{/* 优惠券选择弹窗 */}
|
||||
<Popup
|
||||
visible={couponVisible}
|
||||
position="bottom"
|
||||
onClose={() => setCouponVisible(false)}
|
||||
style={{height: '60vh'}}
|
||||
>
|
||||
<View className="coupon-popup">
|
||||
<View className="coupon-popup__header">
|
||||
<Text className="text-sm">选择优惠券</Text>
|
||||
<Button
|
||||
size="small"
|
||||
fill="none"
|
||||
onClick={() => setCouponVisible(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="coupon-popup__content">
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
<Gap height={50}/>
|
||||
|
||||
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
|
||||
<View className={'btn-bar flex justify-between items-center'}>
|
||||
<div className={'flex flex-col justify-center items-start mx-4'}>
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<span className={'total-price text-sm text-gray-500'}>实付金额:</span>
|
||||
<span className={'text-red-500 text-xl font-bold'}>¥{getFinalPrice().toFixed(2)}</span>
|
||||
</View>
|
||||
{selectedCoupon && (
|
||||
<View className={'text-xs text-gray-400'}>
|
||||
已优惠 ¥{getCouponDiscount().toFixed(2)}
|
||||
</View>
|
||||
)}
|
||||
</div>
|
||||
<div className={'buy-btn mx-4'}>
|
||||
<Button
|
||||
type="success"
|
||||
size="large"
|
||||
loading={payLoading}
|
||||
onClick={() => onPay(goods)}
|
||||
>
|
||||
{payLoading ? '支付中...' : '立即付款'}
|
||||
</Button>
|
||||
</div>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderConfirm;
|
||||
4
src/shop/orderConfirmCart/index.config.ts
Normal file
4
src/shop/orderConfirmCart/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单确认',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
44
src/shop/orderConfirmCart/index.scss
Normal file
44
src/shop/orderConfirmCart/index.scss
Normal file
@@ -0,0 +1,44 @@
|
||||
.order-confirm-page {
|
||||
padding-bottom: 100px; // 留出底部固定按钮的空间
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.total-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.address-bottom-line{
|
||||
width: 100%;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
background: #fff;
|
||||
padding: 26rpx 49rpx 0 34rpx;
|
||||
position: relative;
|
||||
&:before {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
background: repeating-linear-gradient(-45deg, #ff6c6c, #ff6c6c 20%, transparent 0, transparent 25%, #1989fa 0,
|
||||
#1989fa 45%, transparent 0, transparent 50%);
|
||||
background-size: 120px;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
211
src/shop/orderConfirmCart/index.tsx
Normal file
211
src/shop/orderConfirmCart/index.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Image, Button, Cell, CellGroup, Input, Space} from '@nutui/nutui-react-taro'
|
||||
import {Location, ArrowRight} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {getShopGoods} from "@/api/shop/shopGoods";
|
||||
import {View} from '@tarojs/components';
|
||||
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
|
||||
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
|
||||
import './index.scss'
|
||||
import {useCart, CartItem} from "@/hooks/useCart";
|
||||
import Gap from "@/components/Gap";
|
||||
import {Payment} from "@/api/system/payment/model";
|
||||
import {PaymentHandler, PaymentType, buildCartOrder} from "@/utils/payment";
|
||||
|
||||
const OrderConfirm = () => {
|
||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||
const [address, setAddress] = useState<ShopUserAddress>()
|
||||
const [payment, setPayment] = useState<Payment>()
|
||||
const [checkoutItems, setCheckoutItems] = useState<CartItem[]>([]);
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const goodsId = router?.params?.goodsId;
|
||||
|
||||
const {
|
||||
cartItems,
|
||||
removeFromCart
|
||||
} = useCart();
|
||||
|
||||
const reload = async () => {
|
||||
const address = await listShopUserAddress({isDefault: true});
|
||||
if (address.length > 0) {
|
||||
console.log(address, '111')
|
||||
setAddress(address[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 加载结算商品数据
|
||||
const loadCheckoutItems = () => {
|
||||
try {
|
||||
const checkoutData = Taro.getStorageSync('checkout_items');
|
||||
if (checkoutData) {
|
||||
const items = JSON.parse(checkoutData) as CartItem[];
|
||||
setCheckoutItems(items);
|
||||
// 清除临时存储的数据
|
||||
Taro.removeStorageSync('checkout_items');
|
||||
} else {
|
||||
// 如果没有选中商品数据,使用全部购物车商品
|
||||
setCheckoutItems(cartItems);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载结算商品失败:', error);
|
||||
setCheckoutItems(cartItems);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一支付入口
|
||||
*/
|
||||
const onPay = async () => {
|
||||
// 基础校验
|
||||
if (!address) {
|
||||
Taro.showToast({
|
||||
title: '请选择收货地址',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkoutItems || checkoutItems.length === 0) {
|
||||
Taro.showToast({
|
||||
title: '没有要结算的商品',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建订单数据
|
||||
const orderData = buildCartOrder(
|
||||
checkoutItems.map(item => ({
|
||||
goodsId: item.goodsId!,
|
||||
quantity: item.quantity || 1
|
||||
})),
|
||||
address.id,
|
||||
{
|
||||
comments: '购物车下单',
|
||||
deliveryType: 0
|
||||
}
|
||||
);
|
||||
|
||||
// 根据支付方式选择支付类型,默认微信支付
|
||||
const paymentType = payment?.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
|
||||
|
||||
// 执行支付
|
||||
await PaymentHandler.pay(orderData, paymentType, {
|
||||
onSuccess: () => {
|
||||
// 支付成功后,从购物车中移除已下单的商品
|
||||
checkoutItems.forEach(item => {
|
||||
removeFromCart(item.goodsId);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (goodsId) {
|
||||
getShopGoods(Number(goodsId)).then(res => {
|
||||
setGoods(res);
|
||||
}).catch(error => {
|
||||
console.error("Failed to fetch goods detail:", error);
|
||||
});
|
||||
}
|
||||
reload().then();
|
||||
loadCheckoutItems();
|
||||
}, [goodsId, cartItems]);
|
||||
|
||||
// 计算总价
|
||||
const getTotalPrice = () => {
|
||||
return checkoutItems.reduce((total, item) => {
|
||||
return total + (parseFloat(item.price) * item.quantity);
|
||||
}, 0).toFixed(2);
|
||||
};
|
||||
|
||||
// 计算商品总数量
|
||||
const getTotalQuantity = () => {
|
||||
return checkoutItems.reduce((total, item) => total + item.quantity, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'order-confirm-page'}>
|
||||
<CellGroup>
|
||||
{
|
||||
address && (
|
||||
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
||||
<Space>
|
||||
<Location/>
|
||||
<View className={'flex flex-col w-full justify-between items-start'}>
|
||||
<Space className={'flex flex-row w-full font-medium'}>
|
||||
<View className={'flex-wrap text-nowrap whitespace-nowrap'}>送至</View>
|
||||
<View style={{width: '64%'}}
|
||||
className={'line-clamp-1 relative'}>{address.province} {address.city} {address.region} {address.address}
|
||||
</View>
|
||||
</Space>
|
||||
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
|
||||
</View>
|
||||
</Space>
|
||||
</Cell>
|
||||
)
|
||||
}
|
||||
{!address && (
|
||||
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
||||
<Space>
|
||||
<Location/>
|
||||
添加收货地址
|
||||
</Space>
|
||||
</Cell>
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
{checkoutItems.map((goods, _) => (
|
||||
<Cell key={goods.goodsId}>
|
||||
<Space>
|
||||
<Image src={goods.image} mode={'aspectFill'} style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
}} lazyLoad={false}/>
|
||||
<View className={'flex flex-col'}>
|
||||
<View className={'font-medium w-full'}>{goods.name}</View>
|
||||
<View className={'number text-gray-400 text-sm py-2'}>80g/袋</View>
|
||||
<Space className={'flex justify-start items-center'}>
|
||||
<View className={'text-red-500'}>¥{goods.price}</View>
|
||||
<View className={'text-gray-500 text-sm'}>x {goods.quantity}</View>
|
||||
</Space>
|
||||
</View>
|
||||
</Space>
|
||||
</Cell>
|
||||
))}
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell title={`商品总价(共${getTotalQuantity()}件)`} extra={<View className={'font-medium'}>{'¥' + getTotalPrice()}</View>}/>
|
||||
<Cell title={'优惠券'} extra={(
|
||||
<View className={'flex justify-between items-center'}>
|
||||
<View className={'text-red-500 text-sm mr-1'}>-¥0.00</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)}/>
|
||||
{/*<Cell title={'配送费'} extra={'¥' + 10}/>*/}
|
||||
<Cell title={'订单备注'} extra={(
|
||||
<Input placeholder={'选填,请先和商家协商一致'} style={{ padding: '0'}}/>
|
||||
)}/>
|
||||
</CellGroup>
|
||||
|
||||
<Gap height={50} />
|
||||
|
||||
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
|
||||
<View className={'btn-bar flex justify-between items-center'}>
|
||||
<div className={'flex justify-center items-center mx-4'}>
|
||||
<span className={'total-price text-sm text-gray-500'}>实付金额:</span>
|
||||
<span className={'text-red-500 text-xl font-bold'}>¥{getTotalPrice()}</span>
|
||||
</div>
|
||||
<div className={'buy-btn mx-4'}>
|
||||
<Button type="success" size="large" onClick={onPay}>立即付款</Button>
|
||||
</div>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderConfirm;
|
||||
4
src/shop/orderDetail/index.config.ts
Normal file
4
src/shop/orderDetail/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单详情',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
26
src/shop/orderDetail/index.scss
Normal file
26
src/shop/orderDetail/index.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.order-detail-page {
|
||||
padding-bottom: 80px; // 留出底部固定按钮的空间
|
||||
|
||||
.nut-cell-group__title {
|
||||
padding: 10px 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.nut-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/shop/orderDetail/index.tsx
Normal file
175
src/shop/orderDetail/index.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Cell, CellGroup, Image, Space, Button} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||
import {getShopOrder, updateShopOrder} from "@/api/shop/shopOrder";
|
||||
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
|
||||
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
|
||||
import dayjs from "dayjs";
|
||||
import PaymentCountdown from "@/components/PaymentCountdown";
|
||||
import './index.scss'
|
||||
|
||||
const OrderDetail = () => {
|
||||
const [order, setOrder] = useState<ShopOrder | null>(null);
|
||||
const [orderGoodsList, setOrderGoodsList] = useState<ShopOrderGoods[]>([]);
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const orderId = router?.params?.orderId;
|
||||
|
||||
// 处理支付超时
|
||||
const handlePaymentExpired = async () => {
|
||||
if (!order) return;
|
||||
|
||||
try {
|
||||
// 自动取消过期订单
|
||||
await updateShopOrder({
|
||||
...order,
|
||||
orderStatus: 2 // 已取消
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
setOrder(prev => prev ? {...prev, orderStatus: 2} : null);
|
||||
|
||||
Taro.showToast({
|
||||
title: '订单已自动取消',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('自动取消订单失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getOrderStatusText = (order: ShopOrder) => {
|
||||
// 优先检查订单状态
|
||||
if (order.orderStatus === 2) return '已取消';
|
||||
if (order.orderStatus === 3) return '取消中';
|
||||
if (order.orderStatus === 4) return '退款申请中';
|
||||
if (order.orderStatus === 5) return '退款被拒绝';
|
||||
if (order.orderStatus === 6) return '退款成功';
|
||||
if (order.orderStatus === 7) return '客户端申请退款';
|
||||
|
||||
// 检查支付状态 (payStatus为boolean类型)
|
||||
if (!order.payStatus) return '待付款';
|
||||
|
||||
// 已付款后检查发货状态
|
||||
if (order.deliveryStatus === 10) return '待发货';
|
||||
if (order.deliveryStatus === 20) return '待收货';
|
||||
if (order.deliveryStatus === 30) return '已收货';
|
||||
|
||||
// 最后检查订单完成状态
|
||||
if (order.orderStatus === 1) return '已完成';
|
||||
if (order.orderStatus === 0) return '未使用';
|
||||
|
||||
return '未知状态';
|
||||
};
|
||||
|
||||
const getPayTypeText = (payType?: number) => {
|
||||
switch (payType) {
|
||||
case 0:
|
||||
return '余额支付';
|
||||
case 1:
|
||||
return '微信支付';
|
||||
case 102:
|
||||
return '微信Native';
|
||||
case 2:
|
||||
return '会员卡支付';
|
||||
case 3:
|
||||
return '支付宝';
|
||||
case 4:
|
||||
return '现金';
|
||||
case 5:
|
||||
return 'POS机';
|
||||
default:
|
||||
return '未知支付方式';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (orderId) {
|
||||
console.log('shop-goods', orderId)
|
||||
getShopOrder(Number(orderId)).then(async (res) => {
|
||||
setOrder(res);
|
||||
|
||||
// 获取订单商品列表
|
||||
const goodsRes = await listShopOrderGoods({orderId: Number(orderId)});
|
||||
if (goodsRes && goodsRes.length > 0) {
|
||||
setOrderGoodsList(goodsRes);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Failed to fetch order detail:", error);
|
||||
});
|
||||
}
|
||||
}, [orderId]);
|
||||
|
||||
if (!order) {
|
||||
return <div>加载中...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'order-detail-page'}>
|
||||
{/* 支付倒计时显示 - 详情页实时更新 */}
|
||||
{!order.payStatus && order.orderStatus !== 2 && (
|
||||
<div className="order-detail-countdown flex justify-center p-4 border-b border-gray-50">
|
||||
<PaymentCountdown
|
||||
createTime={order.createTime}
|
||||
payStatus={order.payStatus}
|
||||
realTime={true}
|
||||
showSeconds={true}
|
||||
mode="badge"
|
||||
onExpired={handlePaymentExpired}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellGroup>
|
||||
{orderGoodsList.map((item, index) => (
|
||||
<Cell key={index}>
|
||||
<div className={'flex items-center'}>
|
||||
<Image src={item.image || '/default-goods.png'} width="80" height="80" lazyLoad={false}/>
|
||||
<div className={'ml-2'}>
|
||||
<div className={'text-sm font-bold'}>{item.goodsName}</div>
|
||||
{item.spec && <div className={'text-gray-500 text-xs'}>规格:{item.spec}</div>}
|
||||
<div className={'text-gray-500 text-xs'}>数量:{item.totalNum}</div>
|
||||
<div className={'text-red-500 text-lg'}>¥{item.price}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
))}
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell title="订单编号" description={order.orderNo}/>
|
||||
<Cell title="下单时间" description={dayjs(order.createTime).format('YYYY-MM-DD HH:mm:ss')}/>
|
||||
<Cell title="订单状态" description={getOrderStatusText(order)}/>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell title="收货人" description={order.realName}/>
|
||||
<Cell title="手机号" description={order.phone}/>
|
||||
<Cell title="收货地址" description={order.address}/>
|
||||
</CellGroup>
|
||||
|
||||
{order.payStatus && (
|
||||
<CellGroup>
|
||||
<Cell title="支付方式" description={getPayTypeText(order.payType)}/>
|
||||
<Cell title="实付金额" description={`¥${order.payPrice}`}/>
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
<View className={'h5-div fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-5 border-t border-gray-200'}>
|
||||
<View className={'flex justify-end px-4'}>
|
||||
<Space>
|
||||
{!order.payStatus && <Button onClick={() => console.log('取消订单')}>取消订单</Button>}
|
||||
{!order.payStatus && <Button type="primary" onClick={() => console.log('立即支付')}>立即支付</Button>}
|
||||
{order.orderStatus === 1 && <Button onClick={() => console.log('申请退款')}>申请退款</Button>}
|
||||
{order.deliveryStatus === 20 &&
|
||||
<Button type="primary" onClick={() => console.log('确认收货')}>确认收货</Button>}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderDetail;
|
||||
33
src/shop/search/components/GoodsItem.scss
Normal file
33
src/shop/search/components/GoodsItem.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
// 使用与首页相同的样式,主要依赖Tailwind CSS类名
|
||||
.buy-btn {
|
||||
background: linear-gradient(to right, #1cd98a, #24ca94);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cart-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 20px 0 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.car-no {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
58
src/shop/search/components/GoodsItem.tsx
Normal file
58
src/shop/search/components/GoodsItem.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { View } from '@tarojs/components'
|
||||
import { Image } from '@nutui/nutui-react-taro'
|
||||
import { Share } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||
import './GoodsItem.scss'
|
||||
|
||||
interface GoodsItemProps {
|
||||
goods: ShopGoods
|
||||
}
|
||||
|
||||
const GoodsItem = ({ goods }: GoodsItemProps) => {
|
||||
// 跳转到商品详情
|
||||
const goToDetail = () => {
|
||||
Taro.navigateTo({
|
||||
url: `/shop/goodsDetail/index?id=${goods.goodsId}`
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
||||
<Image
|
||||
src={goods.image || ''}
|
||||
mode={'aspectFit'}
|
||||
lazyLoad={false}
|
||||
radius="10px 10px 0 0"
|
||||
height="180"
|
||||
onClick={goToDetail}
|
||||
/>
|
||||
<View className={'flex flex-col p-2 rounded-lg'}>
|
||||
<View>
|
||||
<View className={'car-no text-sm'}>{goods.name || goods.goodsName}</View>
|
||||
<View className={'flex justify-between text-xs py-1'}>
|
||||
<span className={'text-orange-500'}>{goods.comments || ''}</span>
|
||||
<span className={'text-gray-400'}>已售 {goods.sales || 0}</span>
|
||||
</View>
|
||||
<View className={'flex justify-between items-center py-2'}>
|
||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
||||
<span className={'text-xs'}>¥</span>
|
||||
<span className={'font-bold text-2xl'}>{goods.price || '0.00'}</span>
|
||||
</View>
|
||||
<View className={'buy-btn'}>
|
||||
<View className={'cart-icon'}>
|
||||
<Share size={20} className={'mx-4 mt-2'}
|
||||
onClick={goToDetail}/>
|
||||
</View>
|
||||
<View className={'text-white pl-4 pr-5'}
|
||||
onClick={goToDetail}>购买
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GoodsItem
|
||||
3
src/shop/search/index.config.ts
Normal file
3
src/shop/search/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品搜索'
|
||||
})
|
||||
103
src/shop/search/index.scss
Normal file
103
src/shop/search/index.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
.search-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
|
||||
// 搜索输入框样式
|
||||
.search-input-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 20px;
|
||||
padding: 0 12px;
|
||||
|
||||
.search-icon {
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
|
||||
input {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 0 16px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-content {
|
||||
padding-top: calc(32px + env(safe-area-inset-top));
|
||||
|
||||
.search-history {
|
||||
background: #fff;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
.history-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.history-item {
|
||||
padding: 8px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 16px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background: #e5e5e5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
.result-header {
|
||||
padding: 16px;
|
||||
color: #666;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.loading-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 0;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
237
src/shop/search/index.tsx
Normal file
237
src/shop/search/index.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import {SetStateAction, useEffect, useState} from 'react'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {Loading, Empty, InfiniteLoading, Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {Search} from '@nutui/icons-react-taro';
|
||||
import {ShopGoods} from '@/api/shop/shopGoods/model'
|
||||
import {pageShopGoods} from '@/api/shop/shopGoods'
|
||||
import GoodsItem from './components/GoodsItem'
|
||||
import './index.scss'
|
||||
|
||||
const SearchPage = () => {
|
||||
const router = useRouter()
|
||||
const [keywords, setKeywords] = useState<string>('')
|
||||
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [searchHistory, setSearchHistory] = useState<string[]>([])
|
||||
|
||||
// 从路由参数获取搜索关键词
|
||||
useEffect(() => {
|
||||
const {keywords: routeKeywords} = router.params || {}
|
||||
if (routeKeywords) {
|
||||
setKeywords(decodeURIComponent(routeKeywords))
|
||||
handleSearch(decodeURIComponent(routeKeywords), 1).then()
|
||||
}
|
||||
|
||||
// 加载搜索历史
|
||||
loadSearchHistory()
|
||||
}, [])
|
||||
|
||||
// 加载搜索历史
|
||||
const loadSearchHistory = () => {
|
||||
try {
|
||||
const history = Taro.getStorageSync('search_history') || []
|
||||
setSearchHistory(history)
|
||||
} catch (error) {
|
||||
console.error('加载搜索历史失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存搜索历史
|
||||
const saveSearchHistory = (keyword: string) => {
|
||||
try {
|
||||
let history = Taro.getStorageSync('search_history') || []
|
||||
// 去重并添加到开头
|
||||
history = history.filter((item: string) => item !== keyword)
|
||||
history.unshift(keyword)
|
||||
// 只保留最近10条
|
||||
history = history.slice(0, 10)
|
||||
Taro.setStorageSync('search_history', history)
|
||||
setSearchHistory(history)
|
||||
} catch (error) {
|
||||
console.error('保存搜索历史失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeywords = (keywords: SetStateAction<string>) => {
|
||||
setKeywords(keywords)
|
||||
handleSearch(typeof keywords === "string" ? keywords : '').then()
|
||||
}
|
||||
|
||||
// 搜索商品
|
||||
const handleSearch = async (searchKeywords: string, pageNum: number = 1) => {
|
||||
if (!searchKeywords.trim()) {
|
||||
Taro.showToast({
|
||||
title: '请输入搜索关键词',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const params = {
|
||||
keywords: searchKeywords.trim(),
|
||||
page: pageNum,
|
||||
size: 10,
|
||||
isShow: 1 // 只搜索上架商品
|
||||
}
|
||||
|
||||
const result = await pageShopGoods(params)
|
||||
|
||||
if (pageNum === 1) {
|
||||
setGoodsList(result?.list || [])
|
||||
setTotal(result?.count || 0)
|
||||
// 保存搜索历史
|
||||
saveSearchHistory(searchKeywords.trim())
|
||||
} else {
|
||||
setGoodsList(prev => [...prev, ...(result?.list || [])])
|
||||
}
|
||||
|
||||
setHasMore((result?.list?.length || 0) >= 10)
|
||||
setPage(pageNum)
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
Taro.showToast({
|
||||
title: '搜索失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (!loading && hasMore && keywords.trim()) {
|
||||
handleSearch(keywords, page + 1).then()
|
||||
}
|
||||
}
|
||||
|
||||
// 点击历史搜索
|
||||
const onHistoryClick = (keyword: string) => {
|
||||
setKeywords(keyword)
|
||||
setPage(1)
|
||||
handleSearch(keyword, 1)
|
||||
}
|
||||
|
||||
// 清空搜索历史
|
||||
const clearHistory = () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清空搜索历史吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
Taro.removeStorageSync('search_history')
|
||||
setSearchHistory([])
|
||||
} catch (error) {
|
||||
console.error('清空搜索历史失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="search-page pt-3">
|
||||
<div className={'px-2'}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: '#ffffff',
|
||||
padding: '0 5px',
|
||||
borderRadius: '20px',
|
||||
marginTop: '5px',
|
||||
}}
|
||||
>
|
||||
<Search size={18} className={'ml-2 text-gray-400'}/>
|
||||
<Input
|
||||
placeholder="搜索商品"
|
||||
value={keywords}
|
||||
onChange={handleKeywords}
|
||||
onConfirm={() => handleSearch(keywords)}
|
||||
style={{padding: '9px 8px'}}
|
||||
/>
|
||||
<div
|
||||
className={'flex items-center'}
|
||||
>
|
||||
<Button type="success" style={{background: 'linear-gradient(to bottom, #1cd98a, #24ca94)'}}
|
||||
onClick={() => handleSearch(keywords)}>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/*<SearchBar style={{height: `${statusBarHeight}px`}} shape="round" placeholder="搜索商品" onChange={setKeywords} onSearch={handleSearch}/>*/}
|
||||
|
||||
{/* 搜索内容 */}
|
||||
<View className="search-content">
|
||||
{/* 搜索历史 */}
|
||||
{!keywords && searchHistory.length > 0 && (
|
||||
<View className="search-history">
|
||||
<View className="history-header">
|
||||
<View className="text-sm">搜索历史</View>
|
||||
<View className={'text-gray-400'} onClick={clearHistory}>清空</View>
|
||||
</View>
|
||||
<View className="history-list">
|
||||
{searchHistory.map((item, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className="history-item"
|
||||
onClick={() => onHistoryClick(item)}
|
||||
>
|
||||
{item}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 搜索结果 */}
|
||||
{keywords && (
|
||||
<View className="search-results">
|
||||
{/* 结果统计 */}
|
||||
<View className="result-header">
|
||||
找到 {total} 件相关商品
|
||||
</View>
|
||||
|
||||
{/* 商品列表 */}
|
||||
{loading && page === 1 ? (
|
||||
<View className="loading-wrapper">
|
||||
<Loading>搜索中...</Loading>
|
||||
</View>
|
||||
) : goodsList.length > 0 ? (
|
||||
<div className={'py-3'}>
|
||||
<div className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
||||
<InfiniteLoading
|
||||
hasMore={hasMore}
|
||||
// @ts-ignore
|
||||
onLoadMore={loadMore}
|
||||
loadingText="加载中..."
|
||||
loadMoreText="没有更多了"
|
||||
>
|
||||
{goodsList.map((item) => (
|
||||
<GoodsItem key={item.goodsId} goods={item}/>
|
||||
))}
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Empty description="暂无相关商品"/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchPage
|
||||
Reference in New Issue
Block a user