绿满眶商城微信小程序-uniapp
peng
2025-10-11 f1d0c08bbef3ffef0b9bbd0c181d2047c9866a7f
pages/product/m-buy/goods.vue
@@ -1,6 +1,6 @@
<template>
   <div class="wrapper">
      <u-popup class="popup" v-model="buyMask" :height="setup.height" closeable :mode="setup.mode" :border-radius="setup.radius" @close="closeMask()">
      <u-popup class="popup" :value="buyMask" :height="setup.height" closeable :mode="setup.mode" :border-radius="setup.radius" @input="handlePopupInput" @close="closeMask()">
         <!-- 商品 -->
         <view class="goods-box bottom">
            <view class="goods-header">
@@ -102,7 +102,93 @@
                  <uni-number-box class="uNumber" :min="1" :max="999" :disabled="goodsDetail.quantity === 0"  v-model="num"></uni-number-box>
               </view>
               <view class="template">
                  {{JSON.stringify(consumizetemplateInfo)}}
                  <view class="template-title" v-if="consumizetemplateInfo && consumizetemplateInfo.name">
                     <text class="title-text">{{ consumizetemplateInfo.name }}</text>
                  </view>
                  <!-- 图片选择区域 -->
                  <view class="template-images" v-if="consumizetemplateInfo && consumizetemplateInfo.templateImgs && consumizetemplateInfo.templateImgs.length > 0">
                     <view class="images-grid">
                        <view
                           v-for="(img, index) in consumizetemplateInfo.templateImgs"
                           :key="img.id"
                           class="image-item"
                           :class="{ selected: selectedImages.includes(img.imgUrl) }"
                           @click="selectImage(img.imgUrl)"
                        >
                           <image
                              :src="getFilePreviewUrlSync(img.imgUrl)"
                              class="image-preview"
                              mode="aspectFill"
                           />
                           <view v-if="selectedImages.includes(img.imgUrl)" class="selected-overlay">
                              <uni-icons type="checkmarkempty" size="30" color="#fff"></uni-icons>
                           </view>
                        </view>
                     </view>
                  </view>
                  <!-- 动态表单项 -->
                  <view class="template-form" v-if="consumizetemplateInfo && consumizetemplateInfo.templateConstomizeTitles">
                     <view
                        v-for="(item, index) in consumizetemplateInfo.templateConstomizeTitles"
                        :key="item.id"
                        class="form-item"
                     >
                        <text class="form-label">{{ item.templateTitle }}</text>
                        <!-- 文本输入框 -->
                        <input
                           v-if="item.contentType === 'TEXT'"
                           class="form-input"
                           :value="getFormValue(item.id)"
                           @input="updateFormValue(item.id, item.templateTitle, $event.target.value)"
                           :placeholder="'请输入' + item.templateTitle"
                        />
                        <!-- 图片上传(不使用u-upload组件) -->
                        <view v-if="item.contentType === 'IMAGE'" class="form-upload">
                           <!-- 上传按钮 -->
                           <view class="upload-btn" @click="chooseImage(item.id)">
                              <uni-icons type="plusempty" size="40" color="#999"></uni-icons>
                              <text class="upload-text">点击上传</text>
                           </view>
                           <!-- 图片预览区域 -->
                           <view v-if="imagePreviewUrls[item.id]" class="image-preview-container">
                              <image
                                 :src="imagePreviewUrls[item.id]"
                                 class="uploaded-image"
                                 mode="aspectFill"
                              />
                              <uni-icons
                                 type="closeempty"
                                 class="delete-icon"
                                 @click="deleteImage(item.id)"
                                 size="20"
                                 color="#fff"
                              ></uni-icons>
                           </view>
                           <!-- 上传进度条 -->
                           <progress
                              v-if="uploadProgress[item.id] && uploadProgress[item.id] > 0 && uploadProgress[item.id] < 100"
                              :percent="uploadProgress[item.id]"
                              active-mode="forwards"
                              show-info
                              stroke-width="6"
                              :active="true"
                              active-color="#ff573e"
                              style="margin-top: 10rpx;"
                           />
                           <!-- 上传状态信息 -->
                           <text v-if="uploadStatus[item.id]" class="upload-status">
                              {{ uploadStatus[item.id] }}
                           </text>
                        </view>
                     </view>
                  </view>
               </view>
            </scroll-view>
            <!-- 按钮 -->
@@ -120,6 +206,10 @@
import * as API_trade from '@/api/trade.js';
import setup from './popup';
// import uniNumberBox from '@/components/uni-number-box'
import { getFilePreviewUrl } from "@/api/common.js";
import { getSTSToken } from "@/api/common.js";
import { getFileKey } from "@/utils/file.js";
export default {
   components: {
      // uniNumberBox
@@ -137,7 +227,23 @@
         formatList: [],
         currentSelected: [],
         skuList: '',
         isClose: false //是否可以点击遮罩关闭
         isClose: false, //是否可以点击遮罩关闭
         // 表单相关数据
         selectedImages: [], // 选中的模板图片
         formValues: {
            templateId: "", // 模板ID
            templateName: "", // 模板名称
            chooseImage: "", // 选中的图片
            templateForm: [] // 表单数组 [{id, templateTitle, value}]
         }, // 表单值
         imagePreviewUrls: {}, // 图片预览URL
         uploadProgress: {}, // 上传进度
         uploadStatus: {}, // 上传状态信息
         cosClient: null, // COS客户端
         bucket: '', // 存储桶
         region: '', // 地域
         previewUrls: {} // 文件预览地址缓存
      };
   },
   props: {
@@ -211,6 +317,7 @@
      buyType: {
         handler(val) {
            if (val) {
               // 使用直接赋值而不是this.$set
               this.buyType = val;
            }
         },
@@ -218,39 +325,337 @@
      },
      selectSkuList: {
         handler(val, oldval) {
            // 使用setTimeout延迟触发事件,避免影响弹窗
            setTimeout(() => {
            this.$emit('changed', val);
            }, 100);
         },
         deep: true
      },
      'goodsDetail.quantity': {
         handler(val) {
            if (val == 0) {
               // 使用setTimeout延迟显示提示,避免影响弹窗
               setTimeout(() => {
               uni.showToast({
                  title: '商品已售罄',
                  duration: 2000,
                  icon: 'none'
               })
                  });
               }, 100);
               this.num = 1;
            }
         }
      },
      // 监听模板信息变化
      consumizetemplateInfo: {
         handler(newVal, oldVal) {
            if (newVal) {
               // 使用setTimeout延迟执行,避免影响弹窗
               setTimeout(() => {
                  // 预加载模板图片的预览地址
                  this.preloadTemplateImages();
                  // 初始化模板表单数据
                  this.initTemplateFormData();
               }, 100);
            }
         },
         immediate: true,
         deep: true
      }
   },
   methods: {
      // 初始化腾讯云COS客户端
      initCOS() {
         getSTSToken().then(res => {
            const COS = require('@/lib/cos-wx-sdk-v5.js');
            // 使用直接赋值而不是this.$set
            this.cosClient = new COS({
               SecretId: res.data.data.tmpSecretId,
               SecretKey: res.data.data.tmpSecretKey,
               SecurityToken: res.data.data.sessionToken,
               StartTime: res.data.data.stsStartTime,
               ExpiredTime: res.data.data.stsEndTime,
               SimpleUploadMethod: 'putObject'
            });
            this.bucket = res.data.data.bucket;
            this.region = res.data.data.region;
         }).catch(err => {
            console.error('初始化COS失败', err);
            // 使用setTimeout延迟显示提示,避免影响弹窗
            setTimeout(() => {
               uni.showToast({
                  title: '上传服务初始化失败',
                  icon: 'none'
               });
            }, 100);
         });
      },
      // 获取文件预览地址
      async fetchFilePreviewUrl(fileKey) {
         // 如果是完整的URL,直接返回
         if (fileKey && (fileKey.startsWith('http://') || fileKey.startsWith('https://'))) {
            return fileKey;
         }
         // 如果已经缓存过,直接返回缓存值
         if (this.previewUrls[fileKey]) {
            return this.previewUrls[fileKey];
         }
         // 调用API获取预览地址
         try {
            const res = await getFilePreviewUrl(fileKey);
            const previewUrl = res.data.data;
            // 使用直接赋值而不是this.$set
            this.previewUrls[fileKey] = previewUrl;
            return previewUrl;
         } catch (error) {
            console.error('获取文件预览地址失败', error);
            return fileKey; // 如果获取失败,返回原始值
         }
      },
      // 选择模板图片
      selectImage(imgUrl) {
         const index = this.selectedImages.indexOf(imgUrl);
         if (index > -1) {
            // 如果已选中,则取消选择
            this.selectedImages.splice(index, 1);
         } else {
            // 否则添加到选中列表
            this.selectedImages.push(imgUrl);
         }
         // 更新formValues中的chooseImage
         this.formValues.chooseImage = this.selectedImages.join(',');
      },
      // 选择图片(参考video.vue的实现)
      chooseImage(fieldId) {
         if (!this.cosClient) {
            // 使用setTimeout延迟显示提示,避免影响弹窗
            setTimeout(() => {
               uni.showToast({
                  title: '上传服务未初始化',
                  icon: 'none'
               });
            }, 100);
            // 重新初始化COS
            this.initCOS();
            return;
         }
         uni.chooseImage({
            count: 1,
            sizeType: ['compressed'],
            sourceType: ['album', 'camera'],
            success: (res) => {
               const tempFilePath = res.tempFilePaths[0];
               // 获取文件名
               let fileName = tempFilePath.substring(tempFilePath.lastIndexOf('/') + 1);
               // 处理安卓可能的URI编码
               if(fileName.indexOf('%') > -1) {
                  fileName = decodeURIComponent(fileName);
               }
               // 生成文件key
               const fileKey = getFileKey(fileName);
               // 显示预览图
               this.$set(this.imagePreviewUrls, fieldId, tempFilePath);
               // 初始化上传状态
               this.$set(this.uploadProgress, fieldId, 0);
               this.$set(this.uploadStatus, fieldId, '上传中...');
               // 强制更新视图
               this.$forceUpdate();
               // 执行上传
               this.cosClient.uploadFile({
                  Bucket: this.bucket,
                  Region: this.region,
                  Key: fileKey,
                  FilePath: tempFilePath,
                  SliceSize: 1024 * 1024 * 5, // 5M
                  onProgress: (progressData) => {
                     // 更新上传进度
                     const progress = Math.round(progressData.percent * 100);
                     this.$set(this.uploadProgress, fieldId, progress);
                     // 强制更新视图
                     this.$forceUpdate();
                  }
               }, (err, data) => {
                  // 清除上传进度
                  // 使用Vue.delete或this.$delete可能会触发组件更新,改用直接删除
                  delete this.uploadProgress[fieldId];
                  if (err) {
                     console.error('上传失败', err);
                     // 更新上传状态
                     this.$set(this.uploadStatus, fieldId, '上传失败');
                     // 清除预览图
                     this.$delete(this.imagePreviewUrls, fieldId);
                     // 注意:这里不再需要清除formValues中的字段,因为我们已经修改了数据结构
                     // 强制更新视图
                     this.$forceUpdate();
                     // 使用setTimeout延迟显示错误提示,避免影响弹窗
                     setTimeout(() => {
                        uni.showToast({
                           title: '上传失败',
                           icon: 'none'
                        });
                     }, 100);
                  } else {
                     // 获取当前表单项的信息
                     const currentItem = this.consumizetemplateInfo.templateConstomizeTitles.find(item => item.id === fieldId);
                     const templateTitle = currentItem ? currentItem.templateTitle : '';
                     // 上传成功,更新表单值
                     this.updateFormValue(fieldId, templateTitle, fileKey);
                     // 更新上传状态
                     this.$set(this.uploadStatus, fieldId, '上传成功');
                     // 强制更新视图
                     this.$forceUpdate();
                     // 使用setTimeout延迟显示成功提示,避免影响弹窗
                     setTimeout(() => {
                        uni.showToast({
                           title: '上传成功',
                           icon: 'success'
                        });
                     }, 100);
                  }
               });
            },
            fail: (err) => {
               console.error('选择图片失败', err);
               // 区分用户取消操作和真正的错误
               // 如果是用户主动取消,不显示提示
               if (err.errMsg && err.errMsg.indexOf('cancel') === -1 && err.errMsg.indexOf('fail') !== -1) {
                  // 使用setTimeout延迟显示提示,避免影响弹窗
                  setTimeout(() => {
                     uni.showToast({
                        title: '未选择图片',
                        icon: 'none'
                     });
                  }, 100);
               }
            }
         });
      },
      // 删除图片
      deleteImage(fieldId) {
         // 清除预览图
         this.$delete(this.imagePreviewUrls, fieldId);
         // 清除上传状态
         this.$delete(this.uploadStatus, fieldId);
         // 清除上传进度
         this.$delete(this.uploadProgress, fieldId);
         // 从formValues中移除对应的表单字段
         const index = this.formValues.templateForm.findIndex(item => item.id === fieldId);
         if (index > -1) {
            this.formValues.templateForm.splice(index, 1);
         }
         // 使用setTimeout延迟显示提示,避免影响弹窗
         setTimeout(() => {
            uni.showToast({
               title: '删除成功',
               icon: 'success'
            });
         }, 100);
         // 强制更新视图
         this.$forceUpdate();
      },
      // 获取表单值
      getFormValue(fieldId) {
         const field = this.formValues.templateForm.find(item => item.id === fieldId);
         return field ? field.value : '';
      },
      // 更新表单值
      updateFormValue(fieldId, templateTitle, value) {
         // 更新模板基本信息
         this.formValues.templateId = this.consumizetemplateInfo.id;
         this.formValues.templateName = this.consumizetemplateInfo.name;
         // 查找是否已存在该字段
         const existingIndex = this.formValues.templateForm.findIndex(item => item.id === fieldId);
         if (existingIndex > -1) {
            // 更新现有字段
            this.$set(this.formValues.templateForm, existingIndex, {
               id: fieldId,
               templateTitle: templateTitle,
               value: value
            });
         } else {
            // 添加新字段
            this.formValues.templateForm.push({
               id: fieldId,
               templateTitle: templateTitle,
               value: value
            });
         }
      },
      // 更新模板表单数据
      updateTemplateFormData(fieldId, templateTitle, value) {
         // 这个方法现在可以保持不变,因为我们已经在updateFormValue中处理了formValues的更新
         // 如果需要其他处理,可以在这里添加
      },
      // 表单提交处理
      handleFormSubmit() {
         // 使用新的formValues数据结构
         const formData = this.formValues;
         console.log('表单数据:', formData);
         // 这里可以添加实际的表单提交逻辑
         // 使用setTimeout延迟显示提示,避免影响弹窗
         setTimeout(() => {
            uni.showToast({
               title: '表单提交成功',
               icon: 'success'
            });
         }, 100);
      },
      numCheck(val) {
         if (this.wholesaleList && this.wholesaleList.length > 0) {
            if (this.num <= this.wholesaleList[0].num) {
               // 使用setTimeout延迟显示提示,避免影响弹窗
               setTimeout(() => {
               uni.showToast({
                  title: '批发商品购买数量不能小于起批数量!',
                  duration: 2000,
                  icon: 'none'
               });
               }, 100);
               this.num = this.wholesaleList[0].num;
            }
         }
      },
      closeMask() {
         this.$emit('closeBuy', false);
      },
      // 处理弹窗输入事件,避免直接修改prop
      handlePopupInput(val) {
         this.$emit('closeBuy', val);
      },
      
      /**点击规格 */
@@ -285,11 +690,14 @@
               goodsId: this.goodsDetail.goodsId
            });
         } else {
            // 使用setTimeout延迟显示提示,避免影响弹窗
            setTimeout(() => {
            uni.showToast({
               title: '暂无该商品!',
               duration: 2000,
               icon: 'none'
            });
            }, 100);
         }
      },
@@ -299,9 +707,12 @@
      buy(data) {
         API_trade.addToCart(data).then(res => {
            if (res.data.success) {
               // 使用setTimeout延迟跳转,避免影响弹窗
               setTimeout(() => {
               uni.navigateTo({
                  url: `/pages/order/fillorder?way=${data.cartType}&addr=${''}&parentOrder=${encodeURIComponent(JSON.stringify(this.parentOrder))}`
               });
               }, 100);
            }
         });
      },
@@ -310,11 +721,15 @@
       * 添加到购物车或购买
       */
      addToCartOrBuy(val) {
         console.log(JSON.stringify(this.formValues))
         if (!this.selectSkuList) {
            // 使用setTimeout延迟显示提示,避免影响弹窗
            setTimeout(() => {
            uni.showToast({
               title: '请选择规格商品',
               icon: 'none'
            });
            }, 100);
            return;
         }
         let data = {
@@ -325,10 +740,13 @@
         if (val == 'cart') {
            API_trade.addToCart(data).then(res => {
               if (res.data.code == 200) {
                  // 使用setTimeout延迟显示提示,避免影响弹窗
                  setTimeout(() => {
                  uni.showToast({
                     title: '商品已添加到购物车',
                     icon: 'none'
                  });
                  }, 100);
                  this.$emit('queryCart');
                  this.closeMask();
@@ -346,9 +764,12 @@
            API_trade.addToCart(data).then(res => {
               if (res.data.code == 200) {
                  // 使用setTimeout延迟跳转,避免影响弹窗
                  setTimeout(() => {
                  uni.navigateTo({
                     url: `/pages/order/fillorder?way=${data.cartType}&addr=${this.addr.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(this.parentOrder))}`
                        url: `/pages/order/fillorder?way=${data.cartType}&addr=${this.addr.id || ''}&template=${encodeURIComponent(JSON.stringify(this.formValues))}&parentOrder=${encodeURIComponent(JSON.stringify(this.parentOrder))}`
                  });
                  }, 100);
               }
            });
         }
@@ -380,6 +801,7 @@
                        return i.value === values.value;
                     })
                  ) {
                     // 使用直接赋值而不是this.$set
                     arrItem.values.push(values);
                  }
@@ -387,6 +809,7 @@
                     return key.name;
                  });
                  if (!keys.includes(name)) {
                     // 使用直接赋值而不是this.$set
                     arr.push({
                        name: name,
                        values: [values]
@@ -397,6 +820,7 @@
         });
         arr.shift();
         // 使用直接赋值而不是this.$set
         this.formatList = arr;
         list.forEach(item => {
@@ -405,6 +829,7 @@
               item.specValues
                  .filter(i => i.specName !== 'images')
                  .forEach((value, _index) => {
                     // 使用直接赋值而不是this.$set
                     this.currentSelected[_index] = value.specValue;
                     this.selectName = value.specValue;
@@ -417,15 +842,60 @@
            }
         });
         // 使用直接赋值而不是this.$set
         this.skuList = list;
         // console.log(" this.skuList", this.skuList)
      },
      // 预加载模板图片的预览地址
      async preloadTemplateImages() {
         if (this.consumizetemplateInfo && this.consumizetemplateInfo.templateImgs) {
            for (const img of this.consumizetemplateInfo.templateImgs) {
               await this.fetchFilePreviewUrl(img.imgUrl);
            }
         }
      },
      // 获取文件预览地址(同步版本)
      getFilePreviewUrlSync(fileKey) {
         // 如果是完整的URL,直接返回
         if (fileKey && (fileKey.startsWith('http://') || fileKey.startsWith('https://'))) {
            return fileKey;
         }
         // 如果有预览地址,返回预览地址
         if (this.previewUrls && this.previewUrls[fileKey]) {
            return this.previewUrls[fileKey];
         }
         // 否则返回原始值
         return fileKey;
      },
      // 初始化模板表单数据
      initTemplateFormData() {
         if (this.consumizetemplateInfo && this.consumizetemplateInfo.templateConstomizeTitles) {
            // 初始化formValues的基本信息
            this.formValues.templateId = this.consumizetemplateInfo.id;
            this.formValues.templateName = this.consumizetemplateInfo.name;
            this.formValues.chooseImage = "";
            this.formValues.templateForm = [];
         }
      }
   },
   mounted() {
      // 使用setTimeout延迟执行,避免影响弹窗
      setTimeout(() => {
      this.formatSku(this.goodsSpec);
         this.initCOS(); // 初始化COS客户端
         // 预加载模板图片的预览地址
         this.preloadTemplateImages();
         // 初始化模板表单数据
         this.initTemplateFormData();
      console.log("goodsDetail",this.goodsDetail)
      }, 100);
   }
};
</script>
@@ -580,4 +1050,137 @@
      color: #333;
   }
}
// 模板样式
.template {
   padding: 20rpx;
}
.template-title {
   margin-bottom: 30rpx;
   text-align: center;
}
.title-text {
   font-size: 36rpx;
   font-weight: bold;
   color: #333;
}
// 图片选择区域
.template-images {
   margin-bottom: 40rpx;
}
.images-grid {
   display: flex;
   flex-wrap: wrap;
   gap: 20rpx;
}
.image-item {
   width: calc((100% - 40rpx) / 3);
   height: 200rpx;
   border: 2rpx solid #e0e0e0;
   border-radius: 10rpx;
   overflow: hidden;
   position: relative;
   &.selected {
      border-color: #ff6b00;
      box-shadow: 0 0 10rpx rgba(255, 107, 0, 0.5);
   }
}
.selected-overlay {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background-color: rgba(0, 0, 0, 0.3);
   display: flex;
   align-items: center;
   justify-content: center;
}
.image-preview {
   width: 100%;
   height: 100%;
}
// 表单区域
.template-form {
}
.form-item {
   margin-bottom: 30rpx;
}
.form-label {
   display: block;
   margin-bottom: 15rpx;
   font-size: 28rpx;
   color: #333;
}
.form-input {
   width: 100%;
   height: 80rpx;
   padding: 0 20rpx;
   border: 2rpx solid #e0e0e0;
   border-radius: 10rpx;
   font-size: 28rpx;
   box-sizing: border-box;
}
.form-upload {
   margin-top: 15rpx;
}
// 图片上传样式
.upload-btn {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   width: 150rpx;
   height: 150rpx;
   border: 2rpx dashed #ccc;
   border-radius: 10rpx;
   background-color: #f8f8f8;
}
.upload-text {
   font-size: 24rpx;
   color: #999;
   margin-top: 10rpx;
}
.image-preview-container {
   position: relative;
   width: 150rpx;
   height: 150rpx;
   margin-top: 10rpx;
}
.uploaded-image {
   width: 100%;
   height: 100%;
   border-radius: 10rpx;
}
.delete-icon {
   position: absolute;
   top: -10rpx;
   right: -10rpx;
   background-color: #ff6b00;
   border-radius: 50%;
   width: 30rpx;
   height: 30rpx;
   display: flex;
   align-items: center;
   justify-content: center;
}
</style>