Ver Fonte

feat: enhance cart functionality and introduce select good popup host

- Updated the API URL for the development environment to https://bk.shuhi.com.
- Refactored cart page to utilize an event bus for improved component communication and added a handler for cart load completion.
- Introduced a new component, SelectGoodPopupHost, to manage product selection popups across various pages, enhancing modularity and reusability.
- Removed direct references to the SelectGoodPopup component in favor of event-based communication for opening popups.
- Cleaned up unused code and improved the overall structure of the cart and product selection functionalities.
ylong há 3 semanas atrás
pai
commit
bf310da073

+ 1 - 1
.env.dev.js

@@ -1,5 +1,5 @@
 export default {
     "NODE_ENV": 'development',
-    "apiUrl":"https://bpi.shuhi.com",
+    "apiUrl":"https://bk.shuhi.com",
     "apiUrlPrefix":"/api",
 };

+ 3 - 2
App.vue

@@ -1,5 +1,6 @@
 <script>
 import { eventBus } from '@/utils/event-bus'
+import { updateCartBadge } from '@/utils/uniapp-api.js'
 import { isPolicyConsented, requestShowPrivacyPopup } from '@/utils/policy-consent-manager.js'
 
 export default {
@@ -40,7 +41,7 @@ export default {
 		this.handleLaunchWithPolicy(options)
 	},
 	onShow(options) {
-		this.$updateCartBadge();
+		updateCartBadge();
 		this.handleAppShowOptions(options);
 	},
 	onUnload() {
@@ -170,7 +171,7 @@ export default {
 
 								eventBus.emit('loginSuccess', { response: response.data, params: data })
 								that.globalData.isColdLaunch = false;
-								that.$updateCartBadge();
+								updateCartBadge();
 							}
 						});
 				},

+ 1 - 0
api/config.js

@@ -76,6 +76,7 @@ export const httpResponse = (res) => {
         uni.showToast({
           icon: "none",
           title: res.data.msg || "服务器异常,请联系客服",
+          duration: 3000,
         });
         return resolve(res.data);
       }

+ 33 - 0
components/select-good-popup-host.vue

@@ -0,0 +1,33 @@
+<template>
+    <select-good-popup ref="popup" @notice-change="onNoticeChange" />
+</template>
+
+<script>
+import SelectGoodPopup from '@/pages-sell/components/select-good-popup/index.vue'
+import { eventBus } from '@/utils/event-bus'
+
+export default {
+    name: 'SelectGoodPopupHost',
+    components: {
+        SelectGoodPopup,
+    },
+    mounted() {
+        this.openHandler = (payload = {}) => {
+            const { product, sourceFrom } = payload
+            if (!product) return
+            this.$refs.popup?.open(product, sourceFrom)
+        }
+        eventBus.on('openSelectGoodPopup', this.openHandler)
+    },
+    beforeDestroy() {
+        if (this.openHandler) {
+            eventBus.off('openSelectGoodPopup', this.openHandler)
+        }
+    },
+    methods: {
+        onNoticeChange(value) {
+            eventBus.emit('selectGoodNoticeChange', value)
+        },
+    },
+}
+</script>

+ 60 - 78
pages-car/pages/index.vue

@@ -1,45 +1,47 @@
 <template>
     <view class="cart-page">
-        <!-- 购物车列表 -->
-        <view class="cart-list">
-            <!-- <u-swipe-action :show="item.show" :index="index" v-for="(item, index) in cartList" :key="item.id"
-                @click="clickAction" @open="openAction" @touchstart="swipeStart" :options="actionOptions">
-                <cart-item :item="item" @check="handleCheck" @changeNum="handleChangeNum" @reduce="handleReduce"
-                    @selectCondition="onSelectCondition"></cart-item>
-            </u-swipe-action> -->
-            <uni-swipe-action>
-                <uni-swipe-action-item v-for="(item, index) in cartList" :key="item.id" :right-options="uniActionOptions" @click="uniClickAction($event, index)" @change="uniSwipeChange($event, index)" :show="item.show">
-                    <cart-item :item="item" @check="handleCheck" @changeNum="handleChangeNum" @reduce="handleReduce"
-                        @selectCondition="onSelectCondition"></cart-item>
-                </uni-swipe-action-item>
-            </uni-swipe-action>
-        </view>
-
-        <!-- 品相切换弹窗 -->
-        <condition-popup ref="conditionPopup" @select="handleConditionUpdate"></condition-popup>
-
-        <!-- 空状态 -->
-        <view class="empty-cart" v-if="cartList.length === 0 && !loading" style="margin:15% 0">
-            <u-empty text="购物车空空如也" mode="car"></u-empty>
-        </view>
-
-        <!-- 为你推荐 -->
-        <view class="recommend-section" v-if="recommendList.length > 0">
-            <view class="section-title">
-                <text class="line"></text>
-                <text class="text">为你推荐</text>
-                <text class="line"></text>
+        <page-scroll
+            ref="pageRef"
+            url="/token/shop/cart/list"
+            method="post"
+            :page-size="10"
+            :immediate="false"
+            slotEmpty
+            emptyText="购物车空空如也"
+            height="100vh"
+            bgColor="#f5f5f5"
+            @updateList="handleUpdateList"
+        >
+            <view class="cart-list" v-if="cartList.length > 0">
+                <uni-swipe-action>
+                    <uni-swipe-action-item v-for="(item, index) in cartList" :key="item.id" :right-options="uniActionOptions" @click="uniClickAction($event, index)" @change="uniSwipeChange($event, index)" :show="item.show">
+                        <cart-item :item="item" @check="handleCheck" @changeNum="handleChangeNum" @reduce="handleReduce"
+                            @selectCondition="onSelectCondition"></cart-item>
+                    </uni-swipe-action-item>
+                </uni-swipe-action>
             </view>
-            <view class="recommend-grid">
-                <view class="grid-item" v-for="(item, index) in recommendList" :key="index">
-                    <image :src="item.cover" mode="aspectFill" class="cover"></image>
-                    <view class="title">{{ item.title }}</view>
+
+            <!-- 为你推荐 -->
+            <view class="recommend-section" v-if="recommendList.length > 0">
+                <view class="section-title">
+                    <text class="line"></text>
+                    <text class="text">为你推荐</text>
+                    <text class="line"></text>
+                </view>
+                <view class="recommend-grid">
+                    <view class="grid-item" v-for="(item, index) in recommendList" :key="index">
+                        <image :src="item.cover" mode="aspectFill" class="cover"></image>
+                        <view class="title">{{ item.title }}</view>
+                    </view>
                 </view>
             </view>
-        </view>
 
-        <!-- 底部占位 -->
-        <view style="height: 120rpx;"></view>
+            <!-- 底部占位,避免被结算栏遮挡 -->
+            <view style="height: 120rpx;"></view>
+        </page-scroll>
+
+        <!-- 品相切换弹窗 -->
+        <condition-popup ref="conditionPopup" @select="handleConditionUpdate"></condition-popup>
 
         <!-- 悬浮清空按钮 -->
         <!-- <view class="floating-clear-btn" v-if="cartList.length > 0" @click="handleClearCart">
@@ -89,6 +91,7 @@
     import ReduceQrcode from '@/components/reduce-qrcode/reduce-qrcode.vue';
     import CommonDialog from '@/components/common-dialog.vue';
     import ConditionPopup from '../components/condition-popup.vue';
+    import pageScroll from '@/components/pageScroll/index.vue';
     import { eventBus } from '@/utils/event-bus.js';
 
     export default {
@@ -97,11 +100,11 @@
             PriceReductionPopup,
             ReduceQrcode,
             CommonDialog,
-            ConditionPopup
+            ConditionPopup,
+            pageScroll
         },
         data() {
             return {
-                loading: true,
                 currentItem: null,
                 isAllSelected: false,
                 cartList: [],
@@ -214,10 +217,27 @@
         },
         methods: {
             refreshCartPage() {
-                this.loadData();
+                this.$nextTick(() => {
+                    this.$refs.pageRef?.loadData(true);
+                });
                 this.getShippingText();
                 this.$updateCartBadge();
             },
+            handleUpdateList(list) {
+                const storageCheckedIds = uni.getStorageSync('cartCheckedIds') || [];
+                const prevCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
+                const checkedIds = [...new Set([...storageCheckedIds, ...prevCheckedIds])];
+
+                this.cartList = (list || []).map(item => ({
+                    ...item,
+                    checked: checkedIds.includes(item.id),
+                    show: 'none'
+                }));
+
+                const finalCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
+                uni.setStorageSync('cartCheckedIds', finalCheckedIds);
+                eventBus.emit('cartPageLoadDone');
+            },
             getShippingText() {
                 this.$u.api.getShippingTempleteAjax().then(res => {
                     if (res.code === 200) {
@@ -272,44 +292,6 @@
                     if (index != idx) this.cartList[idx].show = false;
                 })
             },
-            loadData() {
-                this.loading = true;
-                // 获取购物车列表
-                this.$u.api.getShopCartListAjax({}).then(res => {
-                    this.loading = false;
-
-                    // 获取本地缓存的选中状态,和当前列表的选中状态合并
-                    let storageCheckedIds = uni.getStorageSync('cartCheckedIds') || [];
-                    const currentCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
-
-                    // 合并并去重
-                    let checkedIds = [...new Set([...storageCheckedIds, ...currentCheckedIds])];
-
-                    // 本地添加选中状态属性
-                    const list = res.rows || [];
-                    this.cartList = list.map(item => {
-                        return {
-                            ...item,
-                            checked: checkedIds.includes(item.id),
-                            show: 'none'
-                        };
-                    });
-
-                    // 更新缓存中存在的且当前在购物车里的项
-                    const finalCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
-                    uni.setStorageSync('cartCheckedIds', finalCheckedIds);
-
-                }).catch(() => {
-                    this.loading = false;
-                });
-
-                // 获取推荐列表
-                // this.$u.api.getSearchRecommendAjax().then(res => {
-                //     if (res.code == 200) {
-                //         this.recommendList = res.data || [];
-                //     }
-                // });
-            },
             handleCheck({ id, checked }) {
                 const item = this.cartList.find(i => i.id === id);
                 if (item) {
@@ -492,7 +474,7 @@
                     uni.hideLoading();
                     this.$u.toast('更新成功');
 
-                    this.loadData();
+                    this.$refs.pageRef?.loadData(true);
                 }).catch(() => {
                     uni.hideLoading();
                 });

+ 2 - 11
pages-sell/components/hot-recommend-item/index.vue

@@ -30,19 +30,14 @@
 			</view>
 		</view>
 
-		<!-- Product Selection Popup -->
-		<SelectGoodPopup ref="popup" @confirm="onPopupConfirm"></SelectGoodPopup>
 	</view>
 </template>
 
 <script>
-import SelectGoodPopup from '../select-good-popup/index.vue';
+import { eventBus } from '@/utils/event-bus'
 
 export default {
 	name: 'HotRecommendItem',
-	components: {
-		SelectGoodPopup
-	},
 	props: {
 		item: {
 			type: Object,
@@ -82,12 +77,8 @@ export default {
 			});
 		},
 		handleAddToCart() {
-			// 加入购物车时,传递 sourceFrom 参数为 1
-			this.$refs.popup.open(this.item, 1);
+			eventBus.emit('openSelectGoodPopup', { product: this.item, sourceFrom: 1 });
 		},
-		onPopupConfirm(data) {
-			this.$emit('add-cart', data);
-		}
 	}
 }
 </script>

+ 6 - 11
pages-sell/components/hot-sell-item/index.vue

@@ -38,19 +38,14 @@
 			</view>
 		</view>
 
-		<!-- Product Selection Popup -->
-		<SelectGoodPopup ref="popup" @confirm="onPopupConfirm"></SelectGoodPopup>
 	</view>
 </template>
 
 <script>
-import SelectGoodPopup from '../select-good-popup/index.vue';
+import { eventBus } from '@/utils/event-bus'
 
 export default {
 	name: 'HotSellItem',
-	components: {
-		SelectGoodPopup
-	},
 	props: {
 		rank: {
 			type: Number,
@@ -91,12 +86,12 @@ export default {
 			});
 		},
 		handleAddToCart() {
-			this.item.isbn = this.item.bookIsbn || '';
-			this.$refs.popup.open(this.item, 1);
+			const product = {
+				...this.item,
+				isbn: this.item.bookIsbn || this.item.isbn || '',
+			};
+			eventBus.emit('openSelectGoodPopup', { product, sourceFrom: 1 });
 		},
-		onPopupConfirm(data) {
-			this.$emit('add-cart', data);
-		}
 	}
 }
 </script>

+ 5 - 13
pages-sell/components/recommend-item/index.vue

@@ -43,19 +43,14 @@
 			<rich-text class="desc-content" :nodes="highlightedDesc"></rich-text>
 		</view>
 
-		<!-- Product Selection Popup -->
-		<SelectGoodPopup ref="popup" @confirm="onPopupConfirm"></SelectGoodPopup>
 	</view>
 </template>
 
 <script>
-	import SelectGoodPopup from '../select-good-popup/index.vue';
+	import { eventBus } from '@/utils/event-bus'
 
 	export default {
 		name: 'RecommendItem',
-		components: {
-			SelectGoodPopup
-		},
 		props: {
 			item: {
 				type: Object,
@@ -108,13 +103,10 @@
 				});
 			},
 			handleAddToCart() {
-				// Open the popup using ref
-				// 加入购物车时,传递 sourceFrom 参数为 2
-				this.$refs.popup.open(this.item, this.showDesc ? 2 : 1);
-			},
-			onPopupConfirm(data) {
-				// 不再触发事件,避免重复调用接口
-				// this.$emit('add-cart', data);
+				eventBus.emit('openSelectGoodPopup', {
+					product: this.item,
+					sourceFrom: this.showDesc ? 2 : 1,
+				});
 			},
 			//图书详情页
 			handleClick() {

+ 4 - 1
pages-sell/pages/hot-sell.vue

@@ -33,6 +33,7 @@
 				></hot-sell-item>
 			</page-scroll>
 		</view>
+		<select-good-popup-host />
 	</view>
 </template>
 
@@ -40,12 +41,14 @@
 import HotSellItem from '../components/hot-sell-item/index.vue'
 import Navbar from '@/components/navbar/navbar.vue'
 import pageScroll from '@/components/pageScroll/index.vue'
+import SelectGoodPopupHost from '@/components/select-good-popup-host.vue'
 
 export default {
 	components: {
 		HotSellItem,
 		Navbar,
-		pageScroll
+		pageScroll,
+		SelectGoodPopupHost,
 	},
 	data() {
 		return {

+ 4 - 1
pages-sell/pages/recommend.vue

@@ -26,17 +26,20 @@
 			<recommend-item v-for="(item, index) in bookList" :key="index" :item="item" :show-desc="cateType != 1" @update-item="updateBookItem($event, index)"></recommend-item>
 			<u-loadmore :status="loadStatus" margin-top="30" margin-bottom="30"></u-loadmore>
 		</view>
+		<select-good-popup-host />
 	</view>
 </template>
 
 <script>
 import RecommendItem from '../components/recommend-item/index.vue'
 import Navbar from '@/components/navbar/navbar.vue';
+import SelectGoodPopupHost from '@/components/select-good-popup-host.vue';
 
 export default {
 	components: {
 		RecommendItem,
-		Navbar
+		Navbar,
+		SelectGoodPopupHost,
 	},
 	data() {
 		return {

+ 4 - 1
pages-sell/pages/search-result.vue

@@ -92,6 +92,7 @@
 
         <!-- Surprise Packet Dialog -->
         <PacketDialog ref="packetDialog"></PacketDialog>
+        <select-good-popup-host />
     </view>
 </template>
 
@@ -100,13 +101,15 @@ import RecommendItem from '../components/recommend-item/index.vue';
 import HotRecommendItem from '../components/hot-recommend-item/index.vue';
 import Navbar from '@/components/navbar/navbar.vue';
 import PacketDialog from '@/pages-sell/components/packet-dialog/index.vue';
+import SelectGoodPopupHost from '@/components/select-good-popup-host.vue';
 
 export default {
     components: {
         RecommendItem,
         HotRecommendItem,
         Navbar,
-        PacketDialog
+        PacketDialog,
+        SelectGoodPopupHost,
     },
     data() {
         return {

+ 4 - 1
pages-sell/pages/topic.vue

@@ -28,17 +28,20 @@
 				</view>
 			</view>
 		</view>
+		<select-good-popup-host />
 	</view>
 </template>
 
 <script>
 	import RecommendItem from '../components/recommend-item/index.vue'
 	import Navbar from '@/components/navbar/navbar.vue'
+	import SelectGoodPopupHost from '@/components/select-good-popup-host.vue'
 
 	export default {
 		components: {
 			RecommendItem,
-			Navbar
+			Navbar,
+			SelectGoodPopupHost,
 		},
 		data() {
 			return {

+ 11 - 3
pages/cart/index.vue

@@ -20,6 +20,17 @@ import { eventBus } from '@/utils/event-bus.js'
 
             }
         },
+        created() {
+            this.cartLoadDoneHandler = () => {
+                uni.stopPullDownRefresh();
+            };
+            eventBus.on('cartPageLoadDone', this.cartLoadDoneHandler);
+        },
+        onUnload() {
+            if (this.cartLoadDoneHandler) {
+                eventBus.off('cartPageLoadDone', this.cartLoadDoneHandler);
+            }
+        },
         onShow() {
             // 每次显示页面时更新角标;通过事件通知子组件刷新,避免访问 $refs 触发小程序占位组件报错
             this.$updateCartBadge();
@@ -28,9 +39,6 @@ import { eventBus } from '@/utils/event-bus.js'
         onPullDownRefresh() {
             this.$updateCartBadge();
             eventBus.emit('cartPageRefresh');
-            setTimeout(() => {
-                uni.stopPullDownRefresh();
-            }, 1000);
         },
         // 分享配置
         onShareAppMessage(res) {

+ 2 - 5
pages/home/index.vue

@@ -361,14 +361,11 @@ export default {
                                 uni.showToast({
                                     icon: "none",
                                     title: res.msg,
+                                    duration: 3000,
                                 });
                             }
-                        } else {
-                            uni.showToast({
-                                icon: "none",
-                                title: res.msg,
-                            });
                         }
+                        // res.code !== 200 已由请求拦截器 toast,无需重复弹出
                     });
             },
             // 加价

BIN
static/img/kf.png


+ 43 - 31
uview-ui/libs/request/index.js

@@ -28,49 +28,59 @@ class Request {
 
 		return new Promise((resolve, reject) => {
 			options.complete = (response) => {
-				// 请求返回后,隐藏loading(如果请求返回快的话,可能会没有loading)
-				uni.hideLoading();
 				// 清除定时器,如果请求回来了,就无需loading
 				clearTimeout(this.config.timer);
 				this.config.timer = null;
-				// 判断用户对拦截返回数据的要求,如果originalData为true,返回所有的数据(response)到拦截器,否则只返回response.data
-				if(this.config.originalData) {
-					// 判断是否存在拦截器
-					if (this.interceptor.response && typeof this.interceptor.response === 'function') {
-						let resInterceptors = this.interceptor.response(response);
-						// 如果拦截器不返回false,就将拦截器返回的内容给this.$u.post的then回调
-						if (resInterceptors !== false) {
-							resolve(resInterceptors);
-						} else {
-							// 如果拦截器返回false,意味着拦截器定义者认为返回有问题,直接接入catch回调
-							reject(response);
-						}
-					} else {
-						// 如果要求返回原始数据,就算没有拦截器,也返回最原始的数据
-						resolve(response);
-					}
-				} else {
-					if (response.statusCode == 200) {
+				// 微信小程序 hideLoading 与 showToast 共用原生层:
+				// 请求结束后立刻 hideLoading,紧接着弹 toast 会被一起关掉(一闪而过)。
+				// 仅在真正展示过 loading 时才关闭,并错开一拍再处理响应。
+				const handleComplete = () => {
+					// 判断用户对拦截返回数据的要求,如果originalData为true,返回所有的数据(response)到拦截器,否则只返回response.data
+					if(this.config.originalData) {
+						// 判断是否存在拦截器
 						if (this.interceptor.response && typeof this.interceptor.response === 'function') {
-							let resInterceptors = this.interceptor.response(response.data);
+							let resInterceptors = this.interceptor.response(response);
+							// 如果拦截器不返回false,就将拦截器返回的内容给this.$u.post的then回调
 							if (resInterceptors !== false) {
 								resolve(resInterceptors);
 							} else {
-								reject(response.data);
+								// 如果拦截器返回false,意味着拦截器定义者认为返回有问题,直接接入catch回调
+								reject(response);
 							}
 						} else {
-							// 如果不是返回原始数据(originalData=false),且没有拦截器的情况下,返回纯数据给then回调
-							resolve(response.data);
+							// 如果要求返回原始数据,就算没有拦截器,也返回最原始的数据
+							resolve(response);
 						}
 					} else {
-						// 不返回原始数据的情况下,服务器状态码不为200,modal弹框提示
-						// if(response.errMsg) {
-						// 	uni.showModal({
-						// 		title: response.errMsg
-						// 	});
-						// }
-						reject(response)
+						if (response.statusCode == 200) {
+							if (this.interceptor.response && typeof this.interceptor.response === 'function') {
+								let resInterceptors = this.interceptor.response(response.data);
+								if (resInterceptors !== false) {
+									resolve(resInterceptors);
+								} else {
+									reject(response.data);
+								}
+							} else {
+								// 如果不是返回原始数据(originalData=false),且没有拦截器的情况下,返回纯数据给then回调
+								resolve(response.data);
+							}
+						} else {
+							// 不返回原始数据的情况下,服务器状态码不为200,modal弹框提示
+							// if(response.errMsg) {
+							// 	uni.showModal({
+							// 		title: response.errMsg
+							// 	});
+							// }
+							reject(response)
+						}
 					}
+				};
+				if (this.config.loadingShow) {
+					this.config.loadingShow = false;
+					uni.hideLoading();
+					setTimeout(handleComplete, 100);
+				} else {
+					handleComplete();
 				}
 			}
 
@@ -83,6 +93,7 @@ class Request {
 			// 而没有清除前者的定时器,导致前者超时,一直显示loading
 			if(this.config.showLoading && !this.config.timer) {
 				this.config.timer = setTimeout(() => {
+					this.config.loadingShow = true;
 					uni.showLoading({
 						title: this.config.loadingText,
 						mask: this.config.loadingMask
@@ -113,6 +124,7 @@ class Request {
 			loadingText: '请求中...',
 			loadingTime: 800, // 在此时间内,请求还没回来的话,就显示加载中动画,单位ms
 			timer: null, // 定时器
+			loadingShow: false, // 是否已真正弹出过 loading(用于避免 hideLoading 误关 toast)
 			originalData: false, // 是否在拦截器中返回服务端的原始数据,见文档说明
 			loadingMask: true, // 展示loading的时候,是否给一个透明的蒙层,防止触摸穿透
 		}