Selaa lähdekoodia

feat: 添加页面滚动同步功能并优化购物车逻辑

fix(api): 移除响应拦截器中的调试日志
fix(popup): 阻止弹层点击事件冒泡
fix(cart): 优化购物车角标更新逻辑并添加Promise支持
feat(search): 添加扫码搜索功能
feat(detail): 优化海报生成添加折扣标签
style(info-card): 调整余额价样式和显示逻辑
refactor(cart): 移除清空购物车按钮并优化选中状态缓存
feat(confirm-order): 添加优惠详情展示区块
ylong 2 viikkoa sitten
vanhempi
sitoutus
de5829d98a

+ 18 - 14
.hbuilderx/launch.json

@@ -1,16 +1,20 @@
-{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
-  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
-    "version": "0.0",
-    "configurations": [{
-     	"default" : 
-     	{
-     		"launchtype" : "local"
-     	},
-     	"mp-weixin" : 
-     	{
-     		"launchtype" : "local"
-     	},
-     	"type" : "uniCloud"
-     }
+{
+    // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+    // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version" : "0.0",
+    "configurations" : [
+        {
+            "default" : {
+                "launchtype" : "local"
+            },
+            "mp-weixin" : {
+                "launchtype" : "local"
+            },
+            "type" : "uniCloud"
+        },
+        {
+            "playground" : "standard",
+            "type" : "uni-app:app-android"
+        }
     ]
 }

+ 0 - 1
api/config.js

@@ -53,7 +53,6 @@ export const httpRequest = (config) => {
 
 // 此处配置响应拦截器
 export const httpResponse = (res) => {
-  console.log(res, "res");
   return new Promise(function (resolve, reject) {
     if (res.statusCode == 200) {
       if (res.data.code !== 200 && res.data.code !== 1001 && res.data.code !== 1002) {

+ 2 - 1
components/custom-popup.vue

@@ -9,7 +9,7 @@
 				zIndex: zIndex
 			}"
 			:class="{'custom-popup__mask--show': showPopup}"
-			@tap="maskClick"
+			@tap.stop="maskClick"
 			@touchmove.stop.prevent="clear"
 		></view>
 		<!-- 弹出层 -->
@@ -23,6 +23,7 @@
 				zIndex: zIndex + 1,
 				backgroundColor: bgColor
 			}"
+			@tap.stop
 			@touchmove.stop.prevent="clear"
 		>
 			<!-- 顶部安全区适配 -->

+ 2 - 2
pages-car/components/buy-order-item.vue

@@ -84,8 +84,8 @@
                 @click.stop="handleAction('cancel', order)">取消订单</u-button>
 
             <!-- 钱款去向 -->
-            <u-button v-if="order.showMoneyDestination == 1" size="mini" shape="circle" type="primary" plain
-                :custom-style="btnStyle" @click.stop="handleAction('moneyWhere', order)">钱款去向</u-button>
+            <!-- <u-button v-if="order.showMoneyDestination == 1" size="mini" shape="circle" type="primary" plain
+                :custom-style="btnStyle" @click.stop="handleAction('moneyWhere', order)">钱款去向</u-button> -->
 
             <!-- 查看详情 -->
             <u-button v-if="order.showShowDetail == 1" size="mini" shape="circle" plain :custom-style="btnStyle"

+ 2 - 2
pages-car/components/order-bottom-bar.vue

@@ -37,8 +37,8 @@
 			@click="emitAction('confirm')">确认收货</u-button>
 
 		<!-- 钱款去向 -->
-		<u-button v-if="orderInfo.showMoneyDestination" size="mini" shape="circle" :custom-style="themeBtnStyle"
-			@click="emitAction('moneyWhere')">钱款去向</u-button>
+		<!-- <u-button v-if="orderInfo.showMoneyDestination" size="mini" shape="circle" :custom-style="themeBtnStyle"
+			@click="emitAction('moneyWhere')">钱款去向</u-button> -->
 	</view>
 </template>
 

+ 19 - 0
pages-car/pages/confirm-order.vue

@@ -51,6 +51,25 @@
             </u-cell-group>
         </view>
 
+        <!-- 优惠详情区块 -->
+        <view class="section-card mt-20" style="padding: 0; overflow: hidden;" v-if="(preOrder.discountList && preOrder.discountList.length > 0) || preOrder.totalReduceMoney > 0">
+            <u-cell-group :border="false">
+                <u-cell-item title="优惠详情" :arrow="false" :border-bottom="true"
+                    :title-style="{ color: '#333', fontWeight: 'bold' }"></u-cell-item>
+                <template v-if="preOrder.discountList && preOrder.discountList.length > 0">
+                    <u-cell-item v-for="(item, index) in preOrder.discountList" :key="'discount-'+index"
+                        :title="item.discountActivityMsg" :value="'-¥' + item.discountMoney"
+                        :arrow="false" :border-bottom="true"
+                        :title-style="{ color: '#666' }" :value-style="{ color: '#ff4500', fontWeight: 'bold' }"></u-cell-item>
+                </template>
+                <template v-if="preOrder.totalReduceMoney > 0">
+                    <u-cell-item title="分享降价" :value="'-¥' + preOrder.totalReduceMoney"
+                        :arrow="false" :border-bottom="false"
+                        :title-style="{ color: '#666' }" :value-style="{ color: '#ff4500', fontWeight: 'bold' }"></u-cell-item>
+                </template>
+            </u-cell-group>
+        </view>
+
         <!-- 底部栏 -->
         <view class="bottom-bar">
             <view class="order-summary">

+ 12 - 4
pages-car/pages/index.vue

@@ -36,9 +36,9 @@
         <view style="height: 120rpx;"></view>
 
         <!-- 悬浮清空按钮 -->
-        <view class="floating-clear-btn" v-if="cartList.length > 0" @click="handleClearCart">
+        <!-- <view class="floating-clear-btn" v-if="cartList.length > 0" @click="handleClearCart">
             <u-icon name="trash" size="36" color="#fa3534"></u-icon>
-        </view>
+        </view> -->
 
         <!-- 底部结算栏 -->
         <view class="bottom-fixed" v-if="cartList.length > 0">
@@ -235,6 +235,10 @@
                 const item = this.cartList.find(i => i.id === id);
                 if (item) {
                     item.checked = checked;
+                    
+                    // 更新本地缓存
+                    const finalCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
+                    uni.setStorageSync('cartCheckedIds', finalCheckedIds);
                 }
             },
             handleChangeNum({ id, num }) {
@@ -274,6 +278,10 @@
                         item.checked = checked;
                     }
                 });
+                
+                // 保存选中状态到本地缓存
+                const finalCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
+                uni.setStorageSync('cartCheckedIds', finalCheckedIds);
             },
             toggleSelectAll() {
                 // 由 u-checkbox 处理
@@ -354,14 +362,14 @@
                     content: '确定要删除该商品吗?',
                     success: (res) => {
                         if (res.confirm) {
-                            this.$u.api.deleteCartItemAjax(item.id).then(() => {
+                            this.$u.api.deleteCartItemAjax(item.id).then(async () => {
+                                await this.$updateCartBadge();
                                 this.$u.toast('删除成功');
                                 // 从本地列表中移除
                                 const index = this.cartList.findIndex(i => i.id === item.id);
                                 if (index > -1) {
                                     this.cartList.splice(index, 1);
                                 }
-                                this.$updateCartBadge();
                             });
                         } else {
                             item.show = false;

+ 0 - 1
pages-mine/pages/withdraw.vue

@@ -147,7 +147,6 @@ export default {
             uni.$u.http.post('/token/user/withdrawApply', {
                 money: this.withdrawAmount
             }).then(res => {
-                console.log(res, "res");
                 if (res.code === 200) {
                     uni.showToast({ title: '提现申请成功', icon: 'success' })
                     setTimeout(() => {

+ 29 - 18
pages-sell/components/detail/info-card.vue

@@ -3,20 +3,20 @@
 		<view class="title-row">
 			<text class="book-title">{{ product.bookName }}</text>
 		</view>
-		
+
 		<view class="price-row">
 			<text class="currency">¥</text>
 			<text class="price">{{ product.sellPrice || product.price }}</text>
-			<view class="balance-tag" v-if="product.balanceMoney">
-				<text>余额价 {{ product.balanceMoney }}</text>
+			<view class="balance-tag" v-if="displayBalanceMoney">
+				<text>余额价 ¥{{ displayBalanceMoney }}</text>
 			</view>
 		</view>
-		
+
 		<view class="meta-row">
 			<text class="label">原价:</text>
 			<text class="original-price">¥{{ product.price }}</text>
 		</view>
-		
+
 		<view class="author-row">
 			<text class="author">作者:{{ product.author }}</text>
 			<view class="works-link" @click="goToAuthorWorks">
@@ -36,6 +36,16 @@ export default {
 			default: () => ({})
 		}
 	},
+	computed: {
+		displayBalanceMoney() {
+			if (this.product.skuList && this.product.skuList.length > 0) {
+				// 获取所有sku中的最低余额价
+				const goods = this.product.skuList.find(sku => sku.price == this.product.sellPrice);
+				return goods.balanceMoney;
+			}
+			return null;
+		}
+	},
 	methods: {
 		goToAuthorWorks() {
 			if (!this.product.author) return;
@@ -52,68 +62,69 @@ export default {
 	background: #fff;
 	border-radius: 24rpx 24rpx 0 0;
 	padding: 30rpx;
-	
+
 	.title-row {
 		margin-bottom: 20rpx;
+
 		.book-title {
 			font-size: 40rpx;
 			font-weight: bold;
 			color: #333;
 		}
 	}
-	
+
 	.price-row {
 		display: flex;
 		align-items: center;
 		margin-bottom: 10rpx;
-		
+
 		.currency {
 			font-size: 32rpx;
 			color: #D81A00;
 			font-weight: bold;
 		}
-		
+
 		.price {
 			font-size: 48rpx;
 			color: #D81A00;
 			font-weight: bold;
 			margin-right: 20rpx;
 		}
-		
+
 		.balance-tag {
-			background: #EFA941;
+			background: #38C148;
 			border-radius: 20rpx;
 			padding: 2rpx 16rpx;
-			
+
 			text {
-				font-size: 22rpx;
+				font-size: 24rpx;
 				color: #fff;
 			}
 		}
 	}
-	
+
 	.meta-row {
 		margin-bottom: 20rpx;
 		font-size: 26rpx;
 		color: #999;
-		
+
 		.original-price {
 			text-decoration: line-through;
 			margin-left: 10rpx;
 		}
 	}
-	
+
 	.author-row {
 		display: flex;
 		justify-content: space-between;
 		align-items: center;
 		font-size: 28rpx;
 		color: #F2950A;
-		
+
 		.author {
 			flex: 1;
 		}
-		
+
 		.works-link {
 			display: flex;
 			align-items: center;

+ 26 - 1
pages-sell/components/detail/poster-popup.vue

@@ -210,11 +210,36 @@ export default {
 				const priceWidth = ctx.measureText(price).width;
 				ctx.fillText(price, contentPadding, currentY);
 
+				let nextX = contentPadding + priceWidth + 20 * scale;
+
+				// Discount Tag
+				if (this.product.sellPrice && this.product.price && this.product.price > 0 && this.product.sellPrice < this.product.price) {
+					const discountNum = (this.product.sellPrice / this.product.price * 10);
+					const discountText = '二手'+(Number.isInteger(discountNum) ? discountNum : discountNum.toFixed(1)) + '折';
+					
+					const tagFontSize = 22 * scale;
+					ctx.setFontSize(tagFontSize);
+					const textWidth = ctx.measureText(discountText).width;
+					const tagPadX = 10 * scale;
+					const tagPadY = 6 * scale;
+					const tagW = textWidth + tagPadX * 2;
+					const tagH = tagFontSize + tagPadY * 2;
+					
+					const tagY = currentY - tagFontSize - tagPadY + 2 * scale; 
+					
+					this.roundRect(ctx, nextX, tagY, tagW, tagH, 8 * scale, '#D81A00');
+					
+					ctx.setFillStyle('#FFFFFF');
+					ctx.fillText(discountText, nextX + tagPadX, currentY - 2 * scale);
+					
+					nextX += tagW + 20 * scale;
+				}
+
 				// Original Price
 				ctx.setFillStyle('#999999');
 				ctx.setFontSize(26 * scale);
 				const origPrice = `原价: ¥ ${this.product.price || '0.00'}`;
-				ctx.fillText(origPrice, contentPadding + priceWidth + 20 * scale, currentY);
+				ctx.fillText(origPrice, nextX, currentY);
 
 				// Divider
 				const footerHeight = 180 * scale;

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

@@ -1,5 +1,5 @@
 <template>
-	<view class="hot-item">
+	<view class="hot-item" @click="navigateToDetail">
 		<image :src="item.cover" class="book-cover" mode="aspectFill"></image>
 		<view class="info-right">
 			<view class="info-top">
@@ -43,6 +43,11 @@ export default {
 		}
 	},
 	methods: {
+		navigateToDetail() {
+			uni.navigateTo({
+				url: '/pages-sell/pages/detail?isbn=' + this.item.isbn
+			});
+		},
 		handleAddToCart() {
 			// 加入购物车时,传递 sourceFrom 参数为 1
 			this.$refs.popup.open(this.item, 1);

+ 7 - 7
pages-sell/components/select-good-popup/index.vue

@@ -1,6 +1,6 @@
 <template>
-	<view>
-		<u-popup v-model="visible" mode="bottom" border-radius="24" :safe-area-inset-bottom="true" :mask-close-able="false"
+	<view @click.stop>
+		<u-popup v-model="visible" mode="bottom" border-radius="24" :safe-area-inset-bottom="true" :mask-close-able="true"
 			@close="close">
 			<view class="popup-content" @click.stop>
 			<!-- Header -->
@@ -36,7 +36,7 @@
 
 			<!-- Promo Note -->
 			<view class="promo-note">
-				<text>下单时用购余额支付可享余额价 ( 售价 8 折优惠 )</text>
+				<text>下单时用购余额支付可享余额价 ( 售价 8 折优惠 )</text>
 			</view>
 
 			<!-- Options -->
@@ -217,10 +217,10 @@ export default {
 				quantity: this.quantity,
 				conditionType: conditionType,
 				sourceFrom: this.sourceFrom
-			}).then(res => {
+			}).then(async res => {
 				if (res.code === 200) {
+					await this.$updateCartBadge();
 					this.$u.toast('加入购物车成功');
-					this.$updateCartBadge();
 					this.$emit('confirm', {
 						product: this.currentProduct,
 						quality: this.currentQuality,
@@ -257,10 +257,10 @@ export default {
 				quantity: this.quantity,
 				conditionType: conditionType,
 				sourceFrom: this.sourceFrom
-			}).then(res => {
+			}).then(async res => {
 				if (res.code === 200) {
+					await this.$updateCartBadge();
 					this.$u.toast('加入购物车成功');
-					this.$updateCartBadge();
 					this.$emit('confirm', {
 						product: this.currentProduct,
 						quality: this.currentQuality,

+ 45 - 10
pages-sell/components/sell-container/index.vue

@@ -13,19 +13,28 @@
 		</view>
 
 		<!-- 主要内容区域 -->
-		<view class="main-content" :style="{ marginTop: (statusBarHeight + 44) + 'px' }">
-			<!-- 搜索框 -->
-			<view class="search-wrapper" @click="navigateTo('/pages-sell/pages/search')">
-				<view class="search-box-uview">
-					<u-search placeholder="搜索关键字" :show-action="false" bg-color="transparent" height="40"
-						:clearabled="true" v-model="keyword" :disabled="true"
-						search-icon="/pages-sell/static/search-icon.png"></u-search>
-					<view class="search-btn-overlay">
-						<text>搜索</text>
+		<view class="main-content" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
+			<!-- 顶部固定区域:导航栏背景和搜索框 -->
+			<view class="fixed-header-area" :style="{ 
+				paddingTop: statusBarHeight + 44 + 'px',
+				backgroundImage: scrollTop > 0 ? 'url(/pages-sell/static/top-bg.png)' : 'none',
+				backgroundPosition: `center top`
+			}">
+				<view class="search-wrapper" @click="navigateTo('/pages-sell/pages/search')">
+					<view class="search-box-uview">
+						<u-search placeholder="搜索关键字" :show-action="false" bg-color="transparent" height="40"
+							:clearabled="true" v-model="keyword" :disabled="true"
+							search-icon="/pages-sell/static/search-icon.png"></u-search>
+						<view class="search-btn-overlay">
+							<text>搜索</text>
+						</view>
 					</view>
 				</view>
 			</view>
 
+			<!-- 占位,防止内容被 fixed 区域遮挡 -->
+			<view class="search-placeholder"></view>
+
 			<!-- tab标签 -->
 			<!-- <view class="tabs-wrapper">
 				<u-tabs :list="hotTagList" :current="currentHotTag" @change="changeHotTag" bgColor="transparent"
@@ -183,6 +192,8 @@ export default {
 			swiperList: [],
 			statusBarHeight: 20,
 			keyword: '',
+			// 记录滚动距离以实现背景错位效果
+			scrollTop: 0,
 			// u-tabs 需要对象数组 [{name: 'xxx'}]
 			hotTagList: [
 				{ name: '热搜' },
@@ -216,7 +227,11 @@ export default {
 		this.getIndexCateInfo()
 	},
 	methods: {
-		//是否有分享列表数据
+		// 接收外部传入的页面滚动事件
+		onPageScroll(scrollTop) {
+			this.scrollTop = scrollTop || 0;
+			console.log(this.scrollTop)
+		},
 		async hasShareList() {
 			const res = await this.$u.api.getShareRedBagListAjax();
 			if (res.code === 200 || res.code === 0) {
@@ -363,6 +378,26 @@ export default {
 	padding: 0 24rpx;
 }
 
+/* 顶部固定区域:背景和搜索框 */
+.fixed-header-area {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	z-index: 99;
+	background-size: 100% auto;
+	background-repeat: no-repeat;
+	padding-left: 24rpx;
+	padding-right: 24rpx;
+	padding-bottom: 16rpx;
+	box-sizing: border-box;
+	transition: background-color 0.2s;
+}
+
+.search-placeholder {
+	height: 100rpx;
+}
+
 /* 搜索框 */
 .search-wrapper {
 	margin-top: 10rpx;

+ 2 - 2
pages-sell/pages/detail.vue

@@ -193,14 +193,14 @@
                     uni.hideLoading();
                 });
             },
-            onPopupConfirm(data) {
+            async onPopupConfirm(data) {
                 console.log('Added to cart:', data);
+                await this.$updateCartBadge();
                 uni.showToast({
                     title: '已加入购物车',
                     icon: 'success',
                     duration: 3000
                 });
-                this.$updateCartBadge();
             },
             handleCollect() {
                 if (!this.product || !this.product.isbn) {

+ 13 - 1
pages-sell/pages/search.vue

@@ -5,7 +5,7 @@
         <!-- Search Bar Area -->
         <view class="search-area">
             <view class="search-box">
-                <image src="/pages-sell/static/search/icon-scan.png" class="search-icon-left" mode="aspectFit"></image>
+                <image src="/pages-sell/static/search/icon-scan.png" class="search-icon-left" mode="aspectFit" @click="onScan"></image>
                 <input class="search-input" v-model="keyword" placeholder="书名 / 作者 / ISBN" confirm-type="search"
                     @confirm="onSearch" :focus="true" />
                 <u-icon name="close-circle-fill" color="#c0c4cc" size="32" v-if="keyword" @click="keyword = ''"
@@ -142,6 +142,18 @@
                 if (!this.keyword.trim()) return;
                 this.doSearch(this.keyword);
             },
+            onScan() {
+                uni.scanCode({
+                    success: (res) => {
+                        if (res.result) {
+                            this.keyword = res.result;
+                        }
+                    },
+                    fail: (err) => {
+                        console.log('扫码失败', err);
+                    }
+                });
+            },
             doSearch(key) {
                 if (!key) return;
                 // 去除可能包含的 HTML 标签 (如后端返回的 <em>)

+ 2 - 2
pages-sell/pages/topic.vue

@@ -110,14 +110,14 @@
 				this.$u.api.addShopCartAjax({
 					bookId: data.id,
 					num: 1
-				}).then(res => {
+				}).then(async res => {
 					if (res.code == 200) {
+                        await this.$updateCartBadge();
 						uni.showToast({
 							title: '已加入购物车',
 							icon: 'success',
 							duration: 3000
 						});
-                        this.$updateCartBadge();
 					}
 				});
 			}

+ 5 - 0
pages/sell/index.vue

@@ -26,6 +26,11 @@ export default {
 			uni.stopPullDownRefresh();
 		}, 1000);
 	},
+	onPageScroll(e) {
+		const comp = this.$refs.sellContainer;
+		if (!comp) return;
+		comp.scrollTop = e.scrollTop;
+	},
 	methods: {
 		
 	}

+ 35 - 26
utils/uniapp-api.js

@@ -23,32 +23,41 @@ export const copyByUniappApi = (data, msg = '已复制到剪贴板') => {
 
 // 全局更新购物车角标
 export const updateCartBadge = () => {
-    // 检查是否登录,未登录不请求
-    const token = uni.getStorageSync('token');
-    if (!token) {
-        uni.removeTabBarBadge({ index: 2 });
-        return;
-    }
-    
-    // 确保 uView http 可用
-    if (uni.$u && uni.$u.http) {
-        uni.$u.http.post('/token/shop/cart/getCount').then(res => {
-            if (res.code == 200) {
-                const count = res.data;
-                uni.$emit('cartCountChanged', count);
-                if (count > 0) {
-                    uni.setTabBarBadge({
-                        index: 2,
-                        text: String(count)
-                    });
+    return new Promise((resolve) => {
+        // 检查是否登录,未登录不请求
+        const token = uni.getStorageSync('token');
+        if (!token) {
+            uni.removeTabBarBadge({ index: 2 });
+            resolve(0);
+            return;
+        }
+        
+        // 确保 uView http 可用
+        if (uni.$u && uni.$u.http) {
+            uni.$u.http.post('/token/shop/cart/getCount').then(res => {
+                if (res.code == 200) {
+                    const count = res.data;
+                    uni.$emit('cartCountChanged', count);
+                    if (count > 0) {
+                        uni.setTabBarBadge({
+                            index: 2,
+                            text: String(count)
+                        });
+                    } else {
+                        uni.removeTabBarBadge({
+                            index: 2
+                        });
+                    }
+                    resolve(count);
                 } else {
-                    uni.removeTabBarBadge({
-                        index: 2
-                    });
+                    resolve(0);
                 }
-            }
-        }).catch(e => {
-            console.log('更新购物车角标失败', e);
-        });
-    }
+            }).catch(e => {
+                console.log('更新购物车角标失败', e);
+                resolve(0);
+            });
+        } else {
+            resolve(0);
+        }
+    });
 }