Просмотр исходного кода

更新:1.消息有了分类:文字消息与商品(图片)消息 2.新增商品消息

shizhongqi 11 месяцев назад
Родитель
Сommit
5caa537a00

+ 255 - 44
hdApp/src/admin/chat/chatPage.vue

@@ -2,7 +2,7 @@
   <view class="chat-container">
     <!-- 连接状态指示器 -->
     <view class="connection-status" v-if="!isConnected">
-      <text class="status-text">{{ reconnectCount > 0 ? '重连中...' : '连接中...' }}</text>
+      <text class="status-text">{{ '连接中...' }}</text>
     </view>
 
     <!-- 聊天消息列表 -->
@@ -29,13 +29,39 @@
           <view class="avatar-wrapper">
             <image class="avatar" :src="message.avatar" mode="aspectFill" />
           </view>
-          <view class="message-content-wrapper">
+          <view v-if="message.messageType == 'text'|| message.messageType != 'goods'" class="message-content-wrapper">
             <view class="username" v-if="isMultiUser">{{ message.username }}</view>
             <view class="message-bubble left-bubble">
               <text class="message-text">{{ message.message }}</text>
             </view>
             <view class="message-time">{{ message.time }}</view>
           </view>
+          <view v-if="message.messageType == 'goods'" class="message-content-wrapper">
+            <view class="username align-right" v-if="isMultiUser">{{
+              message.username
+            }}</view>
+            <view class="goods-info right-bubble">
+              <view class="goods-main">
+                <image class="goods-image" :src="message.goodsInfo.goodsImg" mode="aspectFill"></image>
+                <view class="goods-details">
+                  <view class="goods-name-wrapper">
+                    <text class="goods-name">{{message.goodsInfo.goodsName}}</text>
+                  </view>
+                  <!-- <view class="sales-info">
+                    <text class="delivery-time">48小时发</text>
+                  </view> -->
+                  <text v-if="message.goodsInfo.goodsPriceType == 1" class="goods-price">¥{{ message.goodsInfo.goodsPrice }}</text>
+                </view>
+              </view>
+              <view class="goods-footer">
+              </view>
+              <view class="action-buttons">
+                <input class="price-input" type="number" placeholder="输入价格" />
+                <button class="action-btn buy-btn" click="createPrice(message.goodsInfo)">报价</button>
+              </view>
+            </view>
+            <view class="message-time align-right">{{ message.time }}</view>
+          </view>
         </view>
 
         <!-- 右侧消息(我的) -->
@@ -69,7 +95,6 @@
           :placeholder-style="placeholderStyle"
           @confirm="sendMessage"
           @focus="onInputFocus"
-          @blur="onInputBlur"
           @input="onInputChange"
           confirm-type="send"
           :adjust-position="false"
@@ -77,13 +102,13 @@
           :maxlength="500"
           :show-confirm-bar="false"
           :focus="inputFocus"
-        />
+          /><!-- @blur="onInputBlur" -->
         <button
           class="send-button"
           :class="{ 'send-active': inputText.trim() }"
           @click="sendMessage"
           :disabled="!inputText.trim()"
-        >
+        > <!-- @touchstart.prevent="sendMessage" -->
           发送
         </button>
       </view>
@@ -116,10 +141,10 @@ export default {
       messageList: [],
       // WebSocket相关
       socket: null,
-      isConnected: false,
+      isConnected: true,
       room: '',
       reconnectCount: 0,
-      maxReconnectCount: 3,
+      maxReconnectCount: 5,
       isManualDisconnect: false, // 添加标志位:是否主动断开连接
       scrollWithAnimation: false, // 控制滚动动画
       // 分页加载
@@ -135,7 +160,14 @@ export default {
     };
   },
   onLoad(options) {
-    console.log('---------- options: ', options);
+    // console.log('---------- options: ', options);
+    if (options.chatPerson) {
+      // 设置聊天人名称
+      uni.setNavigationBarTitle({
+        title: options.chatPerson
+      })
+    }
+
     // 根据传入参数判断是否多人聊天
     if (options.isMultiUser) {
       this.isMultiUser = options.isMultiUser === 'true';
@@ -219,10 +251,17 @@ export default {
         userId: this.currentUser.id
       }).then((res) => {
         if (res.code == 1 && res.data) {
-          this.allMessages = res.data.map((item, index) => ({
-            ...item,
-            id: `msg_${index}`, // 为每条消息添加唯一ID
-          }));
+          this.allMessages = res.data.map((item, index) => {
+            const newItem = {
+              ...item,
+              id: `message-${index}`, // 为每条消息添加唯一ID
+            };
+            //如果消息类型为 'goods',就调用 parseGoodsMessage 并将结果存入新属性 goodsInfo
+            if (newItem.messageType === 'goods') {
+              newItem.goodsInfo = this.parseGoodsMessage(newItem.message);
+            }
+            return newItem;
+          });
 
           if (this.allMessages.length > this.initialLoadSize) {
             this.messageList = this.allMessages.slice(
@@ -299,13 +338,12 @@ export default {
 
         // 构建子协议参数(模拟POST body传输)
         const protocols = this.buildWebSocketProtocols();
-        console.log('protocols: ', protocols);
         // 创建WebSocket连接
         this.socket = uni.connectSocket({
           url: wsUrl,
           protocols: protocols,
           success: () => {
-            console.log('WebSocket连接创建成功');
+            console.log('创建WebSocket连接...');
           },
           fail: (error) => {
             console.error('WebSocket连接创建失败:', error);
@@ -313,9 +351,9 @@ export default {
           }
         });
 
-        // 监听WebSocket连接打开
+        // 监听 WebSocket连接打开
         this.socket.onOpen(() => {
-          console.log('WebSocket连接已打开');
+          console.log('成功连接WebSocket');
           this.isConnected = true;
           this.reconnectCount = 0;
           this.isManualDisconnect = false; // 连接成功时重置标志位
@@ -326,10 +364,9 @@ export default {
           this.handleWebSocketMessage(event.data);
         });
 
-        // 监听WebSocket连接关闭
+        // 监听 WebSocket连接关闭
         this.socket.onClose(() => {
           console.log('WebSocket连接已关闭');
-          this.isConnected = false;
 
           // 只有在非主动断开的情况下才尝试重连
           if (!this.isManualDisconnect && this.reconnectCount < this.maxReconnectCount) {
@@ -337,12 +374,17 @@ export default {
               this.reconnectWebSocket();
             }, 3000);
           }
+          
+          // 尝试重新连接超过 maxReconnectCount 次才标记为未连接状态
+          if (!this.isManualDisconnect && this.reconnectCount >= this.maxReconnectCount) {
+            this.isConnected = false;
+          }
         });
 
         // 监听WebSocket错误
         this.socket.onError((error) => {
           console.error('WebSocket连接错误:', error);
-          this.isConnected = false;
+          this.isConnected = true;
           this.handleConnectionError();
         });
       } catch (error) {
@@ -404,7 +446,6 @@ export default {
 
         // 验证协议字符串的合法性
         const isValid = this.validateProtocols(protocols);
-        console.log('协议验证结果:', isValid);
         if (!isValid) {
           this.$msg('协议验证失败');
           return;
@@ -450,7 +491,7 @@ export default {
       }
       // #endif
 
-      // #ifdef MP-WEIXIN || MP-ALIPAY || APP-PLUS
+      // #ifndef H5
       // 小程序和App环境使用自定义编码
       return this.customBase64Encode(str);
       // #endif
@@ -516,6 +557,12 @@ export default {
       // 先进行标准Base64编码
       const base64 = this.base64Encode(str);
 
+      // 增加一个保护
+      if (typeof base64 !== 'string') {
+          console.error('Base64编码失败,返回非字符串值:', base64);
+          return ''; // 返回一个空字符串或其他默认值
+      }
+
       // 转换为URL安全格式:替换 +/= 字符
       return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
     },
@@ -639,6 +686,9 @@ export default {
           isMine = true;
         }
 
+        const messageType = messageData.messageType || "text";
+        console.log('messageType -- ', messageType);
+
         // 创建消息对象
         const newMessage = {
           id: 'msg_receive_' + Math.floor(Date.now() / 1000) + '_' + Math.floor(Math.random() * 100000) + 1,
@@ -647,16 +697,24 @@ export default {
           username: messageData.name || messageData.username || '未知用户',
           avatar: messageData.avatar || this.$constant.imgUrl + '/retail/default-img.png',
           time: this.getCurrentTime(),
+          messageType: messageType,
         };
 
+        if (newMessage.messageType === 'goods') {
+          newMessage.goodsInfo = this.parseGoodsMessage(newMessage.message);
+        }
+
         console.log('收到 ---------- newMessage: ', newMessage);
         // 添加到消息列表
         this.messageList.push(newMessage);
 
         // 滚动到底部
-        this.$nextTick(() => {
-          this.scrollToBottom(); // 用户操作时使用动画
-        });
+        // this.$nextTick(() => {
+        //   this.scrollToBottom(); // 用户操作时使用动画
+        // });
+        setTimeout(() => {
+          this.scrollToBottom();
+        }, 100);
       } catch (error) {
         console.error('解析WebSocket消息失败:', error, data);
       }
@@ -689,19 +747,20 @@ export default {
           username: this.currentUser.username,
           avatar: avatar,
           time: this.getCurrentTime(),
+          messageType: "text",
         };
 
         // 添加到消息列表
         this.messageList.push(myMessage);
         // 清空输入框
         this.inputText = '';
+
         // 重新聚焦
-        this.$nextTick(() => {
-          this.inputFocus = true
-        })
-        // 滚动到底部
         // this.$nextTick(() => {
-        //   this.scrollToBottom(); // 用户操作时使用动画
+        //   this.inputFocus = false
+        //   this.$nextTick(() => {
+        //     this.inputFocus = true
+        //   })
         // });
 
         // 构建要发送的消息数据
@@ -711,7 +770,8 @@ export default {
           username: this.currentUser.username,
           avatar: avatar,
           shopId: this.currentUser.shopId, // 发送者:门店使用shopId,客户使用customId
-          timestamp: timestamp
+          timestamp: timestamp,
+          messageType: "text",
         };
 
         const json = JSON.stringify(messageData);
@@ -731,6 +791,14 @@ export default {
             // 发送失败时,可以考虑移除刚添加的消息或标记为失败
           }
         });
+
+        // 滚动到底部
+        // this.$nextTick(() => {
+        //   this.scrollToBottom(); // 用户操作时使用动画
+        // });
+        setTimeout(() => {
+          this.scrollToBottom();
+        }, 280);
       } catch (error) {
         console.error('发送消息异常:', error);
         uni.showToast({
@@ -742,11 +810,12 @@ export default {
 
     // 处理连接错误
     handleConnectionError() {
-      uni.showToast({
-        title: '连接失败',
-        icon: 'none',
-        duration: 2000
-      });
+      console.log('连接失败--', this.reconnectCount);
+      // uni.showToast({
+      //   title: '连接失败',
+      //   icon: 'none',
+      //   duration: 2000
+      // });
     },
 
     // 滚动到底部
@@ -766,21 +835,21 @@ export default {
     },
     // 输入框聚焦事件
     onInputFocus() {
-      // console.log('输入框聚焦');
-      // // 微信小程序中,聚焦时延迟滚动到底部
-      // setTimeout(() => {
-      //   this.scrollToBottom();
-      // }, 300);
+      console.log('输入框聚焦');
+      // 微信小程序中,聚焦时延迟滚动到底部
+      setTimeout(() => {
+        this.scrollToBottom();
+      }, 450);
       this.isInputFocused = true
       this.inputFocus = true
-      this.hideExtraPanels() // 聚焦时隐藏表情和更多面板
+      //this.hideExtraPanels() // 聚焦时隐藏表情和更多面板
     },
 
     // 输入框失焦事件
     onInputBlur() {
-      // console.log('输入框失焦');
+      console.log('输入框失焦');
       this.isInputFocused = false
-      this.inputFocus = false
+      // this.inputFocus = false
     },
 
     // 输入内容变化事件
@@ -795,7 +864,31 @@ export default {
         this.inputText = lines.slice(0, 5).join('\n');
       }
     },
-  }
+    // 解析商品消息
+    parseGoodsMessage(messageStr) {
+      try {
+        // 首先检查 messageStr 是否已经是对象
+        if (typeof messageStr === "object" && messageStr !== null) {
+          return messageStr;
+        }
+        // 如果是字符串,则尝试解析
+        if (typeof messageStr === "string") {
+          const parsed = JSON.parse(messageStr);
+          return parsed;
+        }
+        // 如果两者都不是,返回空对象
+        return {};
+      } catch (error) {
+        // console.error("解析商品消息失败:", error, messageStr);
+        // 在解析失败时返回一个包含默认值的对象,以避免模板渲染错误
+        return {
+          goodsImg: "",
+          goodsName: "商品信息解析失败",
+          goodsPrice: "0.00",
+        };
+      }
+    },
+  },
 };
 </script>
 
@@ -930,6 +1023,124 @@ export default {
   }
 }
 
+.goods-info.right-bubble {
+  background-color: #fff !important;
+  color: #333;
+
+  &::before {
+    border-left-color: #fff !important;
+  }
+}
+
+.goods-info {
+  background-color: #fff !important;
+  color: #333;
+  padding: 20rpx;
+  border-radius: 16rpx;
+  &.right-bubble {
+    background-color: #fff !important; // 覆盖默认气泡颜色
+    &::before {
+      border-left-color: #fff; // 确保箭头颜色与背景一致
+    }
+  }
+  .goods-main {
+    display: flex;
+    position: relative;
+  }
+  .goods-image {
+    width: 180rpx;
+    height: 180rpx;
+    border-radius: 8rpx;
+    margin-right: 20rpx;
+    flex-shrink: 0;
+  }
+
+  .goods-details {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    .goods-name-wrapper {
+      height: 80rpx;
+    }
+    .goods-name {
+      font-size: 28rpx;
+      color: #333;
+      font-weight: bold;
+      // 多行省略
+      text-overflow: -o-ellipsis-lastline;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      display: -webkit-box;
+      -webkit-line-clamp: 2;
+      line-clamp: 2;
+      -webkit-box-orient: vertical;
+    }
+    .sales-info {
+      display: flex;
+      align-items: center;
+      margin-top: 10rpx;
+    }
+    .delivery-time {
+      background-color: #e8f5ff;
+      color: #007aff;
+      font-size: 20rpx;
+      padding: 4rpx 8rpx;
+      border-radius: 4rpx;
+    }
+    .goods-price {
+      font-size: 36rpx;
+      color: #ff5050;
+      font-weight: bold;
+      margin-top: 10rpx;
+    }
+  }
+
+  .goods-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 20rpx;
+    padding-top: 10rpx;
+    border-top: 1rpx solid #f5f5f5;
+  }
+
+  .action-buttons {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    margin-top: 20rpx;
+    .price-input {
+      width: 180rpx;
+      height: 60rpx;
+      border: 1px solid #666666;
+      border-radius: 8rpx;
+      padding: 0 15rpx;
+      font-size: 28rpx;
+      text-align: center;
+    }
+    .action-btn {
+      padding: 10rpx 30rpx;
+      border-radius: 40rpx;
+      font-size: 28rpx;
+      margin-left: 20rpx;
+      line-height: 1.5;
+      &::after {
+        border: none;
+      }
+    }
+    .spec-btn {
+      background-color: #f5f5ff;
+      color: #333;
+      border: 1px solid #eee;
+    }
+    .buy-btn {
+      background-color: #ff9800;
+      color: white;
+    }
+  }
+}
+
 .message-text {
   font-size: 32upx;
   line-height: 1.4;

+ 2 - 1
hdApp/src/admin/chat/list.vue

@@ -105,7 +105,8 @@ export default {
           staffId: this.getLoginInfo.shopAdminId,
           shopId: this.getLoginInfo.shopId,
           name: this.getLoginInfo.name,
-          avatar: this.shopAvatar
+          avatar: this.shopAvatar,
+          chatPerson: item.name
         }
       });
     },

+ 452 - 32
mallApp/src/pages/chat/chatPage.vue

@@ -2,9 +2,7 @@
   <view class="chat-container">
     <!-- 连接状态指示器 -->
     <view class="connection-status" v-if="!isConnected">
-      <text class="status-text">{{
-        reconnectCount > 0 ? "重连中..." : "连接中..."
-      }}</text>
+      <text class="status-text">"连接中...</text>
     </view>
 
     <!-- 聊天消息列表 -->
@@ -44,7 +42,7 @@
 
         <!-- 右侧消息(我的) -->
         <view class="message-right" v-if="message.isMine || message.customId">
-          <view class="message-content-wrapper">
+          <view v-if="message.messageType == 'text'|| message.messageType != 'goods'" class="message-content-wrapper">
             <view class="username align-right" v-if="isMultiUser">{{
               message.username
             }}</view>
@@ -53,6 +51,38 @@
             </view>
             <view class="message-time align-right">{{ message.time }}</view>
           </view>
+          <view v-if="message.messageType == 'goods'" class="message-content-wrapper">
+            <view class="username align-right" v-if="isMultiUser">{{
+              message.username
+            }}</view>
+            <view class="goods-info right-bubble">
+              <view class="goods-main">
+                <image
+                  class="goods-image"
+                  :src="message.goodsInfo.goodsImg"
+                  mode="aspectFill"
+                />
+                <view class="goods-details">
+                  <view class="goods-name-wrapper">
+                    <text class="goods-name">{{ message.goodsInfo.goodsName }}</text>
+                  </view>
+                  <view class="sales-info">
+                    <!-- <text class="delivery-time">48小时发</text> -->
+                  </view>
+                  <text v-if="message.goodsInfo.goodsPriceType == 1" class="goods-price">¥{{ message.goodsInfo.goodsPrice }}</text>
+                </view>
+              </view>
+              <view class="goods-footer">
+                <!-- <text class="footer-text">破损包退·极速退款·7天无理由退货</text>
+                <text class="arrow">></text> -->
+              </view>
+              <view class="action-buttons">
+                <button class="action-btn spec-btn" @click="toGoodsDetail(message.goodsInfo)">查看商品</button>
+                <button v-if="message.goodsInfo.goodsPriceType == 1" class="action-btn buy-btn" click="toBuy">去购买</button>
+              </view>
+            </view>
+            <view class="message-time align-right">{{ message.time }}</view>
+          </view>
           <view class="avatar-wrapper">
             <image class="avatar" :src="message.avatar" mode="aspectFill" />
           </view>
@@ -65,8 +95,32 @@
       </view>
     </scroll-view>
 
+    <!-- 商品信息展示 -->
+    <view class="goods-info-container" v-if="globalGoodsInfo.goodsId">
+      <view class="goods-info-card">
+        <image class="goods-image" :src="globalGoodsInfo.goodsImg" mode="aspectFill" />
+        <view class="goods-details">
+          <text class="goods-name">{{ globalGoodsInfo.goodsName }}</text>
+          <text class="goods-price">¥{{ globalGoodsInfo.goodsPrice }}</text>
+        </view>
+        <view class="goods-actions">
+          <view class="close-icon" @click="closeGoodsCard">
+            <zui-svg-icon icon="general-close" color="#666666" :width="18" :height="18" />
+          </view>
+          <button class="send-goods-button" @click="sendMessageOfGoodsInfo">
+            发送商品
+          </button>
+        </view>
+      </view>
+    </view>
+
     <!-- 输入区域 -->
-    <view class="input-area" :style="{ 'padding-bottom': keyboardHeight > 0 ? keyboardHeight + 'px' : 'calc(20upx + env(safe-area-inset-bottom))' }">
+    <view
+      class="input-area"
+      :style="{
+        'padding-bottom': keyboardHeight > 0 ? keyboardHeight + 'px' : 'calc(20upx + env(safe-area-inset-bottom))',
+      }"
+    >
       <view class="input-wrapper">
         <textarea
           class="message-input"
@@ -124,10 +178,10 @@ export default {
       messageList: [],
       // WebSocket相关
       socket: null,
-      isConnected: false,
+      isConnected: true,
       room: '',
       reconnectCount: 0,
-      maxReconnectCount: 3,
+      maxReconnectCount: 5,
       isManualDisconnect: false, // 添加标志位:是否主动断开连接
       scrollWithAnimation: false, // 控制滚动动画
       // 分页加载
@@ -138,10 +192,25 @@ export default {
       // 键盘相关
       keyboardHeight: 0, // 键盘高度
       isKeyboardShow: false, // 键盘是否显示
+      globalGoodsInfo: {}, // 商品信息
     };
   },
   onLoad(options) {
     console.log("---------- options: ", options);
+
+    if (options.goodsId) {
+      this.globalGoodsInfo = {
+        ...options
+      };
+    }
+
+    if (options.chatPerson) {
+      // 设置聊天人名称
+      uni.setNavigationBarTitle({
+        title: options.chatPerson
+      })
+    }
+
     // 根据传入参数判断是否多人聊天
     if (options.isMultiUser) {
       this.isMultiUser = options.isMultiUser === "true";
@@ -233,10 +302,18 @@ export default {
         userId: userId,
       }).then((res) => {
         if (res.code == 1 && res.data) {
-          this.allMessages = res.data.map((item, index) => ({
-            ...item,
-            id: `msg_${index}`, // 为每条消息添加唯一ID
-          }));
+          this.allMessages = res.data.map((item, index) => {
+            const newItem = {
+              ...item,
+              id: `message-${index}`, // 为每条消息添加唯一ID
+            };
+
+            //如果消息类型为 'goods',就调用 parseGoodsMessage 并将结果存入新属性 goodsInfo
+            if (newItem.messageType === 'goods') {
+              newItem.goodsInfo = this.parseGoodsMessage(newItem.message);
+            }
+            return newItem;
+          });
 
           if (this.allMessages.length > this.initialLoadSize) {
             // 如果消息总数超过首次加载数量,则只显示最新的50条
@@ -326,7 +403,6 @@ export default {
       try {
         // 构建WebSocket URL(移除查询参数)
         const wsUrl = `${this.getHost()}/room`;
-        console.log("---------- wsUrl: ", wsUrl);
 
         // 构建子协议参数(模拟POST body传输)
         const protocols = this.buildWebSocketProtocols();
@@ -336,7 +412,7 @@ export default {
           url: wsUrl,
           protocols: protocols,
           success: () => {
-            console.log("WebSocket连接创建成功");
+            console.log("创建WebSocket连接...");
           },
           fail: (error) => {
             console.error("WebSocket连接创建失败:", error);
@@ -346,7 +422,7 @@ export default {
 
         // 监听WebSocket连接打开
         this.socket.onOpen(() => {
-          console.log("WebSocket连接已打开");
+          console.log("成功连接 WebSocket");
           this.isConnected = true;
           this.reconnectCount = 0;
           this.isManualDisconnect = false; // 连接成功时重置标志位
@@ -360,17 +436,18 @@ export default {
         // 监听WebSocket连接关闭
         this.socket.onClose(() => {
           console.log("WebSocket连接已关闭");
-          this.isConnected = false;
 
           // 只有在非主动断开的情况下才尝试重连
-          if (
-            !this.isManualDisconnect &&
-            this.reconnectCount < this.maxReconnectCount
-          ) {
+          if (!this.isManualDisconnect && this.reconnectCount < this.maxReconnectCount) {
             setTimeout(() => {
               this.reconnectWebSocket();
             }, 3000);
           }
+
+          // 尝试重新连接超过 maxReconnectCount 次才标记为未连接状态
+          if (!this.isManualDisconnect && this.reconnectCount >= this.maxReconnectCount) {
+            this.isConnected = false;
+          }
         });
 
         // 监听WebSocket错误
@@ -504,7 +581,7 @@ export default {
       }
       // #endif
 
-      // #ifdef MP-WEIXIN || MP-ALIPAY || APP-PLUS
+      // #ifndef H5
       // 小程序和App环境使用自定义编码
       return this.customBase64Encode(str);
       // #endif
@@ -671,6 +748,9 @@ export default {
         //   return;
         // }
 
+        const messageType = messageData.messageType || "text";
+        console.log('messageType -- ', messageType);
+
         // 创建消息对象
         const newMessage = {
           id: "msg_receive_" + Math.floor(Date.now() / 1000) + "_" + Math.floor(Math.random() * 100000) + 1,
@@ -678,19 +758,25 @@ export default {
           time: this.getCurrentTime(),
           isMine: false, // 既然不是自己的消息,就标记为false
           username: messageData.name || messageData.username || "未知用户",
-          avatar:
-            messageData.avatar ||
-            this.$constant.imgUrl + "/retail/default-img.png",
+          avatar: messageData.avatar || this.$constant.imgUrl + "/retail/default-img.png",
+          messageType: messageType,
         };
 
+        if (newMessage.messageType === 'goods') {
+          newMessage.goodsInfo = this.parseGoodsMessage(newMessage.message);
+        }
+
         console.log("收到 ---------- newMessage: ", newMessage);
         // 添加到消息列表
         this.messageList.push(newMessage);
 
         // 滚动到底部
-        this.$nextTick(() => {
+        // this.$nextTick(() => {
+        //   this.scrollToBottom();
+        // });
+        setTimeout(() => {
           this.scrollToBottom();
-        });
+        }, 100);
       } catch (error) {
         console.error("解析WebSocket消息失败:", error, data);
       }
@@ -723,6 +809,7 @@ export default {
           username: this.currentUser.username,
           avatar: avatar,
           time: this.getCurrentTime(),
+          messageType: "text",
         };
 
         // 添加到消息列表
@@ -730,9 +817,12 @@ export default {
         // 清空输入框
         this.inputText = "";
         // 滚动到底部
-        this.$nextTick(() => {
-          this.scrollToBottom(); // 用户操作时使用动画
-        });
+        // this.$nextTick(() => {
+        //   this.scrollToBottom(); // 用户操作时使用动画
+        // });
+        setTimeout(() => {
+          this.scrollToBottom();
+        }, 100);
 
         // 构建要发送的消息数据
         const messageData = {
@@ -741,7 +831,8 @@ export default {
           username: this.currentUser.username,
           avatar: avatar,
           customId: this.currentUser.customId, // 发送者:客户使用customId (门店使用shopId)
-          timestamp: timestamp
+          timestamp: timestamp,
+          messageType: "text"
         };
 
         const json = JSON.stringify(messageData);
@@ -803,7 +894,7 @@ export default {
       // 微信小程序中,聚焦时延迟滚动到底部
       setTimeout(() => {
         this.scrollToBottom();
-      }, 300);
+      }, 400);
     },
 
     // 输入框失焦事件
@@ -822,6 +913,141 @@ export default {
         this.inputText = lines.slice(0, 5).join('\n');
       }
     },
+    // 关闭商品信息卡片
+    closeGoodsCard() {
+      this.globalGoodsInfo = {};
+    },
+
+    // 发送商品信息 -- sendMessageOfGoodsInfo
+    sendMessageOfGoodsInfo() {
+      if (!this.isConnected) {
+        uni.showToast({
+          title: "WebSocket未连接",
+          icon: "none",
+        });
+        return;
+      }
+
+      // 构建商品信息消息体 -------------------------------- 所看到的消息体 -------------------------------
+      const goodsMessage = {
+        ...this.globalGoodsInfo,
+      };
+
+      const messageContent = JSON.stringify(goodsMessage); // 所看到的消息体
+      const avatar = this.currentUser.avatar || this.$constant.imgUrl + "/retail/default-img.png";
+      const timestamp = Math.floor(Date.now() / 1000);
+
+      try {
+        // 立即在本地显示发送的消息(右侧)
+        const myMessage = {
+          id: "msg_local_" + timestamp + "_" + (Math.floor(Math.random() * 100000) + 1),
+          isMine: true,
+          // message: `[商品] ${this.goodsInfo.goodsName}`, // 显示简化信息
+          message: messageContent, // 显示简化信息
+          username: this.currentUser.username,
+          avatar: avatar,
+          time: this.getCurrentTime(),
+          // 可以在这里附加一个字段,用于渲染时区分商品消息
+          messageType: "goods",
+          goodsInfo: goodsMessage,
+        };
+
+        // 添加到消息列表
+        this.messageList.push(myMessage);
+        // 清空商品信息并关闭卡片
+        this.closeGoodsCard();
+        // 滚动到底部
+        // this.$nextTick(() => {
+        //   this.scrollToBottom();
+        // });
+        setTimeout(() => {
+          this.scrollToBottom();
+        }, 100);
+
+        // 构建要发送的完整消息数据
+        const messageData = {
+          receiver: this.currentUser.shopId,
+          message: messageContent,
+          username: this.currentUser.username,
+          avatar: avatar,
+          customId: this.currentUser.customId,
+          timestamp: timestamp,
+          messageType: "goods",
+        };
+
+        const json = JSON.stringify(messageData);
+        // 发送到WebSocket服务器
+        this.socket.send({
+          data: json,
+          success: () => {
+            console.log("商品消息发送成功");
+          },
+          fail: (error) => {
+            console.error("商品消息发送失败:", error);
+            uni.showToast({
+              title: "发送失败",
+              icon: "none",
+            });
+          },
+        });
+      } catch (error) {
+        console.error("发送商品消息异常:", error);
+        uni.showToast({
+          title: "发送异常",
+          icon: "none",
+        });
+      }
+    },
+
+    // 解析商品消息
+    parseGoodsMessage(messageStr) {
+      try {
+        // 首先检查 messageStr 是否已经是对象
+        if (typeof messageStr === "object" && messageStr !== null) {
+          return messageStr;
+        }
+        // 如果是字符串,则尝试解析
+        if (typeof messageStr === "string") {
+          const parsed = JSON.parse(messageStr);
+          return parsed;
+        }
+        // 如果两者都不是,返回空对象
+        return {};
+      } catch (error) {
+        // console.error("解析商品消息失败:", error, messageStr);
+        // 在解析失败时返回一个包含默认值的对象,以避免模板渲染错误
+        return {
+          goodsImg: "",
+          goodsName: "商品信息解析失败",
+          goodsPrice: "0.00",
+        };
+      }
+    },
+    // 查看商品
+    toGoodsDetail(goodsInfo) {
+      console.log("---------- goodsInfo: ", goodsInfo);
+      const goodsId = parseInt(goodsInfo.goodsId);
+      const categoryId = parseInt(goodsInfo.categoryId);
+      const hdId = parseInt(goodsInfo.hdId);
+      const account = parseInt(goodsInfo.account);
+      if (isNaN(goodsId) || isNaN(categoryId) || isNaN(hdId) || isNaN(account)) {
+        this.$msg("商品数据异常");
+        return;
+      }
+
+      this.$util.pageTo({
+        url: "/pages/goods/detail",
+        query: { // id=2020&categoryId=1402&hdId=229&account=36548&shopId=36548&name=花仙子&avatar=https://img.theflorist.cn/hhb_small.png
+          id: goodsId,
+          account: goodsInfo.account,
+          hdId: goodsInfo.hdId,
+          categoryId: categoryId,
+          shopId: goodsInfo.shopId,
+          name: goodsInfo.name,
+          avatar: goodsInfo.avatar,
+        },
+      });
+    },
   },
 };
 </script>
@@ -832,6 +1058,84 @@ export default {
   display: flex;
   flex-direction: column;
   background-color: #f5f5f5;
+  position: relative; // 确保 goods-info-card 的定位上下文
+}
+
+.goods-info-container {
+  position: absolute;
+  bottom: 180upx; // 调整位置以适应输入框
+  left: 20upx;
+  right: 20upx;
+  z-index: 10000;
+}
+
+.goods-info-card {
+  display: flex;
+  align-items: center;
+  background-color: #ffffff;
+  padding: 20upx;
+  border-radius: 16upx;
+  box-shadow: 0 4upx 20upx rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
+
+  .goods-image {
+    width: 120upx;
+    height: 120upx;
+    border-radius: 8upx;
+    margin-right: 20upx;
+    flex-shrink: 0;
+  }
+
+  .goods-details {
+    flex: 1;
+    min-width: 0; // 允许 flex 子元素收缩
+    margin-right: 20upx;
+    display: flex;
+    flex-direction: column;
+
+    .goods-name {
+      font-size: 36upx;
+      color: #333;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .goods-price {
+      font-size: 30upx;
+      font-weight: bold;
+      color: #ff5050;
+      margin-top: 4upx;
+    }
+  }
+
+  .goods-actions {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+
+    .close-icon {
+      margin-bottom: 25upx; // 在关闭按钮和发送按钮之间添加一些间距
+      padding: 8upx;
+      color: #999;
+    }
+
+    .send-goods-button {
+      background-color: #ff9800;
+      color: #ffffff;
+      border: none;
+      border-radius: 40upx;
+      font-size: 26upx;
+      padding: 0 30upx;
+      height: 60upx;
+      line-height: 60upx;
+      white-space: nowrap;
+
+      &::after {
+        border: none;
+      }
+    }
+  }
 }
 
 .connection-status {
@@ -930,7 +1234,7 @@ export default {
       height: 0;
       border: 12upx solid transparent;
       border-right-color: #ffffff;
-      z-index: 999999;
+      z-index: 9999;
     }
   }
 
@@ -948,7 +1252,7 @@ export default {
       height: 0;
       border: 12upx solid transparent;
       border-left-color: rgb(27, 193, 66);
-      z-index: 999999;
+      z-index: 9999;
     }
 
     .message-text {
@@ -957,6 +1261,122 @@ export default {
   }
 }
 
+.goods-info.right-bubble {
+  background-color: #fff !important;
+  color: #333;
+
+  &::before {
+    border-left-color: #fff !important;
+  }
+}
+
+.goods-info {
+  background-color: #fff !important;
+  color: #333;
+  padding: 20rpx;
+  border-radius: 16rpx;
+  &.right-bubble {
+    background-color: #fff !important; // 覆盖默认气泡颜色
+    &::before {
+      border-left-color: #fff; // 确保箭头颜色与背景一致
+    }
+  }
+  .goods-main {
+    display: flex;
+    position: relative;
+  }
+  .goods-image {
+    width: 180rpx;
+    height: 180rpx;
+    border-radius: 8rpx;
+    margin-right: 20rpx;
+    flex-shrink: 0;
+  }
+
+  .goods-details {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    .goods-name-wrapper {
+      height: 80rpx;
+    }
+    .goods-name {
+      font-size: 28rpx;
+      color: #333;
+      font-weight: bold;
+      // 多行省略
+      text-overflow: -o-ellipsis-lastline;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      display: -webkit-box;
+      -webkit-line-clamp: 2;
+      line-clamp: 2;
+      -webkit-box-orient: vertical;
+    }
+    .sales-info {
+      display: flex;
+      align-items: center;
+      margin-top: 10rpx;
+    }
+    .delivery-time {
+      background-color: #e8f5ff;
+      color: #007aff;
+      font-size: 20rpx;
+      padding: 4rpx 8rpx;
+      border-radius: 4rpx;
+    }
+    .goods-price {
+      font-size: 36rpx;
+      color: #ff5050;
+      font-weight: bold;
+      margin-top: 10rpx;
+    }
+  }
+
+  .goods-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 20rpx;
+    padding-top: 10rpx;
+    border-top: 1rpx solid #f5f5f5;
+    .footer-text {
+      font-size: 24rpx;
+      color: #999;
+    }
+    .arrow {
+      font-size: 24rpx;
+      color: #999;
+    }
+  }
+
+  .action-buttons {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 20rpx;
+    .action-btn {
+      padding: 10rpx 30rpx;
+      border-radius: 40rpx;
+      font-size: 28rpx;
+      margin-left: 20rpx;
+      line-height: 1.5;
+      &::after {
+        border: none;
+      }
+    }
+    .spec-btn {
+      background-color: #f5f5ff;
+      color: #333;
+      border: 1px solid #eee;
+    }
+    .buy-btn {
+      background-color: #ff9800;
+      color: white;
+    }
+  }
+}
+
 .message-text {
   font-size: 32upx;
   line-height: 1.4;

+ 1 - 0
mallApp/src/pages/chat/list.vue

@@ -109,6 +109,7 @@ export default {
           shopId: item.id,
           name: this.getLoginInfo.name,
           avatar: this.getLoginInfo.avatar,
+          chatPerson: item.shopName != "首店" ? item.shopName : item.merchantName || item.mobile
         },
       });
     },

+ 116 - 76
mallApp/src/pages/goods/detail.vue

@@ -1,9 +1,5 @@
 <template>
-  <scroll-view 
-    class="app-content" 
-    scroll-y="true" 
-    @scroll="scrollFn"
-  >
+  <scroll-view class="app-content" scroll-y="true" @scroll="scrollFn">
     <app-swiper
       :height="{ type: 'number', val: 750 }"
       :tagShow="true"
@@ -18,14 +14,10 @@
       </view>
       <view class="tui-pro-titbox">
         <view class="tui-pro-title app-size-32">{{ data.name }}</view>
-                 <button
-           open-type="share"
-           class="share-button"
-           @click="shareFn"
-         >
-           <i class="iconfont iconfenxiang"></i>
-           <text class="share-text">分享</text>
-         </button>
+        <button open-type="share" class="share-button" @click="shareFn">
+          <i class="iconfont iconfenxiang"></i>
+          <text class="share-text">分享</text>
+        </button>
       </view>
       <view class="shop-express app-color-3">
         <view>线下门店</view>
@@ -84,7 +76,13 @@
       @change="changeNum"
       @close="hidePopup"
     />
-    <buy-foot @buy="showPopup" @goShop="goToShop" :stock="data.stock" :priceType="data.priceType" @goChat="goChat" />
+    <buy-foot
+      @buy="showPopup"
+      @goShop="goToShop"
+      :stock="data.stock"
+      :priceType="data.priceType"
+      @goChat="goChat"
+    />
   </scroll-view>
 </template>
 <script>
@@ -108,13 +106,13 @@ export default {
   },
   data() {
     return {
-      data: {},
+      data: {}, //商品数据
       imgList: [],
       buyNum: 1,
       popupShow: false,
       hdId: 0,
-      account:0,
-      customId:0,
+      account: 0,
+      customId: 0,
       title: "",
       previewContent: [],
       common_title: "",
@@ -124,20 +122,24 @@ export default {
   },
   onLoad(option) {
     this.hdId = this.option.hdId ? this.option.hdId : 0;
-    this.account = this.option.account?this.option.account:0
-    if (option.shopId) {
+    this.account = this.option.account ? this.option.account : 0;
+    if (option.shopId || option.goodsId) {
       console.log("---------- option: ", option);
-      this.shopInfo = option
+      this.shopInfo = option;
     }
   },
   onPageScroll(e) {
     // console.log('Scroll position:', parseInt(e.scrollTop))
     // 通过 ref 调用子组件的方法
     if (this.$refs.richMediaViewerGoods) {
-      this.$refs.richMediaViewerGoods.triggerCheckVisibility(parseInt(e.scrollTop));
+      this.$refs.richMediaViewerGoods.triggerCheckVisibility(
+        parseInt(e.scrollTop)
+      );
     }
     if (this.$refs.richMediaViewerCommon) {
-      this.$refs.richMediaViewerCommon.triggerCheckVisibility(parseInt(e.scrollTop));
+      this.$refs.richMediaViewerCommon.triggerCheckVisibility(
+        parseInt(e.scrollTop)
+      );
     }
   },
   computed: {
@@ -151,19 +153,39 @@ export default {
       // console.log('Scroll position:', parseInt(e.detail.scrollTop))
       // 通过 ref 调用子组件的方法
       if (this.$refs.richMediaViewerGoods) {
-        this.$refs.richMediaViewerGoods.triggerCheckVisibility(parseInt(e.detail.scrollTop));
+        this.$refs.richMediaViewerGoods.triggerCheckVisibility(
+          parseInt(e.detail.scrollTop)
+        );
       }
       if (this.$refs.richMediaViewerCommon) {
-        this.$refs.richMediaViewerCommon.triggerCheckVisibility(parseInt(e.detail.scrollTop));
+        this.$refs.richMediaViewerCommon.triggerCheckVisibility(
+          parseInt(e.detail.scrollTop)
+        );
       }
     },
     getGoodsInfo() {
-       getDetail({
-        id: this.option.id,
-        categoryId: this.option.categoryId,
+      const id = parseInt(this.option.id);
+      if (isNaN(id) || id <= 0) {
+        this.$msg("商品id数据异常");
+        return;
+      }
+
+      const categoryId = parseInt(this.option.categoryId);
+      if (isNaN(categoryId) || categoryId <= 0) {
+        this.$msg("商品分类数据异常");
+        return;
+      }
+
+      getDetail({
+        id: id,
+        categoryId: categoryId,
       }).then((res) => {
         // console.log('商品数据加载成功:', res.data)
-        if (this.$util.isEmpty(res.data)) return false;
+        if (this.$util.isEmpty(res.data)) {
+          this.$msg("商品数据异常");
+          return false;
+        }
+
         let newArr = [];
         for (let i in res.data.imgList) {
           newArr.push({ img: res.data.imgList[i] });
@@ -174,15 +196,17 @@ export default {
         this.customId = res.data.customId;
 
         //商品描述
-        this.title = res.data.picTextGoods && res.data.picTextGoods.title || "";
+        this.title =
+          (res.data.picTextGoods && res.data.picTextGoods.title) || "";
         // 字符串转换为数组
-        let content = res.data.picTextGoods && res.data.picTextGoods.content || "[]";
+        let content =
+          (res.data.picTextGoods && res.data.picTextGoods.content) || "[]";
         this.previewContent =
           typeof content === "string" ? JSON.parse(content) : content || [];
 
         //通用说明
-        this.common_title = res.data.picText && res.data.picText.title || "";
-        content = res.data.picText && res.data.picText.content || "[]";
+        this.common_title = (res.data.picText && res.data.picText.title) || "";
+        content = (res.data.picText && res.data.picText.content) || "[]";
         this.common_previewContent =
           typeof content === "string" ? JSON.parse(content) : content || [];
       });
@@ -211,12 +235,12 @@ export default {
       this.showShare = true;
       // #endif
     },
-    goChat(){
-      if(isNaN(parseInt(this.loginInfo.id))){
+    goChat() {
+      if (isNaN(parseInt(this.loginInfo.id))) {
         this.$msg("登录用户数据异常,请稍后再试");
         return;
       }
-      if(isNaN(parseInt(this.customId))){
+      if (isNaN(parseInt(this.customId))) {
         this.$msg("客户数据异常,请稍后再试");
         return;
       }
@@ -224,16 +248,32 @@ export default {
       //   this.$msg("门店员工数据异常,请稍后再试");
       //   return;
       // }
-      if(isNaN(parseInt(this.shopInfo.shopId))){
+      if (isNaN(parseInt(this.shopInfo.shopId))) {
         this.$msg("门店数据异常,请稍后再试");
         return;
       }
 
       this.$util.pageTo({
         url: "/pages/chat/chatPage",
-        query: { userId: this.loginInfo.id, customId: this.customId, staffId: 0, shopId: this.shopInfo.shopId, name: this.loginInfo.name, avatar: this.loginInfo.avatar }
+        query: {
+          userId: this.loginInfo.id,
+          customId: this.customId,
+          staffId: 0,
+          shopId: this.shopInfo.shopId,
+          name: this.loginInfo.name,
+          avatar: this.loginInfo.avatar,
+          chatPerson: this.shopInfo.name,
+          goodsId: this.data.id, // 商品id
+          goodsName: this.data.name,
+          goodsPriceType: this.data.priceType,
+          goodsPrice: this.data.price,
+          goodsImg: this.data.cover,
+          account: this.shopInfo.account,
+          hdId: this.shopInfo.hdId,
+          categoryId: this.shopInfo.categoryId,
+        },
       });
-    }
+    },
   },
 };
 </script>
@@ -289,44 +329,44 @@ export default {
       .tui-pro-title {
         width: 80%;
       }
-             .share-button {
-         display: flex;
-         align-items: center;
-         justify-content: center;
-         width: 168upx;
-         height: 84upx;
-         background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
-         border-radius: 12upx;
-         border: 1upx solid #e1e5e9;
-         box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.08);
-         transition: all 0.3s ease;
-         margin: 0;
-         padding: 0;
-         
-                   &:active {
-            background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
-            box-shadow: 0 1upx 4upx rgba(0, 0, 0, 0.12);
-          }
-         
-         .iconfont {
-           font-size: 32upx;
-           color: #4a5568;
-           text-shadow: 0 1upx 2upx rgba(255, 255, 255, 0.8);
-           margin-right: 8upx;
-           display: flex;
-           align-items: center;
-           line-height: 1;
-         }
-         
-         .share-text {
-           font-size: 28upx;
-           color: #4a5568;
-           font-weight: 500;
-           display: flex;
-           align-items: center;
-           line-height: 1;
-         }
-       }
+      .share-button {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 168upx;
+        height: 84upx;
+        background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
+        border-radius: 12upx;
+        border: 1upx solid #e1e5e9;
+        box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.08);
+        transition: all 0.3s ease;
+        margin: 0;
+        padding: 0;
+
+        &:active {
+          background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+          box-shadow: 0 1upx 4upx rgba(0, 0, 0, 0.12);
+        }
+
+        .iconfont {
+          font-size: 32upx;
+          color: #4a5568;
+          text-shadow: 0 1upx 2upx rgba(255, 255, 255, 0.8);
+          margin-right: 8upx;
+          display: flex;
+          align-items: center;
+          line-height: 1;
+        }
+
+        .share-text {
+          font-size: 28upx;
+          color: #4a5568;
+          font-weight: 500;
+          display: flex;
+          align-items: center;
+          line-height: 1;
+        }
+      }
     }
     .shop-express {
       padding-right: 30upx;

Разница между файлами не показана из-за своего большого размера
+ 433 - 370
mallApp/src/pages/home/mall.vue


Некоторые файлы не были показаны из-за большого количества измененных файлов