Parcourir la source

feat: 优化海报生成逻辑并修复样式问题

refactor(condition-popup): 调整背景色为白色
fix(apply-refund): 修正商品封面和信息的尺寸
fix(recommend-item): 移除重复调用接口的事件触发
fix(sell-container): 添加点击事件阻止冒泡
feat(mine): 添加红包页面跳转功能
fix(urge-delivery-dialog): 兼容支付宝平台的联系按钮
refactor(poster-popup): 优化海报生成逻辑和错误处理
feat(select-good-popup): 添加防重复提交和分享功能
ylong il y a 3 semaines
Parent
commit
d951a86bcd

+ 1 - 1
pages-car/components/condition-popup.vue

@@ -115,7 +115,7 @@
 				display: flex;
 				justify-content: space-between;
 				align-items: center;
-				background: #F8F8F8;
+				background: #ffffff;
 				border-radius: 12rpx;
 				padding: 24rpx 30rpx;
 				margin-bottom: 24rpx;

+ 126 - 120
pages-car/components/urge-delivery-dialog.vue

@@ -13,9 +13,15 @@
             </view>
 
             <view class="btn-group">
+                <!-- #ifdef MP-ALIPAY -->
                 <button class="action-btn contact-btn" @click="handleContact">
                     继续联系卖家
                 </button>
+                <!-- #endif -->
+                <!-- #ifndef MP-ALIPAY -->
+                <button class="action-btn contact-btn" open-type="contact">继续联系卖家
+                </button>
+                <!-- #endif -->
                 <button class="action-btn confirm-btn" @click="closePopup">
                     我知道了
                 </button>
@@ -25,142 +31,142 @@
 </template>
 
 <script>
-    import CommonDialog from '@/components/common-dialog.vue';
-    export default {
-        components: {
-            CommonDialog
+import CommonDialog from '@/components/common-dialog.vue';
+export default {
+    components: {
+        CommonDialog
+    },
+    data() {
+        return {
+            order: null,
+            deadlineTime: ''
+        };
+    },
+    methods: {
+        open(order) {
+            this.order = order;
+
+            // 调用催发货接口
+            uni.showLoading({ title: '处理中' });
+            this.$u.api.urgeSendAjax(order.orderId).then(res => {
+                uni.hideLoading();
+                if (res.code === 200) {
+                    // 计算发货截止时间
+                    let baseTime = new Date();
+                    if (order && order.createTime) {
+                        // 兼容处理 ios 时间格式
+                        baseTime = new Date(order.createTime.replace(/-/g, '/'));
+                    }
+                    // 加72小时
+                    const deadline = new Date(baseTime.getTime() + 72 * 60 * 60 * 1000);
+                    this.deadlineTime = this.formatDate(deadline);
+
+                    this.$refs.urgeDialog.openPopup();
+                    this.$emit('success');
+                }
+            }).catch(() => {
+                uni.hideLoading();
+            });
         },
-        data() {
-            return {
-                order: null,
-                deadlineTime: ''
-            };
+        closePopup() {
+            this.$refs.urgeDialog.closePopup();
         },
-        methods: {
-            open(order) {
-                this.order = order;
-
-                // 调用催发货接口
-                uni.showLoading({ title: '处理中' });
-                this.$u.api.urgeSendAjax(order.orderId).then(res => {
-                    uni.hideLoading();
-                    if (res.code === 200) {
-                        // 计算发货截止时间
-                        let baseTime = new Date();
-                        if (order && order.createTime) {
-                            // 兼容处理 ios 时间格式
-                            baseTime = new Date(order.createTime.replace(/-/g, '/'));
-                        }
-                        // 加72小时
-                        const deadline = new Date(baseTime.getTime() + 72 * 60 * 60 * 1000);
-                        this.deadlineTime = this.formatDate(deadline);
-
-                        this.$refs.urgeDialog.openPopup();
-                        this.$emit('success');
-                    }
-                }).catch(() => {
-                    uni.hideLoading();
-                });
-            },
-            closePopup() {
-                this.$refs.urgeDialog.closePopup();
-            },
-            handleContact() {
-                this.closePopup();
-                uni.navigateTo({
-                    url: '/pages-mine/pages/customer-service'
-                });
-            },
-            formatDate(date) {
-                const month = (date.getMonth() + 1).toString().padStart(2, '0');
-                const day = date.getDate().toString().padStart(2, '0');
-                const hours = date.getHours().toString().padStart(2, '0');
-                const minutes = date.getMinutes().toString().padStart(2, '0');
-                return `${month}月${day}日 ${hours}:${minutes}`;
-            }
+        handleContact() {
+            this.closePopup();
+            uni.navigateTo({
+                url: '/pages-mine/pages/customer-service'
+            });
+        },
+        formatDate(date) {
+            const month = (date.getMonth() + 1).toString().padStart(2, '0');
+            const day = date.getDate().toString().padStart(2, '0');
+            const hours = date.getHours().toString().padStart(2, '0');
+            const minutes = date.getMinutes().toString().padStart(2, '0');
+            return `${month}月${day}日 ${hours}:${minutes}`;
         }
     }
+}
 </script>
 
 <style lang="scss" scoped>
-    .urge-dialog-box {
-        padding: 50rpx 40rpx 40rpx;
-        position: relative;
-        background: #fff;
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-
-        .close-btn {
-            position: absolute;
-            top: 20rpx;
-            right: 20rpx;
-            padding: 10rpx;
-            z-index: 10;
-        }
+.urge-dialog-box {
+    padding: 50rpx 40rpx 40rpx;
+    position: relative;
+    background: #fff;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .close-btn {
+        position: absolute;
+        top: 20rpx;
+        right: 20rpx;
+        padding: 10rpx;
+        z-index: 10;
+    }
 
-        .icon-area {
-            margin-bottom: 30rpx;
-        }
+    .icon-area {
+        margin-bottom: 30rpx;
+    }
 
-        .urge-title {
-            font-size: 36rpx;
-            font-weight: bold;
-            color: #333;
-            margin-bottom: 20rpx;
-            text-align: center;
-        }
+    .urge-title {
+        font-size: 36rpx;
+        font-weight: bold;
+        color: #333;
+        margin-bottom: 20rpx;
+        text-align: center;
+    }
 
-        .urge-desc {
-            font-size: 26rpx;
-            color: #666;
-            margin-bottom: 50rpx;
-            text-align: center;
-            line-height: 1.5;
-            padding: 0 20rpx;
-
-            .highlight {
-                color: #38C148;
-                margin-left: 10rpx;
-            }
+    .urge-desc {
+        font-size: 26rpx;
+        color: #666;
+        margin-bottom: 50rpx;
+        text-align: center;
+        line-height: 1.5;
+        padding: 0 20rpx;
+
+        .highlight {
+            color: #38C148;
+            margin-left: 10rpx;
         }
+    }
 
-        .btn-group {
-            display: flex;
-            width: 100%;
-            justify-content: space-between;
-
-            .action-btn {
-                flex: 1;
-                height: 80rpx;
-                line-height: 80rpx;
-                border-radius: 40rpx;
-                font-size: 28rpx;
-                font-weight: 500;
-                margin: 0 15rpx;
-
-                &::after {
-                    border: none;
-                }
-
-                &:active {
-                    opacity: 0.9;
-                }
+    .btn-group {
+        display: flex;
+        width: 100%;
+        justify-content: space-between;
+
+        .action-btn {
+            flex: 1;
+            height: 80rpx;
+            line-height: 80rpx;
+            border-radius: 40rpx;
+            font-size: 28rpx;
+            font-weight: 500;
+            margin: 0 15rpx;
+
+            &::after {
+                border: none;
             }
 
-            .contact-btn {
-                background: #fff;
-                color: #38C148;
-                border: 2rpx solid #38C148;
-                flex: 1.5;
-                box-sizing: border-box;
-                line-height: 76rpx; // 调整线高因为有边框
+            &:active {
+                opacity: 0.9;
             }
+        }
 
-            .confirm-btn {
-                background: #38C148;
-                color: #fff;
-            }
+        .contact-btn {
+            background: #fff;
+            color: #38C148;
+            border: 2rpx solid #38C148;
+            flex: 1.5;
+            box-sizing: border-box;
+            line-height: 76rpx; // 调整线高因为有边框
+        }
+
+        .confirm-btn {
+            background: #38C148;
+            color: #fff;
         }
     }
+}
 </style>

+ 2 - 2
pages-car/pages/apply-refund.vue

@@ -685,14 +685,14 @@
 
 			.goods-cover {
 				width: 140rpx;
-				height: 140rpx;
+				height: 150rpx;
 				border-radius: 8rpx;
 				margin-right: 20rpx;
 			}
 
 			.goods-info {
 				flex: 1;
-				height: 140rpx;
+				height: 160rpx;
 				display: flex;
 				flex-direction: column;
 				justify-content: space-between;

BIN
pages-car/static/pay-success.png


+ 153 - 92
pages-sell/components/detail/poster-popup.vue

@@ -58,36 +58,51 @@ export default {
 		async generatePoster() {
 			uni.showLoading({ title: '生成中...' });
 			try {
+				// 确保Canvas元素已渲染
+				await new Promise(resolve => setTimeout(resolve, 100));
+				
 				const ctx = uni.createCanvasContext('posterCanvas', this);
 				this.ctx = ctx;
 				
 				// Setup dimensions
 				const sysInfo = uni.getSystemInfoSync();
-				const dpr = sysInfo.pixelRatio || 2;
-				// Multiply scale by dpr to ensure high-res drawing on the canvas backing store
-				const scale = (sysInfo.windowWidth / 750) * dpr;
+				const baseScale = (sysInfo.windowWidth || 375) / 750;
+				// 清晰度与稳定性折中:按设备像素比绘制,但最多放大到 2 倍
+				const renderRatio = Math.min(Math.max(sysInfo.pixelRatio || 1, 1), 2);
+				const scale = baseScale * renderRatio;
 				
 				// User request: Canvas 340px, Padding 10px.
 				// Mapping to rpx: 340px -> 680rpx, 10px -> 20rpx.
-				const width = Math.floor(680 * scale); 
-				const height = Math.floor(1140 * scale); 
+				const width = Math.floor(680 * scale);
+				const height = Math.floor(1140 * scale);
 				const padding = 20 * scale; 
 				
-				this.canvasWidth = width;
-				this.canvasHeight = height;
+				// 确保Canvas尺寸在合理范围内
+				const maxCanvasSize = 2000; // 保守上限,兼顾清晰度与稳定性
+				if (width > maxCanvasSize || height > maxCanvasSize) {
+					// 缩放Canvas尺寸到合理范围
+					const scaleRatio = Math.min(maxCanvasSize / width, maxCanvasSize / height, 1);
+					const scaledWidth = Math.floor(width * scaleRatio);
+					const scaledHeight = Math.floor(height * scaleRatio);
+					this.canvasWidth = scaledWidth;
+					this.canvasHeight = scaledHeight;
+				} else {
+					this.canvasWidth = width;
+					this.canvasHeight = height;
+				}
 				
 				// 1. Draw Background (Gradient, Rounded)
 				// linear-gradient(0deg, #FBFFFF 0%, #BFFCFE 100%) -> Bottom to Top
-				const grd = ctx.createLinearGradient(0, height, 0, 0);
+				const grd = ctx.createLinearGradient(0, this.canvasHeight, 0, 0);
 				grd.addColorStop(0, '#FBFFFF');
 				grd.addColorStop(1, '#BFFCFE');
 				
-				this.roundRect(ctx, 0, 0, width, height, 36 * scale, grd);
+				this.roundRect(ctx, 0, 0, this.canvasWidth, this.canvasHeight, 36 * scale, grd);
 				
 				// 2. Draw Product Cover
-				const coverPath = await this.getImageInfo(this.product.cover || '/static/img/1.png');
+				const coverPath = await this.getImageInfo(this.product.cover || '/static/img/1.png', '/static/img/1.png');
 				// Image width = Canvas width - 2 * padding
-				const imgWidth = width - (2 * padding);
+				const imgWidth = this.canvasWidth - (2 * padding);
 				const coverHeight = imgWidth; // Square cover
 				const coverX = padding;
 				const coverY = padding;
@@ -97,7 +112,18 @@ export default {
 				// Prototype usually has rounded corners for the cover too.
 				this.roundRectPath(ctx, coverX, coverY, imgWidth, coverHeight, 24 * scale, true, true, true, true);
 				ctx.clip();
-				ctx.drawImage(coverPath, coverX, coverY, imgWidth, coverHeight);
+				try {
+					if (coverPath) {
+						ctx.drawImage(coverPath, coverX, coverY, imgWidth, coverHeight);
+					} else {
+						throw new Error('coverPath is empty');
+					}
+				} catch (e) {
+					console.error('Draw cover image error:', e);
+					// 绘制失败时使用默认颜色块
+					ctx.setFillStyle('#f0f0f0');
+					ctx.fillRect(coverX, coverY, imgWidth, coverHeight);
+				}
 				ctx.restore();
 				
 				// 3. Draw User Info
@@ -116,12 +142,23 @@ export default {
 				
 				// Avatar Image
 				const avatarUrl = this.userInfo.avatar || 'https://img.yzcdn.cn/vant/cat.jpeg';
-				const avatarPath = await this.getImageInfo(avatarUrl);
+				const avatarPath = await this.getImageInfo(avatarUrl, '/static/img/1.png');
 				ctx.save();
 				ctx.beginPath();
 				ctx.arc(avatarX + avatarSize/2, avatarY + avatarSize/2, avatarSize/2, 0, 2 * Math.PI);
 				ctx.clip();
-				ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize);
+				try {
+					if (avatarPath) {
+						ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize);
+					} else {
+						throw new Error('avatarPath is empty');
+					}
+				} catch (e) {
+					console.error('Draw avatar error:', e);
+					// 绘制失败时使用默认颜色块
+					ctx.setFillStyle('#f0f0f0');
+					ctx.fillRect(avatarX, avatarY, avatarSize, avatarSize);
+				}
 				ctx.restore();
 				
 				// User Name & Recomm Text
@@ -131,16 +168,14 @@ export default {
 				
 				ctx.setFillStyle('#1D1D1D');
 				ctx.setFontSize(28 * scale);
-				ctx.font = `bold ${28 * scale}px sans-serif`;
 				ctx.setTextAlign('left');
-				ctx.fillText(this.userInfo.nickname || '微信用户', textLeftX, userInfoY);
+				ctx.fillText(this.userInfo.nickName || '微信用户', textLeftX, userInfoY);
 				
 				ctx.setFillStyle('#666666');
 				ctx.setFontSize(26 * scale);
-				ctx.font = `normal ${26 * scale}px sans-serif`;
 				ctx.setTextAlign('right');
 				// Align to right padding edge
-				ctx.fillText('推荐您一本好书', width - padding, userInfoY);
+				ctx.fillText('推荐您一本好书', this.canvasWidth - padding, userInfoY);
 				ctx.setTextAlign('left'); // Reset
 				
 				// 4. Product Info
@@ -150,17 +185,15 @@ export default {
 				ctx.setFillStyle('#1D1D1D');
 				const titleFontSize = 34 * scale;
 				ctx.setFontSize(titleFontSize);
-				ctx.font = `bold ${titleFontSize}px sans-serif`; 
 				const title = this.product.bookName || '未知书名';
 				// Width constraint: Canvas width - 2*contentPadding
-				const lastTitleY = this.drawText(ctx, title, contentPadding, currentY, width - 2 * contentPadding, titleFontSize * 1.5);
+				const lastTitleY = this.drawText(ctx, title, contentPadding, currentY, this.canvasWidth - 2 * contentPadding, titleFontSize * 1.5);
 				currentY = lastTitleY + 60 * scale;
 				
 				// Author
 				ctx.setFillStyle('#666666');
 				const authorFontSize = 26 * scale;
 				ctx.setFontSize(authorFontSize);
-				ctx.font = `normal ${authorFontSize}px sans-serif`;
 				ctx.fillText(`作者: ${this.product.author || '未知'}`, contentPadding, currentY);
 				currentY += 70 * scale;
 				
@@ -168,42 +201,31 @@ export default {
 				ctx.setFillStyle('#D81A00');
 				const priceFontSize = 44 * scale;
 				ctx.setFontSize(priceFontSize);
-				ctx.font = `bold ${priceFontSize}px sans-serif`;
 				const price = `¥ ${this.product.balanceMoney || '0.00'}`;
 				const priceWidth = ctx.measureText(price).width;
 				ctx.fillText(price, contentPadding, currentY);
 				
-				// Discount Tag
-				// const discount = '5.3折'; 
-				// const tagX = contentPadding + priceWidth + 16 * scale;
-				// const tagY = currentY - 26 * scale;
-				
-				// ctx.setFillStyle('#D81A00');
-				// this.roundRect(ctx, tagX, tagY, 70 * scale, 32 * scale, 4 * scale, '#D81A00');
-				
-				// ctx.setFillStyle('#FFFFFF');
-				// ctx.setFontSize(20 * scale);
-				// ctx.font = `normal ${20 * scale}px sans-serif`;
-				// ctx.fillText(discount, tagX + 8 * scale, tagY + 22 * scale);
-				
 				// Original Price
 				ctx.setFillStyle('#999999');
 				ctx.setFontSize(26 * scale);
-				ctx.font = `normal ${26 * scale}px sans-serif`;
-				const origPrice = `原价: ¥ ${this.product.price || '0.00'}`;
+				const origPrice = `原价: ¥ ${this.product.sellPrice || '0.00'}`;
 				ctx.fillText(origPrice, contentPadding + priceWidth + 20 * scale, currentY);
 				
 				// Divider
 				const footerHeight = 180 * scale;
-				const dividerY = height - footerHeight;
+				const dividerY = this.canvasHeight - footerHeight;
 				
 				ctx.setStrokeStyle('#dddddd');
-				ctx.setLineDash([8, 8]);
+				if (typeof ctx.setLineDash === 'function') {
+					ctx.setLineDash([8, 8]);
+				}
 				ctx.beginPath();
 				ctx.moveTo(padding, dividerY);
-				ctx.lineTo(width - padding, dividerY);
+				ctx.lineTo(this.canvasWidth - padding, dividerY);
 				ctx.stroke();
-				ctx.setLineDash([]);
+				if (typeof ctx.setLineDash === 'function') {
+					ctx.setLineDash([]);
+				}
 				
 				// 5. Footer (Store & QR)
 				const footerContentY = dividerY + 50 * scale;
@@ -211,7 +233,6 @@ export default {
 				// Store Name
 				ctx.setFillStyle('#1D1D1D');
 				ctx.setFontSize(32 * scale);
-				ctx.font = `bold ${32 * scale}px YouSheBiaoTiHei`;
 				const storeName = '书嗨循环书店';
 				const storeNameWidth = ctx.measureText(storeName).width;
 				ctx.fillText(storeName, padding, footerContentY + 10 * scale);
@@ -222,77 +243,117 @@ export default {
 				// Align vertically with text (text baseline is at footerContentY + 10*scale)
 				// Center icon relative to text cap height
 				const iconY = footerContentY + 10 * scale - (30 * scale) / 2 - iconSize / 2 + 40;
-				const iconPath = await this.getImageInfo('/pages-sell/static/goods/icon-shuhai.png');
-				ctx.drawImage(iconPath, iconX, iconY, iconSize, iconSize);
+				try {
+					const iconPath = await this.getImageInfo('/pages-sell/static/goods/icon-shuhai.png', '/pages-sell/static/goods/icon-shuhai.png');
+					if (iconPath) {
+						ctx.drawImage(iconPath, iconX, iconY, iconSize, iconSize);
+					}
+				} catch (e) {
+					console.error('Draw icon error:', e);
+				}
 
 				ctx.setFillStyle('#666666');
 				ctx.setFontSize(22 * scale);
-				ctx.font = `normal ${22 * scale}px sans-serif`;
 				ctx.fillText('不辜负每一个爱书的人', padding, footerContentY + 54 * scale);
 				
 				// QR Code
 				const qrSize = 130 * scale;
-				const qrX = width - padding - qrSize;
+				const qrX = this.canvasWidth - padding - qrSize;
 				const qrY = footerContentY - 20 * scale;
 				
 				// Use gzh.png as the QR code (Figure 2)
-				const qrPath = await this.getImageInfo('/pages-sell/static/goods/icon-shuhai.png'); 
-				ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize);
+				try {
+					const qrPath = await this.getImageInfo('/pages-sell/static/goods/icon-shuhai.png', '/pages-sell/static/goods/icon-shuhai.png');
+					if (qrPath) {
+						ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize);
+					} else {
+						throw new Error('qrPath is empty');
+					}
+				} catch (e) {
+					console.error('Draw QR code error:', e);
+					// 绘制失败时使用默认颜色块
+					ctx.setFillStyle('#f0f0f0');
+					ctx.fillRect(qrX, qrY, qrSize, qrSize);
+				}
 				
-				ctx.draw(false, () => {
-					setTimeout(() => {
-						uni.canvasToTempFilePath({
-							canvasId: 'posterCanvas',
-							destWidth: width,
-							destHeight: height,
-							fileType: 'png',
-							quality: 1,
-							success: (res) => {
-								this.posterImage = res.tempFilePath;
-								uni.hideLoading();
-							},
-							fail: (err) => {
-								console.error(err);
-								uni.hideLoading();
-								uni.showToast({
-									title: '生成失败',
-									icon: 'none'
-								});
-							}
-						}, this);
-					}, 200);
+				// 使用Promise包装draw方法,确保绘制完成
+				await new Promise((resolve) => {
+					ctx.draw(false, () => {
+						// 延长等待时间,确保绘制完成
+						setTimeout(() => {
+							resolve();
+						}, 500);
+					});
 				});
 				
+				// 转换Canvas为图片(增加重试,兼容部分机型导出时机问题)
+				const tempFilePath = await this.canvasToTempFilePathWithRetry({
+					canvasId: 'posterCanvas',
+					// 直接导出实际绘制尺寸,避免“导出阶段再放大”导致模糊
+					destWidth: this.canvasWidth,
+					destHeight: this.canvasHeight,
+					fileType: 'png',
+					quality: 0.9
+				}, 3);
+				this.posterImage = tempFilePath;
+				uni.hideLoading();
+				
 			} catch (e) {
-				console.error(e);
+				console.error('Generate poster error:', e);
 				uni.hideLoading();
 				uni.showToast({ title: '生成出错', icon: 'none' });
 			}
 		},
-		getImageInfo(url) {
+		canvasToTempFilePathWithRetry(options, retries = 2) {
 			return new Promise((resolve, reject) => {
-				if (!url) {
-                    // Return a 1x1 transparent png base64 or similar if no url, or reject
-                    resolve('/static/logo.png'); // Fallback
-                    return;
-                }
-                // Handle local paths directly
-                if (url.startsWith('/') || url.startsWith('static/')) {
-                    resolve(url);
-                    return;
-                }
-                
-				uni.downloadFile({
-					url: url,
-					success: (res) => {
-						if (res.statusCode === 200) {
-							resolve(res.tempFilePath);
-						} else {
-							reject(res);
+				const attempt = (left) => {
+					uni.canvasToTempFilePath({
+						...options,
+						success: (res) => resolve(res.tempFilePath),
+						fail: (err) => {
+							if (left > 0) {
+								setTimeout(() => attempt(left - 1), 180);
+								return;
+							}
+							reject(err);
 						}
+					}, this);
+				};
+				attempt(retries);
+			});
+		},
+		getImageInfo(url, fallback = '') {
+			return new Promise((resolve) => {
+				const safeFallback = fallback || '/static/img/1.png';
+				const src = typeof url === 'string' ? url : '';
+				if (!src) {
+					resolve(safeFallback);
+					return;
+				}
+				// 本地资源直接返回
+				if (src.startsWith('/') || src.startsWith('static/')) {
+					resolve(src);
+					return;
+				}
+				// 优先使用 getImageInfo,兼容更多端图片解码
+				uni.getImageInfo({
+					src,
+					success: (res) => {
+						resolve(res.path || res.tempFilePath || safeFallback);
 					},
-					fail: (err) => {
-						reject(err);
+					fail: () => {
+						// 降级下载,失败时不抛异常,避免中断整张海报生成
+						uni.downloadFile({
+							url: src,
+							success: (downloadRes) => {
+								if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
+									resolve(downloadRes.tempFilePath);
+									return;
+								}
+								resolve(safeFallback);
+							},
+							fail: () => resolve(safeFallback)
+						});
 					}
 				});
 			});

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

@@ -80,8 +80,8 @@
 				this.$refs.popup.open(this.item, this.showDesc ? 2 : 1);
 			},
 			onPopupConfirm(data) {
-				// Emit the confirmed data
-				this.$emit('add-cart', data);
+				// 不再触发事件,避免重复调用接口
+				// this.$emit('add-cart', data);
 			},
 			//图书详情页
 			handleClick() {

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

@@ -67,10 +67,10 @@
 
 			<!-- Footer Buttons -->
 			<view class="footer-btns">
-				<view class="btn btn-orange">
-					<text class="price">¥0.3</text>
-					<text class="desc">分享一人可降 0.5 元</text>
-				</view>
+				<button class="btn btn-orange" open-type="share">
+					<text class="price">¥{{ currentProduct.maxReducePrice }}</text>
+					<text class="desc">分享一人可降 {{ currentProduct.reducePrice }} 元</text>
+				</button>
 				<view class="btn btn-green" @click="handleAction">
 					<text v-if="hasStock" class="price">¥{{ displayTotalPrice }}</text>
 					<text class="desc" :class="{ 'notice': !hasStock }">{{ hasStock ? '加入购物车' : '到货通知' }}</text>
@@ -81,419 +81,432 @@
 </template>
 
 <script>
-	export default {
-		name: 'SelectGoodPopup',
-		data() {
-			return {
-				visible: false,
-				quantity: 1,
-				currentQuality: 1,
-				currentProduct: {},
-				qualityOptions: [],
-				sourceFrom: 0,
-			};
+export default {
+	name: 'SelectGoodPopup',
+	data() {
+		return {
+			visible: false,
+			quantity: 1,
+			currentQuality: 1,
+			currentProduct: {},
+			qualityOptions: [],
+			sourceFrom: 0,
+			isSubmitting: false, // 防重复提交标志
+		};
+	},
+	computed: {
+		currentQualityName() {
+			const opt = this.$conditionMap[this.currentQuality];
+			return opt || '未知';
+		},
+		selectedOption() {
+			return this.qualityOptions.find(o => o.conditionType === this.currentQuality) || {};
+		},
+		displayUnitPrice() {
+			return this.formatPrice(this.selectedOption.price);
+		},
+		displayMinPrice() {
+			return this.formatPrice(this.selectedOption.balanceMoney);
 		},
-		computed: {
-			currentQualityName() {
-				const opt = this.$conditionMap[this.currentQuality];
-				return opt || '未知';
-			},
-			selectedOption() {
-				return this.qualityOptions.find(o => o.conditionType === this.currentQuality) || {};
-			},
-			displayUnitPrice() {
-				return this.formatPrice(this.selectedOption.price);
-			},
-			displayMinPrice() {
-				return this.formatPrice(this.selectedOption.balanceMoney);
-			},
-			displayTotalPrice() {
-				const total = this.toNumber(this.selectedOption.price) * this.quantity;
-				return this.formatPrice(total);
-			},
-			hasStock() {
-				const stock = this.selectedOption.stockNum;
-				if (stock === 0) return false;
-				if (stock === undefined || stock === null || stock === '') return true;
-				return this.toNumber(stock) > 0;
+		displayTotalPrice() {
+			const total = this.toNumber(this.selectedOption.price) * this.quantity;
+			return this.formatPrice(total);
+		},
+		hasStock() {
+			const stock = this.selectedOption.stockNum;
+			if (stock === 0) return false;
+			if (stock === undefined || stock === null || stock === '') return true;
+			return this.toNumber(stock) > 0;
+		}
+	},
+	methods: {
+		open(product, sourceFrom) {
+			this.visible = true;
+			this.quantity = 1;
+			this.sourceFrom = sourceFrom || 0;
+			if (product.isbn) {
+				this.getGoodQualityInfo(product.isbn);
 			}
 		},
-		methods: {
-			open(product, sourceFrom) {
-				this.visible = true;
-				this.quantity = 1;
-				this.sourceFrom = sourceFrom || 0;
-				if (product.isbn) {
-					this.getGoodQualityInfo(product.isbn);
-				}
-			},
-			//根据 isbn 获取商品品相信息
-			getGoodQualityInfo(isbn) {
-				uni.$u.http.get('/token/shop/bookDetail', { isbn }).then(res => {
-					console.log(res);
-					if (res.code === 200) {
-						this.currentProduct = res.data || {};
-						this.qualityOptions = res.data.skuList || [];
-						if (this.qualityOptions.length > 0) {
-							this.currentQuality = this.qualityOptions[0].conditionType
-						}
+		//根据 isbn 获取商品品相信息
+		getGoodQualityInfo(isbn) {
+			uni.$u.http.get('/token/shop/bookDetail', { isbn }).then(res => {
+				console.log(res);
+				if (res.code === 200) {
+					this.currentProduct = res.data || {};
+					this.qualityOptions = res.data.skuList || [];
+					if (this.qualityOptions.length > 0) {
+						this.currentQuality = this.qualityOptions[0].conditionType
 					}
-				});
-			},
-
-			close() {
-				this.visible = false;
-			},
-			isOptionDisabled(opt) {
-				const stock = opt.stockNum;
-				if (stock === 0) return true;
-				if (stock === undefined || stock === null || stock === '') return false;
-				return this.toNumber(stock) <= 0;
-			},
-			selectQuality(opt) {
-				if (this.isOptionDisabled(opt)) {
-					this.$u.toast('该品相暂无库存');
-					return;
-				}
-				this.currentQuality = opt.conditionType;
-			},
-			handleAction() {
-				if (!this.hasStock) {
-					this.handleNotify();
-					return;
 				}
-				this.handleConfirm();
-			},
-			handleNotify() {
-				//设置到货通知
-				uni.$u.http.post('/token/shop/user/noticeArrival', {
-					isbn: this.currentProduct.isbn,
-				}).then(res => {
-					if (res.code === 200) {
-						this.$u.toast('到货通知设置成功');
-					}
-				}).finally(() => {
-					this.close();
-				});
-			},
-			handleConfirm() {
-				if (!this.currentProduct.isbn) {
-					this.$u.toast('商品信息缺失');
-					return;
-				}
-
-				const selectedOption = this.selectedOption;
-				const conditionType = selectedOption.conditionType || this.currentQuality;
+			});
+		},
 
-				if (!conditionType) {
-					this.$u.toast('请选择品相');
-					return;
+		close() {
+			this.visible = false;
+		},
+		isOptionDisabled(opt) {
+			const stock = opt.stockNum;
+			if (stock === 0) return true;
+			if (stock === undefined || stock === null || stock === '') return false;
+			return this.toNumber(stock) <= 0;
+		},
+		selectQuality(opt) {
+			if (this.isOptionDisabled(opt)) {
+				this.$u.toast('该品相暂无库存');
+				return;
+			}
+			this.currentQuality = opt.conditionType;
+		},
+		handleAction() {
+			if (!this.hasStock) {
+				this.handleNotify();
+				return;
+			}
+			this.handleConfirm();
+		},
+		handleNotify() {
+			//设置到货通知
+			uni.$u.http.post('/token/shop/user/noticeArrival', {
+				isbn: this.currentProduct.isbn,
+			}).then(res => {
+				if (res.code === 200) {
+					this.$u.toast('到货通知设置成功');
 				}
+			}).finally(() => {
+				this.close();
+			});
+		},
+		handleConfirm() {
+			if (this.isSubmitting) return; // 防止重复提交
+			
+			if (!this.currentProduct.isbn) {
+				this.$u.toast('商品信息缺失');
+				return;
+			}
 
-				this.$u.api.addShopCartAjax({
-					isbn: this.currentProduct.isbn,
-					quantity: this.quantity,
-					conditionType: conditionType,
-					sourceFrom: this.sourceFrom
-				}).then(res => {
-					if (res.code === 200) {
-						this.$u.toast('加入购物车成功');
-						this.$updateCartBadge();
-						this.$emit('confirm', {
-							product: this.currentProduct,
-							quality: this.currentQuality,
-							quantity: this.quantity
-						});
-						this.close();
-					} else {
-						this.$u.toast(res.msg || '加入购物车失败');
-					}
-				});
-			},
-			formatPrice(price) {
-				if (price === undefined || price === null) return '0.00';
-				return Number(price).toFixed(2);
-			},
-			toNumber(value) {
-				const num = Number(value);
-				return Number.isFinite(num) ? num : 0;
-			},
-			formatPrice(value) {
-				const num = this.toNumber(value);
-				return num.toFixed(2);
+			const selectedOption = this.selectedOption;
+			const conditionType = selectedOption.conditionType || this.currentQuality;
+
+			if (!conditionType) {
+				this.$u.toast('请选择品相');
+				return;
 			}
+
+			this.isSubmitting = true; // 设置提交中状态
+
+			this.$u.api.addShopCartAjax({
+				isbn: this.currentProduct.isbn,
+				quantity: this.quantity,
+				conditionType: conditionType,
+				sourceFrom: this.sourceFrom
+			}).then(res => {
+				if (res.code === 200) {
+					this.$u.toast('加入购物车成功');
+					this.$updateCartBadge();
+					this.$emit('confirm', {
+						product: this.currentProduct,
+						quality: this.currentQuality,
+						quantity: this.quantity
+					});
+					this.close();
+				} else {
+					this.$u.toast(res.msg || '加入购物车失败');
+				}
+			}).finally(() => {
+				this.isSubmitting = false; // 重置提交状态
+			});
+		},
+		formatPrice(price) {
+			if (price === undefined || price === null) return '0.00';
+			return Number(price).toFixed(2);
+		},
+		toNumber(value) {
+			const num = Number(value);
+			return Number.isFinite(num) ? num : 0;
+		},
+		formatPrice(value) {
+			const num = this.toNumber(value);
+			return num.toFixed(2);
 		}
-	};
+	}
+};
 </script>
 
 <style lang="scss" scoped>
-	.popup-content {
-		padding: 30rpx 30rpx 20rpx;
-		background-color: #fff;
-		position: relative;
+.popup-content {
+	padding: 30rpx 30rpx 20rpx;
+	background-color: #fff;
+	position: relative;
+}
+
+.header {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	position: relative;
+	margin-bottom: 30rpx;
+	padding-bottom: 30rpx;
+	border-bottom: 2rpx dashed #eee;
+
+	.title {
+		font-size: 34rpx;
+		font-weight: bold;
+		color: #333;
 	}
 
-	.header {
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		position: relative;
-		margin-bottom: 30rpx;
-		padding-bottom: 30rpx;
-		border-bottom: 2rpx dashed #eee;
-
-		.title {
-			font-size: 34rpx;
-			font-weight: bold;
-			color: #333;
-		}
-
-		.close-icon {
-			position: absolute;
-			right: 0;
-			top: 0;
-			width: 24rpx;
-			height: 24rpx;
-			padding: 10rpx;
-			box-sizing: content-box;
-		}
+	.close-icon {
+		position: absolute;
+		right: 0;
+		top: 0;
+		width: 24rpx;
+		height: 24rpx;
+		padding: 10rpx;
+		box-sizing: content-box;
+	}
+}
+
+.product-info {
+	display: flex;
+	margin-bottom: 30rpx;
+
+	.book-cover {
+		width: 140rpx;
+		height: 180rpx;
+		border-radius: 8rpx;
+		margin-right: 24rpx;
+		background-color: #f5f5f5;
 	}
 
-	.product-info {
+	.info-right {
+		flex: 1;
 		display: flex;
-		margin-bottom: 30rpx;
-
-		.book-cover {
-			width: 140rpx;
-			height: 180rpx;
-			border-radius: 8rpx;
-			margin-right: 24rpx;
-			background-color: #f5f5f5;
-		}
+		flex-direction: column;
+		justify-content: space-between;
+		padding: 10rpx 0;
 
-		.info-right {
-			flex: 1;
+		.price-row {
 			display: flex;
-			flex-direction: column;
-			justify-content: space-between;
-			padding: 10rpx 0;
-
-			.price-row {
-				display: flex;
-				align-items: center;
-
-				.currency {
-					font-size: 36rpx;
-					color: #D81A00;
-					font-weight: bold;
-				}
+			align-items: center;
 
-				.price {
-					font-size: 40rpx;
-					color: #D81A00;
-					font-weight: bold;
-					margin-right: 20rpx;
-					line-height: 1;
-				}
+			.currency {
+				font-size: 36rpx;
+				color: #D81A00;
+				font-weight: bold;
+			}
 
-				.drop-tag {
-					background: #E8F9EA;
-					padding: 4rpx 12rpx;
-					border-radius: 4rpx;
+			.price {
+				font-size: 40rpx;
+				color: #D81A00;
+				font-weight: bold;
+				margin-right: 20rpx;
+				line-height: 1;
+			}
 
-					text {
-						color: #38C248;
-						font-size: 24rpx;
-						font-weight: 500;
-					}
+			.drop-tag {
+				background: #E8F9EA;
+				padding: 4rpx 12rpx;
+				border-radius: 4rpx;
+
+				text {
+					color: #38C248;
+					font-size: 24rpx;
+					font-weight: 500;
 				}
 			}
+		}
 
-			.tag-row {
-				.quality-tag {
-					display: inline-block;
-					background: linear-gradient(0deg, #FFAB26 0%, #FFD426 100%);
-					border-radius: 21rpx 0px 21rpx 0px;
-					padding: 2rpx 24rpx;
-					margin-bottom: 24rpx;
-
-					text {
-						color: #fff;
-						font-size: 24rpx;
-						font-weight: 500;
-					}
+		.tag-row {
+			.quality-tag {
+				display: inline-block;
+				background: linear-gradient(0deg, #FFAB26 0%, #FFD426 100%);
+				border-radius: 21rpx 0px 21rpx 0px;
+				padding: 2rpx 24rpx;
+				margin-bottom: 24rpx;
+
+				text {
+					color: #fff;
+					font-size: 24rpx;
+					font-weight: 500;
 				}
 			}
 		}
 	}
+}
 
-	.tips-row {
-		display: flex;
-		align-items: center;
-		margin-bottom: 16rpx;
+.tips-row {
+	display: flex;
+	align-items: center;
+	margin-bottom: 16rpx;
 
-		text {
-			font-size: 26rpx;
-			color: #8D8D8D;
-			margin-right: 10rpx;
-		}
+	text {
+		font-size: 26rpx;
+		color: #8D8D8D;
+		margin-right: 10rpx;
+	}
 
-		.tips-icon {
-			width: 36rpx;
-			height: 36rpx;
-		}
+	.tips-icon {
+		width: 36rpx;
+		height: 36rpx;
 	}
+}
 
-	.promo-note {
-		margin-bottom: 40rpx;
+.promo-note {
+	margin-bottom: 40rpx;
 
-		text {
-			font-size: 26rpx;
-			color: #8D8D8D;
-		}
+	text {
+		font-size: 26rpx;
+		color: #8D8D8D;
 	}
+}
 
-	.options-list {
-		margin-bottom: 40rpx;
+.options-list {
+	margin-bottom: 40rpx;
 
-		.option-item {
-			position: relative;
-			display: flex;
-			justify-content: space-between;
-			align-items: center;
-			background: #F8F8F8;
-			border-radius: 12rpx;
-			padding: 24rpx 30rpx;
-			margin-bottom: 24rpx;
-			border: 2rpx solid #dfdfdf;
-			transition: all 0.2s;
-
-			&.active {
-				border-color: transparent;
-			}
-
-			&.disabled {
-				background: #F0F0F0;
-				border-color: #E0E0E0;
-				opacity: 0.7;
+	.option-item {
+		position: relative;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		background: #ffffff;
+		border-radius: 12rpx;
+		padding: 24rpx 30rpx;
+		margin-bottom: 24rpx;
+		border: 2rpx solid #dfdfdf;
+		transition: all 0.2s;
+
+		&.active {
+			border-color: transparent;
+		}
 
-				.opt-name,
-				.right text {
-					color: #999;
-				}
+		&.disabled {
+			background: #F0F0F0;
+			border-color: #E0E0E0;
+			opacity: 0.7;
 
-				.opt-discount {
-					background: #CCCCCC;
-				}
+			.opt-name,
+			.right text {
+				color: #999;
 			}
 
-			.bg-image {
-				position: absolute;
-				top: -6rpx;
-				left: -1%;
-				width: 102%;
-				height: 110rpx;
-				z-index: 0;
+			.opt-discount {
+				background: #CCCCCC;
 			}
+		}
 
-			.left,
-			.right {
-				position: relative;
-				z-index: 1;
-			}
+		.bg-image {
+			position: absolute;
+			top: -6rpx;
+			left: -1%;
+			width: 102%;
+			height: 110rpx;
+			z-index: 0;
+		}
 
-			.left {
-				display: flex;
-				align-items: center;
+		.left,
+		.right {
+			position: relative;
+			z-index: 1;
+		}
 
-				.opt-name {
-					font-size: 30rpx;
-					font-weight: bold;
-					color: #333;
-					margin-right: 16rpx;
-				}
+		.left {
+			display: flex;
+			align-items: center;
 
-				.opt-discount {
-					background: #D8D8D8;
-					padding: 0 20rpx;
-					border-radius: 0 20rpx 0 20rpx;
+			.opt-name {
+				font-size: 30rpx;
+				font-weight: bold;
+				color: #333;
+				margin-right: 16rpx;
+			}
 
-					text {
-						color: #fff;
-						font-size: 24rpx;
-						display: inline-block;
-					}
+			.opt-discount {
+				background: #D8D8D8;
+				padding: 0 20rpx;
+				border-radius: 0 20rpx 0 20rpx;
 
-					&.active {
-						background: linear-gradient(0deg, #30E030 0%, #28C445 100%);
-					}
+				text {
+					color: #fff;
+					font-size: 24rpx;
+					display: inline-block;
 				}
-			}
 
-			.right {
-				text {
-					font-size: 28rpx;
-					color: #333;
+				&.active {
+					background: linear-gradient(0deg, #30E030 0%, #28C445 100%);
 				}
 			}
 		}
-	}
-
-	.quantity-row {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-		margin-bottom: 50rpx;
 
-		.label {
-			font-size: 30rpx;
-			font-weight: bold;
-			color: #333;
+		.right {
+			text {
+				font-size: 28rpx;
+				color: #333;
+			}
 		}
 	}
+}
+
+.quantity-row {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 50rpx;
+
+	.label {
+		font-size: 30rpx;
+		font-weight: bold;
+		color: #333;
+	}
+}
 
-	.footer-btns {
-		display: flex;
-		justify-content: space-between;
-		padding-bottom: 10rpx;
-
-		.btn {
-			flex: 1;
-			height: 100rpx;
-			display: flex;
-			flex-direction: column;
-			align-items: center;
-			justify-content: center;
-			font-family: Source Han Sans SC;
-			font-weight: 500;
+.footer-btns {
+	display: flex;
+	justify-content: space-between;
+	padding-bottom: 10rpx;
 
-			.price {
-				font-size: 38rpx;
-				color: #fff;
-				line-height: 1.2;
-			}
+	.btn {
+		flex: 1;
+		height: 100rpx;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		font-family: Source Han Sans SC;
+		font-weight: 500;
+		border: none;
+		outline: none;
+		padding: 0;
+		margin: 0;
+		-webkit-tap-highlight-color: transparent;
+
+		.price {
+			font-size: 38rpx;
+			color: #fff;
+			line-height: 32rpx;
+		}
 
-			.desc {
-				font-size: 24rpx;
-				color: #fff;
-			}
+		.desc {
+			font-size: 24rpx;
+			color: #fff;
+			line-height: 32rpx;
+		}
 
-			.notice {
-				font-size: 32rpx;
-				color: #fff;
-			}
+		.notice {
+			font-size: 32rpx;
+			color: #fff;
+		}
 
-			&.btn-orange {
-				background: linear-gradient(0deg, #EFA941 0%, #FFB84F 100%);
-				width: 340rpx;
-				border-radius: 50rpx 0 0 50rpx;
-			}
+		&.btn-orange {
+			background: linear-gradient(0deg, #EFA941 0%, #FFB84F 100%);
+			width: 340rpx;
+			border-radius: 50rpx 0 0 50rpx;
+		}
 
-			&.btn-green {
-				width: 340rpx;
-				background: linear-gradient(0deg, #38C248 0%, #5FEA6F 100%);
-				border-radius: 0 50rpx 50rpx 0;
-			}
+		&.btn-green {
+			width: 340rpx;
+			background: linear-gradient(0deg, #38C248 0%, #5FEA6F 100%);
+			border-radius: 0 50rpx 50rpx 0;
 		}
 	}
+}
 </style>

+ 1 - 1
pages-sell/components/sell-container/index.vue

@@ -126,7 +126,7 @@
 
 				<view class="book-list">
 					<view class="book-item" v-for="(book, index) in getDisplayBooks(item)" :key="index"
-						@click="navigateTo('/pages-sell/pages/detail?isbn=' + book.bookIsbn)">
+						@click.stop="navigateTo('/pages-sell/pages/detail?isbn=' + book.bookIsbn)">
 						<image :src="book.bookImg" class="book-image" mode="aspectFill"></image>
 						<text class="book-name">{{ book.bookName }}</text>
 						<view class="price-row">

+ 3 - 3
pages/mine/index.vue

@@ -23,10 +23,10 @@
 					<view class="amount">{{ userInfo.accountMoney || 0 }}</view>
 					<view class="label">我的钱包</view>
 				</view>
-				<view class="data-item">
+				<view class="data-item" @click="navigateToTool('/pages-car/pages/red-packet')">
 					<view class="amount">{{ userInfo.couponNum || 0 }}</view>
-					<view class="label">优惠券</view>
-					<view class="badge" v-if="userInfo.couponNum">{{ userInfo.couponNum }}可领</view>
+					<view class="label">我的红包</view>
+					<view class="badge" v-if="userInfo.couponNum">{{ userInfo.couponNum }}可领</view>
 				</view>
 				<view class="data-item">
 					<view class="amount">{{ userInfo.point || 0 }}</view>