Browse Source

客服:完善消息发送(支持文本和表情)、聊天消息获取

puhui999 1 year ago
parent
commit
793252e4aa

+ 215 - 227
manifest.json

@@ -1,239 +1,227 @@
 {
-  "name": "芋道商城",
-  "appid": "__UNI__460BC4C",
-  "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
-  "versionName": "2.1.0",
-  "versionCode": 183,
-  "transformPx": false,
-  "app-plus": {
-    "usingComponents": true,
-    "nvueCompiler": "uni-app",
-    "nvueStyleCompiler": "uni-app",
-    "compilerVersion": 3,
-    "nvueLaunchMode": "fast",
-    "splashscreen": {
-      "alwaysShowBeforeRender": true,
-      "waiting": true,
-      "autoclose": true,
-      "delay": 0
+    "name" : "芋道商城",
+    "appid" : "__UNI__460BC4C",
+    "description" : "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
+    "versionName" : "2.1.0",
+    "versionCode" : 183,
+    "transformPx" : false,
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueCompiler" : "uni-app",
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "nvueLaunchMode" : "fast",
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        "safearea" : {
+            "bottom" : {
+                "offset" : "none"
+            }
+        },
+        "modules" : {
+            "Payment" : {},
+            "Share" : {},
+            "VideoPlayer" : {},
+            "OAuth" : {}
+        },
+        "distribute" : {
+            "android" : {
+                "permissions" : [
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
+                    "<uses-permission android:name=\"android.permission.INTERNET\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_SMS\"/>",
+                    "<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
+                    "<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
+                    "<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
+                ],
+                "minSdkVersion" : 21,
+                "schemes" : "shopro"
+            },
+            "ios" : {
+                "urlschemewhitelist" : [ "baidumap", "iosamap" ],
+                "dSYMs" : false,
+                "privacyDescription" : {
+                    "NSPhotoLibraryUsageDescription" : "需要同意访问您的相册选取图片才能完善该条目",
+                    "NSPhotoLibraryAddUsageDescription" : "需要同意访问您的相册才能保存该图片",
+                    "NSCameraUsageDescription" : "需要同意访问您的摄像头拍摄照片才能完善该条目",
+                    "NSUserTrackingUsageDescription" : "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
+                },
+                "urltypes" : "shopro",
+                "capabilities" : {
+                    "entitlements" : {
+                        "com.apple.developer.associated-domains" : [ "applinks:shopro.sheepjs.com" ]
+                    }
+                },
+                "idfa" : true
+            },
+            "sdkConfigs" : {
+                "speech" : {
+                    "ifly" : {}
+                },
+                "ad" : {},
+                "oauth" : {
+                    "apple" : {},
+                    "weixin" : {
+                        "appid" : "wxae7a0c156da9383b",
+                        "UniversalLinks" : "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
+                    }
+                },
+                "payment" : {
+                    "weixin" : {
+                        "__platform__" : [ "ios", "android" ],
+                        "appid" : "wxae7a0c156da9383b",
+                        "UniversalLinks" : "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
+                    },
+                    "alipay" : {
+                        "__platform__" : [ "ios", "android" ]
+                    }
+                },
+                "share" : {
+                    "weixin" : {
+                        "appid" : "wxae7a0c156da9383b",
+                        "UniversalLinks" : "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
+                    }
+                }
+            },
+            "orientation" : [ "portrait-primary" ],
+            "splashscreen" : {
+                "androidStyle" : "common",
+                "iosStyle" : "common",
+                "useOriginalMsgbox" : true
+            },
+            "icons" : {
+                "android" : {
+                    "hdpi" : "unpackage/res/icons/72x72.png",
+                    "xhdpi" : "unpackage/res/icons/96x96.png",
+                    "xxhdpi" : "unpackage/res/icons/144x144.png",
+                    "xxxhdpi" : "unpackage/res/icons/192x192.png"
+                },
+                "ios" : {
+                    "appstore" : "unpackage/res/icons/1024x1024.png",
+                    "ipad" : {
+                        "app" : "unpackage/res/icons/76x76.png",
+                        "app@2x" : "unpackage/res/icons/152x152.png",
+                        "notification" : "unpackage/res/icons/20x20.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "proapp@2x" : "unpackage/res/icons/167x167.png",
+                        "settings" : "unpackage/res/icons/29x29.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "spotlight" : "unpackage/res/icons/40x40.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png"
+                    },
+                    "iphone" : {
+                        "app@2x" : "unpackage/res/icons/120x120.png",
+                        "app@3x" : "unpackage/res/icons/180x180.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "notification@3x" : "unpackage/res/icons/60x60.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "settings@3x" : "unpackage/res/icons/87x87.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png",
+                        "spotlight@3x" : "unpackage/res/icons/120x120.png"
+                    }
+                }
+            }
+        }
     },
-    "safearea": {
-      "bottom": {
-        "offset": "none"
-      }
+    "quickapp" : {},
+    "quickapp-native" : {
+        "icon" : "/static/logo.png",
+        "package" : "com.example.demo",
+        "features" : [
+            {
+                "name" : "system.clipboard"
+            }
+        ]
     },
-    "modules": {
-      "Payment": {},
-      "Share": {},
-      "VideoPlayer": {},
-      "OAuth": {}
+    "quickapp-webview" : {
+        "icon" : "/static/logo.png",
+        "package" : "com.example.demo",
+        "minPlatformVersion" : 1070,
+        "versionName" : "1.0.0",
+        "versionCode" : 100
     },
-    "distribute": {
-      "android": {
-        "permissions": [
-          "<uses-feature android:name=\"android.hardware.camera\"/>",
-          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
-          "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
-          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
-          "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
-          "<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
-          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
-          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
-          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
-          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
-          "<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
-          "<uses-permission android:name=\"android.permission.INTERNET\"/>",
-          "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
-          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
-          "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
-          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
-          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.READ_SMS\"/>",
-          "<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
-          "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
-          "<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
-          "<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
-          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
-          "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
-          "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
-          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
-          "<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
-          "<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
-        ],
-        "minSdkVersion": 21,
-        "schemes": "shopro"
-      },
-      "ios": {
-        "urlschemewhitelist": [
-          "baidumap",
-          "iosamap"
-        ],
-        "dSYMs": false,
-        "privacyDescription": {
-          "NSPhotoLibraryUsageDescription": "需要同意访问您的相册选取图片才能完善该条目",
-          "NSPhotoLibraryAddUsageDescription": "需要同意访问您的相册才能保存该图片",
-          "NSCameraUsageDescription": "需要同意访问您的摄像头拍摄照片才能完善该条目",
-          "NSUserTrackingUsageDescription": "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
+    "mp-weixin" : {
+        "appid" : "wx98df718e528399d2",
+        "setting" : {
+            "urlCheck" : false,
+            "minified" : true,
+            "postcss" : true
         },
-        "urltypes": "shopro",
-        "capabilities": {
-          "entitlements": {
-            "com.apple.developer.associated-domains": [
-              "applinks:shopro.sheepjs.com"
-            ]
-          }
+        "optimization" : {
+            "subPackages" : true
         },
-        "idfa": true
-      },
-      "sdkConfigs": {
-        "speech": {
-          "ifly": {}
+        "plugins" : {},
+        "lazyCodeLoading" : "requiredComponents",
+        "usingComponents" : {},
+        "permission" : {},
+        "requiredPrivateInfos" : [ "chooseAddress" ]
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "mp-jd" : {
+        "usingComponents" : true
+    },
+    "h5" : {
+        "template" : "index.html",
+        "router" : {
+            "mode" : "history",
+            "base" : "./"
         },
-        "ad": {},
-        "oauth": {
-          "apple": {},
-          "weixin": {
-            "appid": "wxae7a0c156da9383b",
-            "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
-          }
+        "sdkConfigs" : {
+            "maps" : {}
         },
-        "payment": {
-          "weixin": {
-            "__platform__": [
-              "ios",
-              "android"
-            ],
-            "appid": "wxae7a0c156da9383b",
-            "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
-          },
-          "alipay": {
-            "__platform__": [
-              "ios",
-              "android"
-            ]
-          }
+        "async" : {
+            "timeout" : 20000
         },
-        "share": {
-          "weixin": {
-            "appid": "wxae7a0c156da9383b",
-            "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
-          }
-        }
-      },
-      "orientation": [
-        "portrait-primary"
-      ],
-      "splashscreen": {
-        "androidStyle": "common",
-        "iosStyle": "common",
-        "useOriginalMsgbox": true
-      },
-      "icons": {
-        "android": {
-          "hdpi": "unpackage/res/icons/72x72.png",
-          "xhdpi": "unpackage/res/icons/96x96.png",
-          "xxhdpi": "unpackage/res/icons/144x144.png",
-          "xxxhdpi": "unpackage/res/icons/192x192.png"
+        "title" : "芋道商城",
+        "optimization" : {
+            "treeShaking" : {
+                "enable" : true
+            }
         },
-        "ios": {
-          "appstore": "unpackage/res/icons/1024x1024.png",
-          "ipad": {
-            "app": "unpackage/res/icons/76x76.png",
-            "app@2x": "unpackage/res/icons/152x152.png",
-            "notification": "unpackage/res/icons/20x20.png",
-            "notification@2x": "unpackage/res/icons/40x40.png",
-            "proapp@2x": "unpackage/res/icons/167x167.png",
-            "settings": "unpackage/res/icons/29x29.png",
-            "settings@2x": "unpackage/res/icons/58x58.png",
-            "spotlight": "unpackage/res/icons/40x40.png",
-            "spotlight@2x": "unpackage/res/icons/80x80.png"
-          },
-          "iphone": {
-            "app@2x": "unpackage/res/icons/120x120.png",
-            "app@3x": "unpackage/res/icons/180x180.png",
-            "notification@2x": "unpackage/res/icons/40x40.png",
-            "notification@3x": "unpackage/res/icons/60x60.png",
-            "settings@2x": "unpackage/res/icons/58x58.png",
-            "settings@3x": "unpackage/res/icons/87x87.png",
-            "spotlight@2x": "unpackage/res/icons/80x80.png",
-            "spotlight@3x": "unpackage/res/icons/120x120.png"
-          }
+        "devServer" : {
+            "port" : 8383
         }
-      }
-    }
-  },
-  "quickapp": {},
-  "quickapp-native": {
-    "icon": "/static/logo.png",
-    "package": "com.example.demo",
-    "features": [
-      {
-        "name": "system.clipboard"
-      }
-    ]
-  },
-  "quickapp-webview": {
-    "icon": "/static/logo.png",
-    "package": "com.example.demo",
-    "minPlatformVersion": 1070,
-    "versionName": "1.0.0",
-    "versionCode": 100
-  },
-  "mp-weixin": {
-    "appid": "wx98df718e528399d2",
-    "setting": {
-      "urlCheck": false,
-      "minified": true,
-      "postcss": true
-    },
-    "optimization": {
-      "subPackages": true
-    },
-    "plugins": {},
-    "lazyCodeLoading": "requiredComponents",
-    "usingComponents": {},
-    "permission": {},
-    "requiredPrivateInfos": [
-      "chooseAddress"
-    ]
-  },
-  "mp-alipay": {
-    "usingComponents": true
-  },
-  "mp-baidu": {
-    "usingComponents": true
-  },
-  "mp-toutiao": {
-    "usingComponents": true
-  },
-  "mp-jd": {
-    "usingComponents": true
-  },
-  "h5": {
-    "template": "index.html",
-    "router": {
-      "mode": "hash",
-      "base": "./"
-    },
-    "sdkConfigs": {
-      "maps": {}
-    },
-    "async": {
-      "timeout": 20000
     },
-    "title": "芋道商城",
-    "optimization": {
-      "treeShaking": {
-        "enable": true
-      }
-    }
-  },
-  "vueVersion": "3",
-  "_spaceID": "192b4892-5452-4e1d-9f09-eee1ece40639",
-  "locale": "zh-Hans",
-  "fallbackLocale": "zh-Hans"
-}
+    "vueVersion" : "3",
+    "_spaceID" : "192b4892-5452-4e1d-9f09-eee1ece40639",
+    "locale" : "zh-Hans",
+    "fallbackLocale" : "zh-Hans"
+}

+ 239 - 221
pages/chat/components/chatBox.vue

@@ -1,275 +1,262 @@
 <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"
+      class="scroll"
+      :scroll-y="true"
+      :scroll-top="currentTop"
+      @scroll="handle_scroll"
+      @scrolltolower="upper"
     >
-      <!--  消息渲染  -->
-      <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="[
+      <view v-for="(data, index) in scrollData" :key="index" :id="'item-' + index">
+        <template v-if="includePage(index)">
+          <!--  消息渲染  -->
+          <view class="message-item ss-flex-col scroll-item" v-for="(item, index2) in data" :key="index">
+            <view class="ss-flex ss-row-center ss-col-center">
+              <!-- 日期 -->
+              <view v-if="item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item,data, index2)"
+                    class="date-message">
+                {{ formatDate(item.createTime) }}
+              </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="
+            >
+              <!-- 客服头像 -->
+              <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>
+                mode="aspectFill"
+              ></image>
 
-          <!-- 发送状态 -->
-          <span
-            v-if="
+              <!-- 发送状态 -->
+              <span
+                v-if="
                 item.senderType === UserTypeEnum.MEMBER &&
-                index == chatList.length - 1 &&
+                index == data.length - 1 &&
                 isSendSuccess !== 0
               "
-            class="send-status"
-          >
+                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> -->
+                <!-- <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"
+              <!-- 内容 -->
+              <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"
-              ></su-image>
+              >
+              </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>
+        </template>
+        <view v-if="!includePage(index)" :style="{ height: pagesHeight[index] }"></view>
       </view>
-      <!-- 视图滚动锚点  -->
-      <view id="scrollBottom"></view>
     </scroll-view>
+    <!-- TODO puhui999: 这里还有一点问题 -->
+    <view v-show="showGoBottom" class="go-back-btn" @click="handle_goBottom">查看最新消息</view>
   </view>
 </template>
 
 <script setup>
+  /**
+   * uniapp 实现虚拟列表
+   *
+   * see https://juejin.cn/post/7105280477141041183
+   */
+  import { nextTick, reactive, ref, unref } from 'vue';
+  import { onLoad } from '@dcloudio/uni-app';
   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 KeFuApi from '@/sheep/api/promotion/kefu';
+  import { isEmpty } from '@/sheep/helper/utils';
+  import { KeFuMessageContentTypeEnum, UserTypeEnum } from '@/pages/chat/components/constants';
   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 { safeArea } = 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 currentShowPage = ref(0); // 当前展示的页码
+  const pagesHeight = reactive([]); // 记录每个页面的高度
+  const visiblePagesList = ref([-1, 0, 1]);
+  const scrollData = ref([]);
+
+  // 向上滚动
+  const upper = async () => {
+    // 页数据满十条后加载下一页
+    if (currentShowPage.value === 0 || scrollData.value[currentShowPage.value - 1].length === 10) {
+      currentShowPage.value += 1;
+    }
+    await getMessageList();
+    await nextTick();
+    setPageHeight();
+    observer(currentShowPage.value);
+  };
+
+  // 获得消息分页列表
+  const getMessageList = async (pageNo = undefined) => {
+    const { data } = await KeFuApi.getMessageListPage({
+      pageNo: pageNo || currentShowPage.value,
+    });
+    if (isEmpty(data.list)) {
+      return;
     }
-  ];
+    scrollData.value[pageNo ? pageNo - 1 : currentShowPage.value - 1] = data.list;
+  };
+  defineExpose({ getMessageList });
+  const scrollTop = ref(0); // 当前滚动区域距离顶部的距离
+  const currentTop = ref(0);
+  const showGoBottom = ref(false);
+  // 滚动条滚动时触发
+  const handle_scroll = throttle(event => {
+    scrollTop.value = event[0].detail.scrollTop;
+    if (scrollTop > 300) {
+      showGoBottom.value = true;
+    }
+  }, 100);
+  const handle_goBottom = () => {
+    currentTop.value = scrollTop.value;
+    nextTick(() => {
+      currentTop.value = 0;
+    });
+    showGoBottom.value = false;
+  };
 
-  const isSendSuccess = ref(-1)
+  // 获取每页数据的页面高度
+  const setPageHeight = () => {
+    let query = uni.createSelectorQuery();
+    query
+      .select(`#item-${currentShowPage.value}`)
+      .boundingClientRect(res => {
+        console.log(res);
+        pagesHeight[currentShowPage.value] = res && res.height;
+      })
+      .exec();
+  };
+
+  const observer = pageNum => {
+    const observeView = wx
+      .createIntersectionObserver()
+      .relativeTo('#scroll', { top: 0, bottom: 0 });
+    observeView.observe(`#item-${pageNum}`, res => {
+      if (res.intersectionRatio > 0) visiblePagesList.value = [pageNum - 1, pageNum, pageNum + 1];
+    });
+  };
+
+  // 虚拟列表展示可视区域的数据
+  const includePage = index => {
+    return visiblePagesList.value.indexOf(index) > -1;
+  };
+
+  // 防抖
+  function throttle(fnc, delay) {
+    let timer;
+    return function() {
+      let _this = this;
+      let args = arguments;
+      if (!timer) {
+        timer = setTimeout(() => {
+          fnc.call(_this, args);
+          timer = null;
+        }, delay);
+      }
+    };
+  }
+
+  const isSendSuccess = ref(0); // 是否发送成功 -1=发送中|0=发送成功|1发送失败
+  onLoad(() => {
+    setPageHeight();
+    observer(currentShowPage.value);
+  });
   //======================= 工具函数 =======================
   /**
    * 是否显示时间
    * @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();
+  const showTime = (item, data, index) => {
+    if (unref(data)[index + 1]) {
+      let dateString = dayjs(unref(data)[index + 1].createTime).fromNow();
+      return dateString !== dayjs(unref(item).createTime).fromNow();
     }
     return false;
   };
+
   // 处理表情
   function replaceEmoji(data) {
     let newData = data;
@@ -290,6 +277,7 @@
     }
     return newData;
   }
+
   function selEmojiFile(name) {
     for (let index in emojiList) {
       if (emojiList[index].name === name) {
@@ -300,7 +288,37 @@
   }
 </script>
 
-<style scoped lang="scss">
+<style lang="scss" scoped>
+  // scroll-view 倒置,也就是说顶部是底,底部才是顶,下拉加载旧数据,回到底部就是新数据
+  .scroll {
+    transform: rotate(180deg);
+
+    & ::-webkit-scrollbar {
+      display: none;
+      width: 0;
+      height: 0;
+      color: transparent;
+    }
+  }
+
+  // 内容区域也翻转一下
+  .scroll-item {
+    transform: rotate(180deg);
+  }
+
+  .go-back-btn {
+    width: 100rpx;
+    height: 60rpx;
+    line-height: 60rpx;
+    position: fixed;
+    bottom: 100rpx;
+    left: 80%;
+    transform: translateX(-50%);
+    text-align: center;
+    background-color: lightblue;
+    border-radius: 10rpx;
+  }
+
   .chat-box {
     padding: 0 20rpx 0;
 

+ 456 - 0
pages/chat/components/chatBox1.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>

+ 37 - 81
pages/chat/index.vue

@@ -2,12 +2,12 @@
   <s-layout class="chat-wrap" title="客服" navbar="inner">
     <!-- 头部连接状态展示  -->
     <div class="status">
-      {{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
+      {{ socketState.isConnect ? "连接客服成功" : '网络已断开,请检查网络后刷新重试' }}
     </div>
     <!--  覆盖头部导航栏背景颜色  -->
     <div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
     <!--  聊天区域  -->
-    <ChatBox></ChatBox>
+    <ChatBox ref="chatBoxRef"></ChatBox>
     <!--  消息发送区域  -->
     <su-fixed bottom>
       <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
@@ -28,88 +28,50 @@
 </template>
 
 <script setup>
-  import { useChatWebSocket } from '@/pages/chat/socket';
   import ChatBox from './components/chatBox.vue';
-  import { reactive, toRefs } from 'vue';
+  import { nextTick, reactive, ref, toRefs } from 'vue';
   import sheep from '@/sheep';
   import ToolsPopup from '@/pages/chat/components/toolsPopup.vue';
   import MessageInput from '@/pages/chat/components/messageInput.vue';
   import { onLoad } from '@dcloudio/uni-app';
   import SelectPopup from '@/pages/chat/components/select-popup.vue';
+  import { KeFuMessageContentTypeEnum } from '@/pages/chat/components/constants';
+  import FileApi from '@/sheep/api/infra/file';
+  import KeFuApi from '@/sheep/api/promotion/kefu';
+  import { useWebSocket } from '@/sheep/hooks/useWebSocket';
 
   const sys_navBar = sheep.$platform.navbar;
-  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 { socketInit, state } = useWebSocket();
+  const socketState = toRefs(state).socketState;
 
   const chat = reactive({
     msg: '',
     scrollInto: '',
-
     showTools: false,
     toolsMode: '',
-
     showSelect: false,
     selectMode: '',
-    chatStyle: {
-      mode: 'inner',
-      color: '#F8270F',
-      type: 'color',
-      alwaysShow: 1,
-      src: '',
-      list: {},
-    },
   });
 
-  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;
-    }
+  async function onSendMessage() {
     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(() => {
+    try {
+      const data = {
+        contentType: KeFuMessageContentTypeEnum.TEXT,
+        content: chat.msg,
+      };
+      await KeFuApi.sendMessage(data);
+      await getMessageList()
       chat.msg = '';
-    }, 100);
+    } finally {
+      chat.showTools = false;
+    }
+  }
+
+  const chatBoxRef = ref()
+  const getMessageList = async () => {
+    await chatBoxRef.value.getMessageList(1)
   }
 
   //======================= 聊天工具相关 start =======================
@@ -125,6 +87,7 @@
 
   // 点击工具栏开关
   function onTools(mode) {
+    // TODO puhui999: socket 连接不稳定先注释掉
     // if (!socketState.value.isConnect) {
     //   sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
     //   return;
@@ -147,24 +110,18 @@
 
   async function onSelect({ type, data }) {
     let msg = '';
+    // TODO puhui999: 还需要重构
     switch (type) {
       case 'image':
-        const { path, fullurl } = await sheep.$api.app.upload(data.tempFiles[0].path, 'default');
+        const res = await FileApi.uploadFile(data.tempFiles[0].path);
         msg = {
-          from: 'customer',
-          mode: 'image',
-          date: new Date().getTime(),
-          content: {
-            url: fullurl,
-            path: path,
-          },
+          contentType: KeFuMessageContentTypeEnum.IMAGE,
+          content: res.data,
         };
         break;
       case 'goods':
         msg = {
-          from: 'customer',
-          mode: 'goods',
-          date: new Date().getTime(),
+          contentType: KeFuMessageContentTypeEnum.PRODUCT,
           content: {
             item: {
               id: data.goods.id,
@@ -178,9 +135,7 @@
         break;
       case 'order':
         msg = {
-          from: 'customer',
-          mode: 'order',
-          date: new Date().getTime(),
+          contentType: KeFuMessageContentTypeEnum.ORDER,
           content: {
             item: {
               id: data.id,
@@ -200,9 +155,7 @@
         break;
     }
     if (msg) {
-      socketSendMsg(msg, () => {
-        scrollBottom();
-      });
+      // 发送消息
       // scrollBottom();
       chat.showTools = false;
       chat.showSelect = false;
@@ -214,8 +167,11 @@
 
   onLoad(async () => {
     socketInit({}, () => {
-      scrollBottom();
+    // 监听服务端消息
     });
+    await nextTick()
+    // 加载历史消息
+    await getMessageList()
   });
 </script>
 

+ 3 - 2
sheep/api/promotion/kefu.js

@@ -15,10 +15,11 @@ const KeFuApi = {
       },
     });
   },
-  getConversation: () => {
+  getMessageListPage: (params) => {
     return request({
-      url: '/promotion/kefu-conversation/get',
+      url: '/promotion/kefu-message/page',
       method: 'GET',
+      params,
       custom: {
         auth: true,
         showLoading: false,

+ 1 - 4
sheep/hooks/useWebSocket.js

@@ -1,7 +1,4 @@
-import { reactive, ref, unref } from 'vue';
-import sheep from '@/sheep';
-// import chat from '@/sheep/api/chat';
-import dayjs from 'dayjs';
+import { reactive, ref } from 'vue';
 import io from '@hyoga/uni-socket.io';
 import { baseUrl, websocketPath } from '@/sheep/config';
 export function useWebSocket() {