Pārlūkot izejas kodu

客服:聊天消息区域抽离封装

puhui999 1 gadu atpakaļ
vecāks
revīzija
760dad0436

+ 4 - 1
.env

@@ -5,12 +5,15 @@ SHOPRO_VERSION = v1.8.3
 SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
 
 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
-SHOPRO_DEV_BASE_URL = http://127.0.0.1:48080
+SHOPRO_DEV_BASE_URL = http://192.168.1.105:48080
 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
 
 # 后端接口前缀(一般不建议调整)
 SHOPRO_API_PATH = /app-api
 
+# 后端 websocket 接口前缀
+SHOPRO_WEBSOCKET_PATH = /infra/ws
+
 # 开发环境运行端口
 SHOPRO_DEV_PORT = 3000
 

+ 456 - 0
pages/chat/components/chatBox.vue

@@ -0,0 +1,456 @@
+<template>
+  <view class="chat-box" :style="{ height: pageHeight + 'px' }">
+    <!--  竖向滚动区域需要设置固定 height  -->
+    <scroll-view
+      :style="{ height: pageHeight + 'px' }"
+      scroll-y="true"
+      :scroll-with-animation="false"
+      :enable-back-to-top="true"
+      :scroll-into-view="state.scrollInto"
+    >
+      <!--  消息渲染  -->
+      <view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">
+        <view class="ss-flex ss-row-center ss-col-center">
+          <!-- 日期 -->
+          <view v-if="item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)"
+                class="date-message">
+            {{ formatDate(item.date) }}
+          </view>
+          <!-- 系统消息 -->
+          <view v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM" class="system-message">
+            {{ item.content }}
+          </view>
+        </view>
+        <!-- 消息体渲染管理员消息和用户消息并左右展示  -->
+        <view
+          v-if="item.contentType !== KeFuMessageContentTypeEnum.SYSTEM"
+          class="ss-flex ss-col-top"
+          :class="[
+              item.senderType === UserTypeEnum.ADMIN
+                ? `ss-row-left`
+                : item.senderType === UserTypeEnum.MEMBER
+                ? `ss-row-right`
+                : '',
+            ]"
+        >
+          <!-- 客服头像 -->
+          <image
+            v-show="item.senderType === UserTypeEnum.ADMIN"
+            class="chat-avatar ss-m-r-24"
+            :src="
+                sheep.$url.cdn(item?.senderAvatar) ||
+                sheep.$url.static('/static/img/shop/chat/default.png')
+              "
+            mode="aspectFill"
+          ></image>
+
+          <!-- 发送状态 -->
+          <span
+            v-if="
+                item.senderType === UserTypeEnum.MEMBER &&
+                index == chatList.length - 1 &&
+                isSendSuccess !== 0
+              "
+            class="send-status"
+          >
+              <image
+                v-if="isSendSuccess == -1"
+                class="loading"
+                :src="sheep.$url.static('/static/img/shop/chat/loading.png')"
+                mode="aspectFill"
+              ></image>
+            <!-- <image
+              v-if="chatData.isSendSuccess == 1"
+              class="warning"
+              :src="sheep.$url.static('/static/img/shop/chat/warning.png')"
+              mode="aspectFill"
+              @click="onAgainSendMessage(item)"
+            ></image> -->
+            </span>
+
+          <!-- 内容 -->
+          <template v-if="item.contentType === KeFuMessageContentTypeEnum.TEXT">
+            <view class="message-box" :class="{'admin': item.senderType === UserTypeEnum.ADMIN}">
+              <mp-html :content="replaceEmoji(item.content)" />
+            </view>
+          </template>
+          <template v-if="item.contentType === KeFuMessageContentTypeEnum.IMAGE">
+            <view class="message-box" :class="{'admin': item.senderType === UserTypeEnum.ADMIN}" :style="{ width: '200rpx' }">
+              <su-image
+                class="message-img"
+                isPreview
+                :previewList="[sheep.$url.cdn(item.content)]"
+                :current="0"
+                :src="sheep.$url.cdn(item.content)"
+                :height="200"
+                :width="200"
+                mode="aspectFill"
+              ></su-image>
+            </view>
+          </template>
+          <template v-if="item.contentType === KeFuMessageContentTypeEnum.PRODUCT">
+            <GoodsItem
+              :goodsData="item.content.item"
+              @tap="
+                  sheep.$router.go('/pages/goods/index', {
+                    id: item.content.item.id,
+                  })
+                "
+            />
+          </template>
+          <template v-if="item.contentType === KeFuMessageContentTypeEnum.ORDER">
+            <OrderItem
+              from="msg"
+              :orderData="item.content.item"
+              @tap="
+                  sheep.$router.go('/pages/order/detail', {
+                    id: item.content.item.id,
+                  })
+                "
+            />
+          </template>
+          <!-- user头像 -->
+          <image
+            v-if="item.senderType === UserTypeEnum.MEMBER"
+            class="chat-avatar ss-m-l-24"
+            :src="sheep.$url.cdn(item?.senderAvatar) ||
+                sheep.$url.static('/static/img/shop/chat/default.png')"
+            mode="aspectFill"
+          >
+          </image>
+        </view>
+      </view>
+      <!-- 视图滚动锚点  -->
+      <view id="scrollBottom"></view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import OrderItem from '@/pages/chat/components/order.vue';
+  import GoodsItem from '@/pages/chat/components/goods.vue';
+  import { reactive, ref, unref } from 'vue';
+  import { formatDate } from '@/sheep/util';
+  import dayjs from 'dayjs';
+  import { KeFuMessageContentTypeEnum,UserTypeEnum } from './constants';
+  import { emojiList } from '@/pages/chat/emoji';
+
+  const KEFU_MESSAGE_TYPE = 'kefu_message_type'; // 客服消息类型
+  const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
+  const pageHeight = safeArea.height - 44 - 35 - 50;
+  const state = reactive({
+    scrollInto: '',
+  });
+
+  const chatList = [
+    {
+      id: 1,
+      conversationId: 1001,
+      senderId: 1,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 2,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 1, // KeFuMessageContentTypeEnum.TEXT
+      content: "Hello, how are you?",
+      readStatus: false
+    },
+    {
+      id: 2,
+      conversationId: 1001,
+      senderId: 2,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 1,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 1, // KeFuMessageContentTypeEnum.TEXT
+      content: "I'm good, thanks! [流泪][流泪][流泪][流泪]",
+      readStatus: false
+    },
+    {
+      id: 3,
+      conversationId: 1002,
+      senderId: 3,
+      senderType: 2, // UserTypeEnum.ADMIN
+      receiverId: 4,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 2, // KeFuMessageContentTypeEnum.IMAGE
+      content: "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg",
+      readStatus: true
+    },
+    {
+      id: 4,
+      conversationId: 1002,
+      senderId: 4,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 3,
+      receiverType: 2, // UserTypeEnum.ADMIN
+      contentType: 1, // KeFuMessageContentTypeEnum.TEXT
+      content: "This is a text message.",
+      readStatus: false
+    },
+    {
+      id: 5,
+      conversationId: 1003,
+      senderId: 5,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 6,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 3, // KeFuMessageContentTypeEnum.VOICE
+      content: "Voice content here",
+      readStatus: true
+    },
+    {
+      id: 6,
+      conversationId: 1003,
+      senderId: 6,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 5,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 1, // KeFuMessageContentTypeEnum.TEXT
+      content: "Another text message.",
+      readStatus: false
+    },
+    {
+      id: 7,
+      conversationId: 1004,
+      senderId: 7,
+      senderType: 2, // UserTypeEnum.ADMIN
+      receiverId: 8,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 1, // KeFuMessageContentTypeEnum.VIDEO
+      content: "Video content here",
+      readStatus: true
+    },
+    {
+      id: 8,
+      conversationId: 1004,
+      senderId: 8,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 7,
+      receiverType: 2, // UserTypeEnum.ADMIN
+      contentType: 5, // KeFuMessageContentTypeEnum.SYSTEM
+      content: "System message content",
+      readStatus: false
+    },
+    {
+      id: 9,
+      conversationId: 1005,
+      senderId: 9,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 10,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 10, // KeFuMessageContentTypeEnum.PRODUCT
+      content: "Product message content",
+      readStatus: true
+    },
+    {
+      id: 10,
+      conversationId: 1005,
+      senderId: 10,
+      senderType: 1, // UserTypeEnum.MEMBER
+      receiverId: 9,
+      receiverType: 1, // UserTypeEnum.MEMBER
+      contentType: 11, // KeFuMessageContentTypeEnum.ORDER
+      content: "Order message content",
+      readStatus: false
+    }
+  ];
+
+  const isSendSuccess = ref(-1)
+  //======================= 工具函数 =======================
+  /**
+   * 是否显示时间
+   * @param {*} item - 数据
+   * @param {*} index - 索引
+   */
+  const showTime = (item, index) => {
+    if (unref(chatList)[index + 1]) {
+      let dateString = dayjs(unref(chatList)[index + 1].date).fromNow();
+      return dateString !== dayjs(unref(item).date).fromNow();
+    }
+    return false;
+  };
+  // 处理表情
+  function replaceEmoji(data) {
+    let newData = data;
+    if (typeof newData !== 'object') {
+      let reg = /\[(.+?)\]/g; // [] 中括号
+      let zhEmojiName = newData.match(reg);
+      if (zhEmojiName) {
+        zhEmojiName.forEach((item) => {
+          let emojiFile = selEmojiFile(item);
+          newData = newData.replace(
+            item,
+            `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
+              '/static/img/chat/emoji/' + emojiFile,
+            )}"/>`,
+          );
+        });
+      }
+    }
+    return newData;
+  }
+  function selEmojiFile(name) {
+    for (let index in emojiList) {
+      if (emojiList[index].name === name) {
+        return emojiList[index].file;
+      }
+    }
+    return false;
+  }
+</script>
+
+<style scoped lang="scss">
+  .chat-box {
+    padding: 0 20rpx 0;
+
+    .loadmore-btn {
+      width: 98%;
+      height: 40px;
+      font-size: 12px;
+      color: #8c8c8c;
+
+      .loadmore-icon {
+        transform: rotate(90deg);
+      }
+    }
+
+    .message-item {
+      margin-bottom: 33rpx;
+    }
+
+    .date-message,
+    .system-message {
+      width: fit-content;
+      border-radius: 12rpx;
+      padding: 8rpx 16rpx;
+      margin-bottom: 16rpx;
+      background-color: var(--ui-BG-3);
+      color: #999;
+      font-size: 24rpx;
+    }
+
+    .chat-avatar {
+      width: 70rpx;
+      height: 70rpx;
+      border-radius: 50%;
+    }
+
+    .send-status {
+      color: #333;
+      height: 80rpx;
+      margin-right: 8rpx;
+      display: flex;
+      align-items: center;
+
+      .loading {
+        width: 32rpx;
+        height: 32rpx;
+        -webkit-animation: rotating 2s linear infinite;
+        animation: rotating 2s linear infinite;
+
+        @-webkit-keyframes rotating {
+          0% {
+            transform: rotateZ(0);
+          }
+
+          100% {
+            transform: rotateZ(360deg);
+          }
+        }
+
+        @keyframes rotating {
+          0% {
+            transform: rotateZ(0);
+          }
+
+          100% {
+            transform: rotateZ(360deg);
+          }
+        }
+      }
+
+      .warning {
+        width: 32rpx;
+        height: 32rpx;
+        color: #ff3000;
+      }
+    }
+
+    .message-box {
+      max-width: 50%;
+      font-size: 16px;
+      line-height: 20px;
+      // max-width: 500rpx;
+      white-space: normal;
+      word-break: break-all;
+      word-wrap: break-word;
+      padding: 20rpx;
+      border-radius: 10rpx;
+      color: #fff;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+
+      &.admin {
+        background: #fff;
+        color: #333;
+      }
+
+      :deep() {
+        .imgred {
+          width: 100%;
+        }
+
+        .imgred,
+        img {
+          width: 100%;
+        }
+      }
+    }
+
+    :deep() {
+      .goods,
+      .order {
+        max-width: 500rpx;
+      }
+    }
+
+    .message-img {
+      width: 100px;
+      height: 100px;
+      border-radius: 6rpx;
+    }
+
+    .template-wrap {
+      // width: 100%;
+      padding: 20rpx 24rpx;
+      background: #fff;
+      border-radius: 10rpx;
+
+      .title {
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #333;
+        margin-bottom: 29rpx;
+      }
+
+      .item {
+        font-size: 24rpx;
+        color: var(--ui-BG-Main);
+        margin-bottom: 16rpx;
+
+        &:last-of-type {
+          margin-bottom: 0;
+        }
+      }
+    }
+
+    .error-img {
+      width: 400rpx;
+      height: 400rpx;
+    }
+
+    #scrollBottom {
+      height: 120rpx;
+    }
+  }
+</style>

+ 14 - 0
pages/chat/components/constants.js

@@ -0,0 +1,14 @@
+export const KeFuMessageContentTypeEnum = {
+  TEXT: 1, // 文本消息
+  IMAGE: 2, // 图片消息
+  VOICE: 3, // 语音消息
+  VIDEO: 4, // 视频消息
+  SYSTEM: 5, // 系统消息
+  // ========== 商城特殊消息 ==========
+  PRODUCT: 10,//  商品消息
+  ORDER: 11,//  订单消息"
+};
+export const UserTypeEnum = {
+  MEMBER: 1, // 会员 面向 c 端,普通用户
+  ADMIN: 2, // 管理员 面向 b 端,管理后台
+};

+ 62 - 495
pages/chat/index.vue

@@ -1,163 +1,14 @@
 <template>
   <s-layout class="chat-wrap" title="客服" navbar="inner">
+    <!-- 头部连接状态展示  -->
     <div class="status">
       {{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
     </div>
+    <!--  覆盖头部导航栏背景颜色  -->
     <div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
-    <view class="chat-box" :style="{ height: pageHeight + 'px' }">
-      <scroll-view
-        :style="{ height: pageHeight + 'px' }"
-        scroll-y="true"
-        :scroll-with-animation="false"
-        :enable-back-to-top="true"
-        :scroll-into-view="chat.scrollInto"
-      >
-        <button
-          class="loadmore-btn ss-reset-button"
-          v-if="
-            chatList.length &&
-            chatHistoryPagination.lastPage > 1 &&
-            loadingMap[chatHistoryPagination.loadStatus].title
-          "
-          @click="onLoadMore"
-        >
-          {{ loadingMap[chatHistoryPagination.loadStatus].title }}
-          <i
-            class="loadmore-icon sa-m-l-6"
-            :class="loadingMap[chatHistoryPagination.loadStatus].icon"
-          ></i>
-        </button>
-        <view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">
-          <view class="ss-flex ss-row-center ss-col-center">
-            <!-- 日期 -->
-            <view v-if="item.from !== 'system' && showTime(item, index)" class="date-message">
-              {{ formatTime(item.date) }}
-            </view>
-            <!-- 系统消息 -->
-            <view v-if="item.from === 'system'" class="system-message">
-              {{ item.content.text }}
-            </view>
-          </view>
-          <!-- 常见问题 -->
-          <view v-if="item.mode === 'template' && item.content.list.length" class="template-wrap">
-            <view class="title">猜你想问</view>
-            <view
-              class="item"
-              v-for="(item, index) in item.content.list"
-              :key="index"
-              @click="onTemplateList(item)"
-            >
-              * {{ item.title }}
-            </view>
-          </view>
-
-          <view
-            v-if="
-              (item.from === 'customer_service' && item.mode !== 'template') ||
-              item.from === 'customer'
-            "
-            class="ss-flex ss-col-top"
-            :class="[
-              item.from === 'customer_service'
-                ? `ss-row-left`
-                : item.from === 'customer'
-                ? `ss-row-right`
-                : '',
-            ]"
-          >
-            <!-- 客服头像 -->
-            <image
-              v-show="item.from === 'customer_service'"
-              class="chat-avatar ss-m-r-24"
-              :src="
-                sheep.$url.cdn(item?.sender?.avatar) ||
-                sheep.$url.static('/static/img/shop/chat/default.png')
-              "
-              mode="aspectFill"
-            ></image>
-
-            <!-- 发送状态 -->
-            <span
-              v-if="
-                item.from === 'customer' &&
-                index == chatData.chatList.length - 1 &&
-                chatData.isSendSucces !== 0
-              "
-              class="send-status"
-            >
-              <image
-                v-if="chatData.isSendSucces == -1"
-                class="loading"
-                :src="sheep.$url.static('/static/img/shop/chat/loading.png')"
-                mode="aspectFill"
-              ></image>
-              <!-- <image
-                v-if="chatData.isSendSucces == 1"
-                class="warning"
-                :src="sheep.$url.static('/static/img/shop/chat/warning.png')"
-                mode="aspectFill"
-                @click="onAgainSendMessage(item)"
-              ></image> -->
-            </span>
-
-            <!-- 内容 -->
-            <template v-if="item.mode === 'text'">
-              <view class="message-box" :class="[item.from]">
-                <div
-                  class="message-text ss-flex ss-flex-wrap"
-                  @click="onRichtext"
-                  v-html="replaceEmoji(item.content.text)"
-                ></div>
-              </view>
-            </template>
-            <template v-if="item.mode === 'image'">
-              <view class="message-box" :class="[item.from]" :style="{ width: '200rpx' }">
-                <su-image
-                  class="message-img"
-                  isPreview
-                  :previewList="[sheep.$url.cdn(item.content.url)]"
-                  :current="0"
-                  :src="sheep.$url.cdn(item.content.url)"
-                  :height="200"
-                  :width="200"
-                  mode="aspectFill"
-                ></su-image>
-              </view>
-            </template>
-            <template v-if="item.mode === 'goods'">
-              <GoodsItem
-                :goodsData="item.content.item"
-                @tap="
-                  sheep.$router.go('/pages/goods/index', {
-                    id: item.content.item.id,
-                  })
-                "
-              />
-            </template>
-            <template v-if="item.mode === 'order'">
-              <OrderItem
-                from="msg"
-                :orderData="item.content.item"
-                @tap="
-                  sheep.$router.go('/pages/order/detail', {
-                    id: item.content.item.id,
-                  })
-                "
-              />
-            </template>
-            <!-- user头像 -->
-            <image
-              v-show="item.from === 'customer'"
-              class="chat-avatar ss-m-l-24"
-              :src="sheep.$url.cdn(customerUserInfo.avatar)"
-              mode="aspectFill"
-            >
-            </image>
-          </view>
-        </view>
-        <view id="scrollBottom"></view>
-      </scroll-view>
-    </view>
+    <!--  聊天区域  -->
+    <ChatBox></ChatBox>
+    <!--  消息发送区域  -->
     <su-fixed bottom>
       <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
     </su-fixed>
@@ -166,7 +17,7 @@
                  @on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
       <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
     </tools-popup>
-
+    <!--  商品订单选择  -->
     <SelectPopup
       :mode="chat.selectMode"
       :show="chat.showSelect"
@@ -177,17 +28,16 @@
 </template>
 
 <script setup>
+  import { useChatWebSocket } from '@/pages/chat/socket';
+  import ChatBox from './components/chatBox.vue';
+  import { reactive, toRefs } from 'vue';
   import sheep from '@/sheep';
-  import { computed, reactive, toRefs } from 'vue';
+  import ToolsPopup from '@/pages/chat/components/toolsPopup.vue';
+  import MessageInput from '@/pages/chat/components/messageInput.vue';
   import { onLoad } from '@dcloudio/uni-app';
-  import { emojiList } from './emoji.js';
-  import SelectPopup from './components/select-popup.vue';
-  import GoodsItem from './components/goods.vue';
-  import OrderItem from './components/order.vue';
-  import MessageInput from './components/messageInput.vue';
-  import ToolsPopup from './components/toolsPopup.vue';
-  import { useChatWebSocket } from './socket';
+  import SelectPopup from '@/pages/chat/components/select-popup.vue';
 
+  const sys_navBar = sheep.$platform.navbar;
   const {
     socketInit,
     state: chatData,
@@ -209,46 +59,6 @@
   const customerUserInfo = toRefs(chatData).customerUserInfo;
   const socketState = toRefs(chatData).socketState;
 
-  const sys_navBar = sheep.$platform.navbar;
-  const chatConfig = computed(() => sheep.$store('app').chat);
-
-  const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
-  const pageHeight = safeArea.height - 44 - 35 - 50;
-
-  const chatStatus = {
-    online: {
-      text: '在线',
-      colorVariate: '#46c55f',
-    },
-    offline: {
-      text: '离线',
-      colorVariate: '#b5b5b5',
-    },
-    busy: {
-      text: '忙碌',
-      colorVariate: '#ff0e1b',
-    },
-  };
-
-  // 加载更多
-  const loadingMap = {
-    loadmore: {
-      title: '查看更多',
-      icon: 'el-icon-d-arrow-left',
-    },
-    nomore: {
-      title: '没有更多了',
-      icon: '',
-    },
-    loading: {
-      title: '加载中... ',
-      icon: 'el-icon-loading',
-    },
-  };
-  const onLoadMore = () => {
-    chatHistoryPagination.value.page < chatHistoryPagination.value.lastPage && socketHistoryList();
-  };
-
   const chat = reactive({
     msg: '',
     scrollInto: '',
@@ -268,7 +78,41 @@
     },
   });
 
-  //======================= 聊天工具相关 =======================
+  function scrollBottom() {
+    let timeout = null;
+    chat.scrollInto = '';
+    clearTimeout(timeout);
+    timeout = setTimeout(() => {
+      chat.scrollInto = 'scrollBottom';
+    }, 100);
+  }
+
+  // 发送消息
+  function onSendMessage() {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      return;
+    }
+    if (!chat.msg) return;
+    const data = {
+      from: 'customer',
+      mode: 'text',
+      date: new Date().getTime(),
+      content: {
+        text: chat.msg,
+      },
+    };
+    socketSendMsg(data, () => {
+      scrollBottom();
+    });
+    chat.showTools = false;
+    // scrollBottom();
+    setTimeout(() => {
+      chat.msg = '';
+    }, 100);
+  }
+
+  //======================= 聊天工具相关 start =======================
 
   function handleToolsClose() {
     chat.showTools = false;
@@ -366,137 +210,27 @@
     }
   }
 
-  function onAgainSendMessage(item) {
-    if (!socketState.value.isConnect) {
-      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
-      return;
-    }
-    if (!item) return;
-    const data = {
-      from: 'customer',
-      mode: 'text',
-      date: new Date().getTime(),
-      content: item.content,
-    };
-    socketSendMsg(data, () => {
-      scrollBottom();
-    });
-  }
+  //======================= 聊天工具相关 end =======================
 
-  function onSendMessage() {
-    if (!socketState.value.isConnect) {
-      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
-      return;
-    }
-    if (!chat.msg) return;
-    const data = {
-      from: 'customer',
-      mode: 'text',
-      date: new Date().getTime(),
-      content: {
-        text: chat.msg,
-      },
-    };
-    socketSendMsg(data, () => {
-      scrollBottom();
-    });
-    chat.showTools = false;
-    // scrollBottom();
-    setTimeout(() => {
-      chat.msg = '';
-    }, 100);
-  }
-
-  // 点击猜你想问
-  function onTemplateList(e) {
-    if (!socketState.value.isConnect) {
-      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
-      return;
-    }
-    const data = {
-      from: 'customer',
-      mode: 'text',
-      date: new Date().getTime(),
-      content: {
-        text: e.title,
-      },
-      customData: {
-        question_id: e.id,
-      },
-    };
-    socketSendMsg(data, () => {
+  onLoad(async () => {
+    socketInit({}, () => {
       scrollBottom();
     });
-    // scrollBottom();
-  }
-
-  function selEmojiFile(name) {
-    for (let index in emojiList) {
-      if (emojiList[index].name === name) {
-        return emojiList[index].file;
-      }
-    }
-    return false;
-  }
-
-  function replaceEmoji(data) {
-    let newData = data;
-    if (typeof newData !== 'object') {
-      let reg = /\[(.+?)\]/g; // [] 中括号
-      let zhEmojiName = newData.match(reg);
-      if (zhEmojiName) {
-        zhEmojiName.forEach((item) => {
-          let emojiFile = selEmojiFile(item);
-          newData = newData.replace(
-            item,
-            `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
-              '/static/img/chat/emoji/' + emojiFile,
-            )}"/>`,
-          );
-        });
-      }
-    }
-    return newData;
-  }
-
-  function scrollBottom() {
-    let timeout = null;
-    chat.scrollInto = '';
-    clearTimeout(timeout);
-    timeout = setTimeout(() => {
-      chat.scrollInto = 'scrollBottom';
-    }, 100);
-  }
-
-  onLoad(async () => {
-    const { error } = await getUserToken();
-    if (error === 0) {
-      socketInit(chatConfig.value, () => {
-        scrollBottom();
-      });
-    } else {
-      socketState.value.isConnect = false;
-    }
   });
 </script>
 
-<style lang="scss" scoped>
-  .page-bg {
-    width: 100%;
-    position: absolute;
-    top: 0;
-    left: 0;
-    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
-    background-size: 750rpx 100%;
-    z-index: 1;
-  }
-
+<style scoped lang="scss">
   .chat-wrap {
-    // :deep() {
-    //   .ui-navbar-box {
-    //     background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
-    //   }
-    // }
+
+    .page-bg {
+      width: 100%;
+      position: absolute;
+      top: 0;
+      left: 0;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+      background-size: 750rpx 100%;
+      z-index: 1;
+    }
 
     .status {
       position: relative;
@@ -511,172 +245,5 @@
       font-weight: 400;
       color: var(--ui-BG-Main);
     }
-
-    .chat-box {
-      padding: 0 20rpx 0;
-
-      .loadmore-btn {
-        width: 98%;
-        height: 40px;
-        font-size: 12px;
-        color: #8c8c8c;
-
-        .loadmore-icon {
-          transform: rotate(90deg);
-        }
-      }
-
-      .message-item {
-        margin-bottom: 33rpx;
-      }
-
-      .date-message,
-      .system-message {
-        width: fit-content;
-        border-radius: 12rpx;
-        padding: 8rpx 16rpx;
-        margin-bottom: 16rpx;
-        background-color: var(--ui-BG-3);
-        color: #999;
-        font-size: 24rpx;
-      }
-
-      .chat-avatar {
-        width: 70rpx;
-        height: 70rpx;
-        border-radius: 50%;
-      }
-
-      .send-status {
-        color: #333;
-        height: 80rpx;
-        margin-right: 8rpx;
-        display: flex;
-        align-items: center;
-
-        .loading {
-          width: 32rpx;
-          height: 32rpx;
-          -webkit-animation: rotating 2s linear infinite;
-          animation: rotating 2s linear infinite;
-
-          @-webkit-keyframes rotating {
-            0% {
-              transform: rotateZ(0);
-            }
-
-            100% {
-              transform: rotateZ(360deg);
-            }
-          }
-
-          @keyframes rotating {
-            0% {
-              transform: rotateZ(0);
-            }
-
-            100% {
-              transform: rotateZ(360deg);
-            }
-          }
-        }
-
-        .warning {
-          width: 32rpx;
-          height: 32rpx;
-          color: #ff3000;
-        }
-      }
-
-      .message-box {
-        max-width: 50%;
-        font-size: 16px;
-        line-height: 20px;
-        // max-width: 500rpx;
-        white-space: normal;
-        word-break: break-all;
-        word-wrap: break-word;
-        padding: 20rpx;
-        border-radius: 10rpx;
-        color: #fff;
-        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
-
-        &.customer_service {
-          background: #fff;
-          color: #333;
-        }
-
-        :deep() {
-          .imgred {
-            width: 100%;
-          }
-
-          .imgred,
-          img {
-            width: 100%;
-          }
-        }
-      }
-
-      :deep() {
-        .goods,
-        .order {
-          max-width: 500rpx;
-        }
-      }
-
-      .message-img {
-        width: 100px;
-        height: 100px;
-        border-radius: 6rpx;
-      }
-
-      .template-wrap {
-        // width: 100%;
-        padding: 20rpx 24rpx;
-        background: #fff;
-        border-radius: 10rpx;
-
-        .title {
-          font-size: 26rpx;
-          font-weight: 500;
-          color: #333;
-          margin-bottom: 29rpx;
-        }
-
-        .item {
-          font-size: 24rpx;
-          color: var(--ui-BG-Main);
-          margin-bottom: 16rpx;
-
-          &:last-of-type {
-            margin-bottom: 0;
-          }
-        }
-      }
-
-      .error-img {
-        width: 400rpx;
-        height: 400rpx;
-      }
-
-      #scrollBottom {
-        height: 120rpx;
-      }
-    }
-  }
-</style>
-<style>
-  .chat-img {
-    width: 24px;
-    height: 24px;
-    margin: 0 3px;
-  }
-
-  .full-img {
-    object-fit: cover;
-    width: 100px;
-    height: 100px;
-    border-radius: 6px;
   }
 </style>

+ 678 - 0
pages/chat/index1.vue

@@ -0,0 +1,678 @@
+<template>
+  <s-layout class="chat-wrap" title="客服" navbar="inner">
+    <div class="status">
+      {{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
+    </div>
+    <div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
+    <view class="chat-box" :style="{ height: pageHeight + 'px' }">
+      <scroll-view
+        :style="{ height: pageHeight + 'px' }"
+        scroll-y="true"
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+        :scroll-into-view="chat.scrollInto"
+      >
+        <button
+          class="loadmore-btn ss-reset-button"
+          v-if="
+            chatList.length &&
+            chatHistoryPagination.lastPage > 1 &&
+            loadingMap[chatHistoryPagination.loadStatus].title
+          "
+          @click="onLoadMore"
+        >
+          {{ loadingMap[chatHistoryPagination.loadStatus].title }}
+          <i
+            class="loadmore-icon sa-m-l-6"
+            :class="loadingMap[chatHistoryPagination.loadStatus].icon"
+          ></i>
+        </button>
+        <view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">
+          <view class="ss-flex ss-row-center ss-col-center">
+            <!-- 日期 -->
+            <view v-if="item.from !== 'system' && showTime(item, index)" class="date-message">
+              {{ formatTime(item.date) }}
+            </view>
+            <!-- 系统消息 -->
+            <view v-if="item.from === 'system'" class="system-message">
+              {{ item.content.text }}
+            </view>
+          </view>
+          <!-- 常见问题 -->
+          <view v-if="item.mode === 'template' && item.content.list.length" class="template-wrap">
+            <view class="title">猜你想问</view>
+            <view
+              class="item"
+              v-for="(item, index) in item.content.list"
+              :key="index"
+              @click="onTemplateList(item)"
+            >
+              * {{ item.title }}
+            </view>
+          </view>
+
+          <view
+            v-if="
+              (item.from === 'customer_service' && item.mode !== 'template') ||
+              item.from === 'customer'
+            "
+            class="ss-flex ss-col-top"
+            :class="[
+              item.from === 'customer_service'
+                ? `ss-row-left`
+                : item.from === 'customer'
+                ? `ss-row-right`
+                : '',
+            ]"
+          >
+            <!-- 客服头像 -->
+            <image
+              v-show="item.from === 'customer_service'"
+              class="chat-avatar ss-m-r-24"
+              :src="
+                sheep.$url.cdn(item?.sender?.avatar) ||
+                sheep.$url.static('/static/img/shop/chat/default.png')
+              "
+              mode="aspectFill"
+            ></image>
+
+            <!-- 发送状态 -->
+            <span
+              v-if="
+                item.from === 'customer' &&
+                index == chatData.chatList.length - 1 &&
+                chatData.isSendSucces !== 0
+              "
+              class="send-status"
+            >
+              <image
+                v-if="chatData.isSendSucces == -1"
+                class="loading"
+                :src="sheep.$url.static('/static/img/shop/chat/loading.png')"
+                mode="aspectFill"
+              ></image>
+              <!-- <image
+                v-if="chatData.isSendSucces == 1"
+                class="warning"
+                :src="sheep.$url.static('/static/img/shop/chat/warning.png')"
+                mode="aspectFill"
+                @click="onAgainSendMessage(item)"
+              ></image> -->
+            </span>
+
+            <!-- 内容 -->
+            <template v-if="item.mode === 'text'">
+              <view class="message-box" :class="[item.from]">
+                <div
+                  class="message-text ss-flex ss-flex-wrap"
+                  @click="onRichtext"
+                  v-html="replaceEmoji(item.content.text)"
+                ></div>
+              </view>
+            </template>
+            <template v-if="item.mode === 'image'">
+              <view class="message-box" :class="[item.from]" :style="{ width: '200rpx' }">
+                <su-image
+                  class="message-img"
+                  isPreview
+                  :previewList="[sheep.$url.cdn(item.content.url)]"
+                  :current="0"
+                  :src="sheep.$url.cdn(item.content.url)"
+                  :height="200"
+                  :width="200"
+                  mode="aspectFill"
+                ></su-image>
+              </view>
+            </template>
+            <template v-if="item.mode === 'goods'">
+              <GoodsItem
+                :goodsData="item.content.item"
+                @tap="
+                  sheep.$router.go('/pages/goods/index', {
+                    id: item.content.item.id,
+                  })
+                "
+              />
+            </template>
+            <template v-if="item.mode === 'order'">
+              <OrderItem
+                from="msg"
+                :orderData="item.content.item"
+                @tap="
+                  sheep.$router.go('/pages/order/detail', {
+                    id: item.content.item.id,
+                  })
+                "
+              />
+            </template>
+            <!-- user头像 -->
+            <image
+              v-show="item.from === 'customer'"
+              class="chat-avatar ss-m-l-24"
+              :src="sheep.$url.cdn(customerUserInfo.avatar)"
+              mode="aspectFill"
+            >
+            </image>
+          </view>
+        </view>
+        <view id="scrollBottom"></view>
+      </scroll-view>
+    </view>
+    <su-fixed bottom>
+      <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
+    </su-fixed>
+    <!--  聊天工具  -->
+    <tools-popup :show-tools="chat.showTools" :tools-mode="chat.toolsMode" @close="handleToolsClose"
+                 @on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
+      <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
+    </tools-popup>
+
+    <SelectPopup
+      :mode="chat.selectMode"
+      :show="chat.showSelect"
+      @select="onSelect"
+      @close="chat.showSelect = false"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { computed, reactive, toRefs } from 'vue';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { emojiList } from './emoji.js';
+  import SelectPopup from './components/select-popup.vue';
+  import GoodsItem from './components/goods.vue';
+  import OrderItem from './components/order.vue';
+  import MessageInput from './components/messageInput.vue';
+  import ToolsPopup from './components/toolsPopup.vue';
+  import { useChatWebSocket } from './socket';
+  import { useWebSocket } from '@/sheep/hooks/useWebSocket';
+
+  const {
+    socketInit,
+    state: chatData,
+    socketSendMsg,
+    formatChatInput,
+    socketHistoryList,
+    onDrop,
+    onPaste,
+    getFocus,
+    // upload,
+    getUserToken,
+    // socketTest,
+    showTime,
+    formatTime,
+  } = useChatWebSocket();
+  const chatList = toRefs(chatData).chatList;
+  const customerServiceInfo = toRefs(chatData).customerServerInfo;
+  const chatHistoryPagination = toRefs(chatData).chatHistoryPagination;
+  const customerUserInfo = toRefs(chatData).customerUserInfo;
+  const socketState = toRefs(chatData).socketState;
+
+  const sys_navBar = sheep.$platform.navbar;
+  const chatConfig = computed(() => sheep.$store('app').chat);
+
+  const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
+  const pageHeight = safeArea.height - 44 - 35 - 50;
+
+  const chatStatus = {
+    online: {
+      text: '在线',
+      colorVariate: '#46c55f',
+    },
+    offline: {
+      text: '离线',
+      colorVariate: '#b5b5b5',
+    },
+    busy: {
+      text: '忙碌',
+      colorVariate: '#ff0e1b',
+    },
+  };
+
+  // 加载更多
+  const loadingMap = {
+    loadmore: {
+      title: '查看更多',
+      icon: 'el-icon-d-arrow-left',
+    },
+    nomore: {
+      title: '没有更多了',
+      icon: '',
+    },
+    loading: {
+      title: '加载中... ',
+      icon: 'el-icon-loading',
+    },
+  };
+  const onLoadMore = () => {
+    chatHistoryPagination.value.page < chatHistoryPagination.value.lastPage && socketHistoryList();
+  };
+
+  const chat = reactive({
+    msg: '',
+    scrollInto: '',
+
+    showTools: false,
+    toolsMode: '',
+
+    showSelect: false,
+    selectMode: '',
+    chatStyle: {
+      mode: 'inner',
+      color: '#F8270F',
+      type: 'color',
+      alwaysShow: 1,
+      src: '',
+      list: {},
+    },
+  });
+
+  //======================= 聊天工具相关 =======================
+
+  function handleToolsClose() {
+    chat.showTools = false;
+    chat.toolsMode = '';
+  }
+
+  function onEmoji(item) {
+    chat.msg += item.name;
+  }
+
+  // 点击工具栏开关
+  function onTools(mode) {
+    // if (!socketState.value.isConnect) {
+    //   sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+    //   return;
+    // }
+
+    if (!chat.toolsMode || chat.toolsMode === mode) {
+      chat.showTools = !chat.showTools;
+    }
+    chat.toolsMode = mode;
+    if (!chat.showTools) {
+      chat.toolsMode = '';
+    }
+  }
+
+  function onShowSelect(mode) {
+    chat.showTools = false;
+    chat.showSelect = true;
+    chat.selectMode = mode;
+  }
+
+  async function onSelect({ type, data }) {
+    let msg = '';
+    switch (type) {
+      case 'image':
+        const { path, fullurl } = await sheep.$api.app.upload(data.tempFiles[0].path, 'default');
+        msg = {
+          from: 'customer',
+          mode: 'image',
+          date: new Date().getTime(),
+          content: {
+            url: fullurl,
+            path: path,
+          },
+        };
+        break;
+      case 'goods':
+        msg = {
+          from: 'customer',
+          mode: 'goods',
+          date: new Date().getTime(),
+          content: {
+            item: {
+              id: data.goods.id,
+              title: data.goods.title,
+              image: data.goods.image,
+              price: data.goods.price,
+              stock: data.goods.stock,
+            },
+          },
+        };
+        break;
+      case 'order':
+        msg = {
+          from: 'customer',
+          mode: 'order',
+          date: new Date().getTime(),
+          content: {
+            item: {
+              id: data.id,
+              order_sn: data.order_sn,
+              create_time: data.create_time,
+              pay_fee: data.pay_fee,
+              items: data.items.filter((item) => ({
+                goods_id: item.goods_id,
+                goods_title: item.goods_title,
+                goods_image: item.goods_image,
+                goods_price: item.goods_price,
+              })),
+              status_text: data.status_text,
+            },
+          },
+        };
+        break;
+    }
+    if (msg) {
+      socketSendMsg(msg, () => {
+        scrollBottom();
+      });
+      // scrollBottom();
+      chat.showTools = false;
+      chat.showSelect = false;
+      chat.selectMode = '';
+    }
+  }
+
+  function onAgainSendMessage(item) {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      return;
+    }
+    if (!item) return;
+    const data = {
+      from: 'customer',
+      mode: 'text',
+      date: new Date().getTime(),
+      content: item.content,
+    };
+    socketSendMsg(data, () => {
+      scrollBottom();
+    });
+  }
+
+  function onSendMessage() {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      return;
+    }
+    if (!chat.msg) return;
+    const data = {
+      from: 'customer',
+      mode: 'text',
+      date: new Date().getTime(),
+      content: {
+        text: chat.msg,
+      },
+    };
+    socketSendMsg(data, () => {
+      scrollBottom();
+    });
+    chat.showTools = false;
+    // scrollBottom();
+    setTimeout(() => {
+      chat.msg = '';
+    }, 100);
+  }
+
+  // 点击猜你想问
+  function onTemplateList(e) {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      return;
+    }
+    const data = {
+      from: 'customer',
+      mode: 'text',
+      date: new Date().getTime(),
+      content: {
+        text: e.title,
+      },
+      customData: {
+        question_id: e.id,
+      },
+    };
+    socketSendMsg(data, () => {
+      scrollBottom();
+    });
+    // scrollBottom();
+  }
+
+  function selEmojiFile(name) {
+    for (let index in emojiList) {
+      if (emojiList[index].name === name) {
+        return emojiList[index].file;
+      }
+    }
+    return false;
+  }
+
+  function replaceEmoji(data) {
+    let newData = data;
+    if (typeof newData !== 'object') {
+      let reg = /\[(.+?)\]/g; // [] 中括号
+      let zhEmojiName = newData.match(reg);
+      if (zhEmojiName) {
+        zhEmojiName.forEach((item) => {
+          let emojiFile = selEmojiFile(item);
+          newData = newData.replace(
+            item,
+            `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
+              '/static/img/chat/emoji/' + emojiFile,
+            )}"/>`,
+          );
+        });
+      }
+    }
+    return newData;
+  }
+
+  function scrollBottom() {
+    let timeout = null;
+    chat.scrollInto = '';
+    clearTimeout(timeout);
+    timeout = setTimeout(() => {
+      chat.scrollInto = 'scrollBottom';
+    }, 100);
+  }
+  const websocket = useWebSocket()
+  onLoad(async () => {
+    websocket.socketInit({}, () => {
+      scrollBottom();
+    });
+  });
+</script>
+
+<style lang="scss" scoped>
+  .page-bg {
+    width: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    background-size: 750rpx 100%;
+    z-index: 1;
+  }
+
+  .chat-wrap {
+    // :deep() {
+    //   .ui-navbar-box {
+    //     background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    //   }
+    // }
+
+    .status {
+      position: relative;
+      box-sizing: border-box;
+      z-index: 3;
+      height: 70rpx;
+      padding: 0 30rpx;
+      background: var(--ui-BG-Main-opacity-1);
+      display: flex;
+      align-items: center;
+      font-size: 30rpx;
+      font-weight: 400;
+      color: var(--ui-BG-Main);
+    }
+
+    .chat-box {
+      padding: 0 20rpx 0;
+
+      .loadmore-btn {
+        width: 98%;
+        height: 40px;
+        font-size: 12px;
+        color: #8c8c8c;
+
+        .loadmore-icon {
+          transform: rotate(90deg);
+        }
+      }
+
+      .message-item {
+        margin-bottom: 33rpx;
+      }
+
+      .date-message,
+      .system-message {
+        width: fit-content;
+        border-radius: 12rpx;
+        padding: 8rpx 16rpx;
+        margin-bottom: 16rpx;
+        background-color: var(--ui-BG-3);
+        color: #999;
+        font-size: 24rpx;
+      }
+
+      .chat-avatar {
+        width: 70rpx;
+        height: 70rpx;
+        border-radius: 50%;
+      }
+
+      .send-status {
+        color: #333;
+        height: 80rpx;
+        margin-right: 8rpx;
+        display: flex;
+        align-items: center;
+
+        .loading {
+          width: 32rpx;
+          height: 32rpx;
+          -webkit-animation: rotating 2s linear infinite;
+          animation: rotating 2s linear infinite;
+
+          @-webkit-keyframes rotating {
+            0% {
+              transform: rotateZ(0);
+            }
+
+            100% {
+              transform: rotateZ(360deg);
+            }
+          }
+
+          @keyframes rotating {
+            0% {
+              transform: rotateZ(0);
+            }
+
+            100% {
+              transform: rotateZ(360deg);
+            }
+          }
+        }
+
+        .warning {
+          width: 32rpx;
+          height: 32rpx;
+          color: #ff3000;
+        }
+      }
+
+      .message-box {
+        max-width: 50%;
+        font-size: 16px;
+        line-height: 20px;
+        // max-width: 500rpx;
+        white-space: normal;
+        word-break: break-all;
+        word-wrap: break-word;
+        padding: 20rpx;
+        border-radius: 10rpx;
+        color: #fff;
+        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+
+        &.customer_service {
+          background: #fff;
+          color: #333;
+        }
+
+        :deep() {
+          .imgred {
+            width: 100%;
+          }
+
+          .imgred,
+          img {
+            width: 100%;
+          }
+        }
+      }
+
+      :deep() {
+        .goods,
+        .order {
+          max-width: 500rpx;
+        }
+      }
+
+      .message-img {
+        width: 100px;
+        height: 100px;
+        border-radius: 6rpx;
+      }
+
+      .template-wrap {
+        // width: 100%;
+        padding: 20rpx 24rpx;
+        background: #fff;
+        border-radius: 10rpx;
+
+        .title {
+          font-size: 26rpx;
+          font-weight: 500;
+          color: #333;
+          margin-bottom: 29rpx;
+        }
+
+        .item {
+          font-size: 24rpx;
+          color: var(--ui-BG-Main);
+          margin-bottom: 16rpx;
+
+          &:last-of-type {
+            margin-bottom: 0;
+          }
+        }
+      }
+
+      .error-img {
+        width: 400rpx;
+        height: 400rpx;
+      }
+
+      #scrollBottom {
+        height: 120rpx;
+      }
+    }
+  }
+</style>
+<style>
+  .chat-img {
+    width: 24px;
+    height: 24px;
+    margin: 0 3px;
+  }
+
+  .full-img {
+    object-fit: cover;
+    width: 100px;
+    height: 100px;
+    border-radius: 6px;
+  }
+</style>

+ 10 - 8
pages/chat/socket.js

@@ -3,6 +3,7 @@ import sheep from '@/sheep';
 // import chat from '@/sheep/api/chat';
 import dayjs from 'dayjs';
 import io from '@hyoga/uni-socket.io';
+import { baseUrl, websocketPath } from '@/sheep/config';
 
 export function useChatWebSocket(socketConfig) {
   let SocketIo = null;
@@ -14,7 +15,7 @@ export function useChatWebSocket(socketConfig) {
     customerUserInfo: {}, //用户信息
     customerServerInfo: {
       //客服信息
-      title: '连接中...',
+      title: '客服已接入',
       state: 'connecting',
       avatar: null,
       nickname: '',
@@ -35,7 +36,7 @@ export function useChatWebSocket(socketConfig) {
 
     chatConfig: {}, // 配置信息
 
-    isSendSucces: -1, // 是否发送成功 -1=发送中|0=发送成功|1发送失败
+    isSendSuccess: -1, // 是否发送成功 -1=发送中|0=发送成功|1发送失败
   });
 
   /**
@@ -49,7 +50,11 @@ export function useChatWebSocket(socketConfig) {
     if (state.socketState.isConnecting) return; // 重连中,返回false
 
     // 启动初始化
-    SocketIo = io(config.chat_domain, {
+    SocketIo = io(baseUrl + websocketPath, {
+      path:websocketPath,
+      query:{
+        token: getAccessToken()
+      },
       reconnection: true, // 默认 true    是否断线重连
       reconnectionAttempts: 5, // 默认无限次   断线尝试次数
       reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。
@@ -302,8 +307,8 @@ export function useChatWebSocket(socketConfig) {
     );
   };
 
-  // 用户id,获取token
-  const getUserToken = async (id) => {
+  // 获取token
+  const getAccessToken = () => {
     return uni.getStorageSync('token');
   };
 
@@ -803,9 +808,6 @@ export function useChatWebSocket(socketConfig) {
     onDrop,
     onPaste,
     upload,
-
-    getUserToken,
-
     state,
 
     socketTest,

+ 30 - 0
sheep/api/promotion/kefu.js

@@ -0,0 +1,30 @@
+import request from '@/sheep/request';
+
+const KeFuApi = {
+  sendMessage: (data) => {
+    return request({
+      url: '/promotion/kefu-message/send',
+      method: 'POST',
+      data,
+      custom: {
+        auth: true,
+        showLoading: true,
+        loadingMsg: '发送中',
+        showSuccess: true,
+        successMsg: '发送成功',
+      },
+    });
+  },
+  getConversation: () => {
+    return request({
+      url: '/promotion/kefu-conversation/get',
+      method: 'GET',
+      custom: {
+        auth: true,
+        showLoading: false,
+      },
+    });
+  },
+};
+
+export default KeFuApi;

+ 2 - 1
sheep/config/index.js

@@ -11,9 +11,10 @@ console.log(`[芋道商城 ${version}]  http://doc.iocoder.cn`);
 
 export const apiPath = import.meta.env.SHOPRO_API_PATH;
 export const staticUrl = import.meta.env.SHOPRO_STATIC_URL;
-
+export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH;
 export default {
   baseUrl,
   apiPath,
   staticUrl,
+  websocketPath
 };

+ 99 - 0
sheep/hooks/useWebSocket.js

@@ -0,0 +1,99 @@
+import { reactive, ref, unref } from 'vue';
+import sheep from '@/sheep';
+// import chat from '@/sheep/api/chat';
+import dayjs from 'dayjs';
+import io from '@hyoga/uni-socket.io';
+import { baseUrl, websocketPath } from '@/sheep/config';
+export function useWebSocket() {
+  const SocketIo = ref(null)
+  // chat状态数据
+  const state = reactive({
+    socketState: {
+      isConnect: true, //是否连接成功
+      isConnecting: false, //重连中,不允许新的socket开启。
+      tip: '',
+    },
+    chatConfig: {}, // 配置信息
+  });
+  /**
+   * 连接初始化
+   * @param {Object} config  - 配置信息
+   * @param {Function} callBack -回调函数,有新消息接入,保持底部
+   */
+  const socketInit = (config, callBack) => {
+    state.chatConfig = config;
+    if (SocketIo.value && SocketIo.value.connected) return; // 如果socket已经连接,返回false
+    if (state.socketState.isConnecting) return; // 重连中,返回false
+
+    // 启动初始化
+    SocketIo.value = io(baseUrl + websocketPath, {
+      path:websocketPath,
+      query:{
+        token: getAccessToken()
+      },
+      reconnection: true, // 默认 true    是否断线重连
+      reconnectionAttempts: 5, // 默认无限次   断线尝试次数
+      reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。
+      reconnectionDelayMax: 5000, // 默认 5000, 重新连接等待的最长时间 默认 5000
+      randomizationFactor: 0.5, // 默认 0.5 [0-1],随机重连延迟时间
+      timeout: 20000, // 默认 20s
+      transports: ['websocket', 'polling'], // websocket | polling,
+      ...config,
+    });
+
+    // 监听连接
+    SocketIo.value.on('connect', async (res) => {
+      console.log('socket:connect');
+      // 消息返回
+      callBack && callBack(res)
+    });
+
+    // 监听错误 error
+    SocketIo.value.on('error', (error) => {
+      console.log('error:', error);
+    });
+    // 重连失败 connect_error
+    SocketIo.value.on('connect_error', (error) => {
+      console.log('connect_error');
+    });
+    // 连接上,但无反应 connect_timeout
+    SocketIo.value.on('connect_timeout', (error) => {
+      console.log(error, 'connect_timeout');
+    });
+    // 服务进程销毁 disconnect
+    SocketIo.value.on('disconnect', (error) => {
+      console.log(error, 'disconnect');
+    });
+    // 服务重启重连上reconnect
+    SocketIo.value.on('reconnect', (error) => {
+      console.log(error, 'reconnect');
+    });
+    // 开始重连reconnect_attempt
+    SocketIo.value.on('reconnect_attempt', (error) => {
+      state.socketState.isConnect = false;
+      state.socketState.isConnecting = true;
+      console.log(error, 'reconnect_attempt');
+    });
+    // 重新连接中reconnecting
+    SocketIo.value.on('reconnecting', (error) => {
+      console.log(error, 'reconnecting');
+    });
+    // 重新连接错误reconnect_error
+    SocketIo.value.on('reconnect_error', (error) => {
+      console.log('reconnect_error');
+    });
+    // 重新连接失败reconnect_failed
+    SocketIo.value.on('reconnect_failed', (error) => {
+      state.socketState.isConnecting = false;
+      console.log(error, 'reconnect_failed');
+    });
+  };
+  // 获取token
+  const getAccessToken = () => {
+    return uni.getStorageSync('token');
+  };
+  return {
+    state,
+    socketInit
+  }
+}

+ 2 - 2
sheep/util/index.js

@@ -64,7 +64,7 @@ export const convertToInteger = (num) => {
  * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
  * @returns {string} 返回拼接后的时间字符串
  */
-export function formatDate(date, format) {
+export function formatDate(date, format= 'YYYY-MM-DD HH:mm:ss') {
   // 日期不存在,则返回空
   if (!date) {
     return ''
@@ -112,4 +112,4 @@ export function resetPagination(pagination) {
   pagination.list = [];
   pagination.total = 0;
   pagination.pageNo = 1;
-}
+}