فهرست منبع

企微聊天代码同步

Long 4 هفته پیش
والد
کامیت
ec22072c07
43فایلهای تغییر یافته به همراه3014 افزوده شده و 784 حذف شده
  1. 3 0
      .env.development
  2. 12 1
      src/api/common.js
  3. 62 0
      src/api/qw/im.js
  4. BIN
      src/assets/image/default-image.png
  5. BIN
      src/assets/image/qw-im.png
  6. BIN
      src/assets/voice/new-notification.mp3
  7. 2 0
      src/components/LemonUI/components/chatmsg.vue
  8. 21 21
      src/components/LemonUI/components/chatrecords/basic.vue
  9. 28 28
      src/components/LemonUI/components/chatrecords/basic1.vue
  10. 28 28
      src/components/LemonUI/components/chatrecords/segrecords.vue
  11. 28 2
      src/components/LemonUI/components/contact.vue
  12. 6 7
      src/components/LemonUI/components/editor.vue
  13. 27 11
      src/components/LemonUI/components/index.vue
  14. 1 1
      src/components/LemonUI/components/message/elise-audio.vue
  15. 29 0
      src/components/LemonUI/components/message/emotionDynamic.vue
  16. 3 4
      src/components/LemonUI/components/message/image.vue
  17. 118 0
      src/components/LemonUI/components/message/miniprogram.vue
  18. 0 1
      src/components/LemonUI/components/message/text.vue
  19. 3 3
      src/components/LemonUI/components/message/video.vue
  20. 30 3
      src/components/LemonUI/components/message/voice.vue
  21. 9 9
      src/components/LemonUI/components/records.vue
  22. 482 167
      src/components/LemonUI/database/emoji.js
  23. 0 0
      src/components/LemonUI/index.css
  24. 5 1
      src/components/LemonUI/index.js
  25. 7 1
      src/components/LemonUI/lastContentRender.js
  26. 2 2
      src/components/LemonUI/styles/common/icons.styl
  27. 1 1
      src/components/LemonUI/styles/common/index.styl
  28. 7 7
      src/components/LemonUI/styles/common/normalize.styl
  29. 1 1
      src/components/LemonUI/styles/utils/bem.styl
  30. 9 9
      src/components/LemonUI/styles/utils/functional.styl
  31. 1 1
      src/components/LemonUI/utils/constant.js
  32. 80 2
      src/layout/index.vue
  33. 5 1
      src/main.js
  34. 3 1
      src/store/index.js
  35. 29 0
      src/store/modules/qwIm.js
  36. 67 0
      src/utils/ImSocket.js
  37. 43 1
      src/utils/common.js
  38. 575 470
      src/views/qw/qwChat/qq.vue
  39. 423 0
      src/views/qw/qwChat/userDetail/courseManage.vue
  40. 335 0
      src/views/qw/qwChat/userDetail/index.vue
  41. 111 0
      src/views/qw/qwChat/userDetail/userCourseWatchLog.vue
  42. 245 0
      src/views/qw/qwChat/userDetail/userInfoEdit.vue
  43. 173 0
      src/views/qw/qwChat/userDetail/userStorerDetails.vue

+ 3 - 0
.env.development

@@ -25,3 +25,6 @@ VUE_CLI_BABEL_TRANSPILE_MODULES = true
 VUE_APP_PATIENT_INFO = '客户信息'
 # 添加病人
 VUE_APP_ADD_PATIENT = '添加信息'
+
+# IM webSocket地址
+VUE_APP_IM_WS_URL = ws://127.0.0.1:8667/qwImSocket

+ 12 - 1
src/api/common.js

@@ -14,4 +14,15 @@ export function getTask(taskId) {
     method: 'get'
   })
 }
- 
+
+// 上传文件
+export function uploadOss(data) {
+  return request({
+    url: '/common/uploadOSS',
+    method: 'post',
+    headers: {
+      isToken: false
+    },
+    data: data
+  })
+}

+ 62 - 0
src/api/qw/im.js

@@ -91,6 +91,68 @@ export function exportMessage(query) {
   })
 }
 
+// 获取外部联系人详情
+export function getQwExternalContactDetails(query) {
+  return request({
+    url: '/qw/qwMsg/getQwExternalContactDetails',
+    method: 'get',
+    params: query
+  })
+}
+
+// 获取用户信息
+export function getQwUserInfo(query) {
+  return request({
+    url: '/qw/qwMsg/getQwUserInfo',
+    method: 'get',
+    params: query
+  })
+}
+
+// 修改用户信息
+export function updateQwUserInfo(data) {
+  return request({
+    url: '/qw/qwMsg/updateQwUserInfo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取看课记录
+export function queryCourseWatchStatistics(data) {
+  return request({
+    url: '/qw/qwMsg/course/watch',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取课程列表
+export function getFsCourseListBySidebar(data) {
+  return request({
+    url: '/qw/qwMsg/getFsCourseListBySidebar',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取课程小节列表
+export function getFsCourseVideoListBySidebar(data) {
+  return request({
+    url: '/qw/qwMsg/getFsCourseVideoListBySidebar',
+    method: 'post',
+    data: data
+  })
+}
+
+// 创建小程序链接
+export function createMiniLink(data) {
+  return request({
+    url: '/qw/qwMsg/createMiniLink',
+    method: 'post',
+    data: data
+  })
+}
 
 
 

BIN
src/assets/image/default-image.png


BIN
src/assets/image/qw-im.png


BIN
src/assets/voice/new-notification.mp3


+ 2 - 0
src/components/LemonUI/components/chatmsg.vue

@@ -487,6 +487,7 @@ export default {
       );
     },
     _renderSidebar(children, name, fixedtop) {
+      console.log("_renderSidebar", children)
       return (
         <div
           class="lemon-sidebar"
@@ -649,6 +650,7 @@ export default {
       lastContentRender[messageType] = render;
     },
     lastContentRender(message) {
+      console.log("msg", message)
       if (!isFunction(lastContentRender[message.type])) {
         console.error(
           `not found '${

+ 21 - 21
src/components/LemonUI/components/chatrecords/basic.vue

@@ -2,7 +2,7 @@
 <div>
 
     <el-dialog   :visible.sync="dialogVisible" width="70%">
-        <div class="app-container">
+        <div class="app-container">  
              <el-form ref="form" :model="form" :inline="true"  label-width="80px">
                 <el-row>
                     <el-col :span="22">
@@ -21,7 +21,7 @@
              <el-tabs v-model="activeName" @tab-click="handleTabClick">
                 <el-tab-pane label="全部" name="first">
                     <SegRecords :qwUser="qwUser" :conversationId="conversationId" :conversationData="conversationData" :msgType=0  ref="sIMUI0" v-show="!showSearch"></SegRecords>
-
+                 
                     <SegRecords :qwUser="qwUser" :conversationId="conversationId" :conversationData="conversationData" :msgType=0  ref="sIMUI"  @showContextBox="showContextBox"  v-show="showSearch"  ></SegRecords>
                 </el-tab-pane>
 
@@ -37,7 +37,7 @@
                     <SegRecords :qwUser="qwUser" :conversationId="conversationId" :conversationData="conversationData" :msgType=3  ref="sIMUI3"></SegRecords>
                 </el-tab-pane>
             </el-tabs>
-
+          
         </div>
     </el-dialog>
 
@@ -62,7 +62,7 @@
   let pages = {};
   export default {
        name: "ChatRecords",
-       components: {
+       components: {   
           VideoPlayer,
           SegRecords
        },
@@ -99,7 +99,7 @@
                 isFromLogin:false,
                 dialogImgVisible:false,
                 dialogImageUrl: '',
-                dialogVideoVisible:false,
+                dialogVideoVisible:false, 
                 dialogVideoUrl:'',
                 dialogVideoCover:'',
                 imageArr:[],
@@ -133,10 +133,10 @@
            }
        },
        created(){
-
+           
        },
        mounted(){
-
+           
        },
        methods: {
                 handleTabClick(tab, event){
@@ -177,7 +177,7 @@
                     //IMUI.changeContact(this.queryParams.conversationId);
                     //this.getContacts();  //获取联系人信息
                     this.getConversation();   //获取会话信息
-                    ////IMUI.initEmoji(EmojiData);
+                    ////IMUI.initEmoji(EmojiData); 
                 },
                 open(qwUser,conversationId,conversationData){
                      this.dialogVisible=true;
@@ -199,11 +199,11 @@
                         this.roomInfo=response.detail;
                     });
                 },
-                getContacts(){
+                getContacts(){  
                     getContactsByUser(this.qwUser.userId).then(response => {
                         this.contactData = response.data;
                         this.$refs.IMUI.initContacts(this.contactData);
-
+                        
                     });
                 },
                 getConversation(){
@@ -280,7 +280,7 @@
                 },
                 //收到消息后添加消息显示
                 appendMessageAction(msgData){
-                    if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){     //文本   text  image  video  voice
+                    if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){     //文本   text  image  video  voice 
                         const message = {
                                 id: msgData.id,
                                 status: msgData.status,
@@ -325,8 +325,8 @@
                 console.log("Event:menu-avatar-click");
                 },
                 //聊天工具栏点击图片
-                handleImageClick() {
-
+                handleImageClick() { 
+                
                 },
                 //选择图片框确定按钮回调
                 handlePickImageDone(data){
@@ -349,11 +349,11 @@
                         var url=!!message.url?message.url:message.content;
                         this.handlePicturePreview(url);
                     }
-                    else if(message.type=="video"){
+                    else if(message.type=="video"){  
                         this.tooglePlayVideo(message);
                     }
-                    else if(message.type=="file"){
-
+                    else if(message.type=="file"){  
+                        
                     }
                     if (key == "status") {
                         instance.updateMessage({
@@ -491,7 +491,7 @@
        padding-top:0;
     }
     .imui-center{
-        margin-bottom:"60px";
+        margin-bottom:"60px"; 
         display: flex;
         align-items: center;
         justify-content: center;
@@ -543,7 +543,7 @@
     // /deep/.el-dialog__header{
     //     padding-bottom: 0;
     // }
-
+    
 </style>
 
 
@@ -556,7 +556,7 @@
         padding:0 0 10px 0;
         .slot-group-notice
            padding: 10px 10px;
-
+     
       .slot-search
            width:calc(100% - 20px) ;
            margin:5px 10px;
@@ -573,11 +573,11 @@
               height: 30px;
               line-height: 30px;
               margin-right:5px;
-              img
+              img 
                 vertical-align: middle;
                 border-style: none;
                 width: 100%;
                 height: 100%;
                 line-height: 30px;
                 border-radius: 50%;
-</style>
+</style>

+ 28 - 28
src/components/LemonUI/components/chatrecords/basic1.vue

@@ -2,7 +2,7 @@
 <div>
 
     <el-dialog   :visible.sync="dialogVisible" width="70%">
-        <div class="app-container">
+        <div class="app-container">  
              <el-form ref="form" :model="form" :inline="true"  label-width="80px">
                 <el-row>
                     <el-col :span="22">
@@ -19,11 +19,11 @@
               </el-form>
 
             <div class="imui-center qq-lemon-imui">
-                <lemon-records  class="lemon-slot"
+                <lemon-records  class="lemon-slot"  
                     :width="windowWidth"
                     :height="windowHeight"
                     :user="UserData"
-                    ref="IMUI"
+                    ref="IMUI"  
                     :theme="theme"
                     :hide-menu="hideMenu"
                     :hide-menu-avatar="hideMenuAvatar"
@@ -54,10 +54,10 @@
                                 <span style="font-size:12px;">
                                 <!-- <span>打开抽屉:</span>
                                 <span style="cursor:pointer;"  @click="openDrawer('right')">
-                                    右侧  |
+                                    右侧  | 
                                 </span>
                                 <span style="cursor:pointer;" @click="openDrawer('rightInside')">
-                                    右侧内部  |
+                                    右侧内部  | 
                                 </span>
                                 <span style="cursor:pointer;" @click="openDrawer('center')">
                                     居中
@@ -70,7 +70,7 @@
                                 <span>相册</span>
                                 <span>文件</span>
                                 <span>活动</span>
-                                <span>设置(左键弹出菜单)</span>
+                                <span>设置(左键弹出菜单)</span> 
                             </div> -->
                         </div>
                     </template>
@@ -78,7 +78,7 @@
 
             </div>
 
-
+            
         </div>
     </el-dialog>
 
@@ -90,10 +90,10 @@
                :close-on-click-modal="false"
                :destroy-on-close="true"
                :width="aplayer.pWidth"
-               :height="aplayer.height"
+               :height="aplayer.height" 
                @close="dialogVideoVisible=false"
                ref="player">
-              <videoPlayer  :width="aplayer.width" :height="aplayer.height"  :videoWidth="aplayer.videoWidth"  :videoHeight="aplayer.videoHeight"   :vid="aplayer.vid"  :autoplay="true"
+              <videoPlayer  :width="aplayer.width" :height="aplayer.height"  :videoWidth="aplayer.videoWidth"  :videoHeight="aplayer.videoHeight"   :vid="aplayer.vid"  :autoplay="true" 
                 :source="dialogVideoUrl" :cover="dialogVideoCover" ref="player">
               </videoPlayer>
        </el-dialog>
@@ -113,7 +113,7 @@
   let pages = {};
   export default {
        name: "ChatRecords",
-       components: {
+       components: {   
           VideoPlayer,
        },
        data(){
@@ -139,7 +139,7 @@
                 isFromLogin:false,
                 dialogImgVisible:false,
                 dialogImageUrl: '',
-                dialogVideoVisible:false,
+                dialogVideoVisible:false, 
                 dialogVideoUrl:'',
                 dialogVideoCover:'',
                 imageArr:[],
@@ -151,7 +151,7 @@
                     height:"900px",
                     videoWidth:"1000px",
                     videoHeight:"900px"
-
+                    
                 },
                 player: null,
                 roomMembers:[],
@@ -174,7 +174,7 @@
            }
        },
         created(){
-
+           
        },
        mounted(){
             this.deviceId=this.qwUser.deviceId;
@@ -185,14 +185,14 @@
        methods: {
                 /** 搜索按钮操作 */
                 handleQuery() {
-
+                   
                 },
                 init(){
                     const IMUI = this.$refs.IMUI;
                     //IMUI.changeContact(this.queryParams.conversationId);
                     //this.getContacts();  //获取联系人信息
                     this.getConversation();   //获取会话信息
-                    ////IMUI.initEmoji(EmojiData);
+                    ////IMUI.initEmoji(EmojiData); 
                 },
                 open(conversationId,conversationData){
                      this.conversationData=conversationData;
@@ -212,11 +212,11 @@
                         this.roomInfo=response.detail;
                     });
                 },
-                getContacts(){
+                getContacts(){  
                     getContactsByUser(this.qwUser.userId).then(response => {
                         this.contactData = response.data;
                         this.$refs.IMUI.initContacts(this.contactData);
-
+                        
                     });
                 },
                 getConversation(){
@@ -293,7 +293,7 @@
                 },
                 //收到消息后添加消息显示
                 appendMessageAction(msgData){
-                    if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){     //文本   text  image  video  voice
+                    if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){     //文本   text  image  video  voice 
                         const message = {
                                 id: msgData.id,
                                 status: msgData.status,
@@ -338,8 +338,8 @@
                 console.log("Event:menu-avatar-click");
                 },
                 //聊天工具栏点击图片
-                handleImageClick() {
-
+                handleImageClick() { 
+                
                 },
                 //选择图片框确定按钮回调
                 handlePickImageDone(data){
@@ -362,11 +362,11 @@
                         var url=!!message.url?message.url:message.content;
                         this.handlePicturePreview(url);
                     }
-                    else if(message.type=="video"){
+                    else if(message.type=="video"){  
                         this.tooglePlayVideo(message);
                     }
-                    else if(message.type=="file"){
-
+                    else if(message.type=="file"){  
+                        
                     }
                     if (key == "status") {
                         instance.updateMessage({
@@ -504,7 +504,7 @@
        padding-top:0;
     }
     .imui-center{
-        margin-bottom:"60px";
+        margin-bottom:"60px"; 
         display: flex;
         align-items: center;
         justify-content: center;
@@ -552,7 +552,7 @@
     // /deep/.el-dialog__header{
     //     padding-bottom: 0;
     // }
-
+    
 </style>
 
 
@@ -565,7 +565,7 @@
         padding:0 0 10px 0;
         .slot-group-notice
            padding: 10px 10px;
-
+     
       .slot-search
            width:calc(100% - 20px) ;
            margin:5px 10px;
@@ -582,11 +582,11 @@
               height: 30px;
               line-height: 30px;
               margin-right:5px;
-              img
+              img 
                 vertical-align: middle;
                 border-style: none;
                 width: 100%;
                 height: 100%;
                 line-height: 30px;
                 border-radius: 50%;
-</style>
+</style>

+ 28 - 28
src/components/LemonUI/components/chatrecords/segrecords.vue

@@ -2,11 +2,11 @@
 <div>
 
     <div class="imui-center qq-lemon-imui">
-        <lemon-records  class="lemon-slot"
+        <lemon-records  class="lemon-slot"  
             :width="windowWidth"
             :height="windowHeight"
             :user="UserData"
-            ref="IMUI"
+            ref="IMUI"  
             :sendText="sendText"
             :theme="theme"
             :show-context="showContext"
@@ -39,16 +39,16 @@
         <img style="width:100%;height:auto" :src="dialogImageUrl" alt="" />
     </el-dialog>
 
-     <el-dialog :visible.sync="dialogVideoVisible"
-                append-to-body
+     <el-dialog :visible.sync="dialogVideoVisible" 
+                append-to-body 
                :close-on-click-modal="false"
                :destroy-on-close="true"
                :width="aplayer.pWidth"
-               :height="aplayer.height"
+               :height="aplayer.height" 
                @close="dialogVideoVisible=false"
                ref="player">
-              <videoPlayer  :width="aplayer.width" :height="aplayer.height"  :videoWidth="aplayer.videoWidth"
-                :videoHeight="aplayer.videoHeight"   :vid="aplayer.vid"  :autoplay="true"
+              <videoPlayer  :width="aplayer.width" :height="aplayer.height"  :videoWidth="aplayer.videoWidth"  
+                :videoHeight="aplayer.videoHeight"   :vid="aplayer.vid"  :autoplay="true" 
                 :source="dialogVideoUrl" :cover="dialogVideoCover" ref="player">
               </videoPlayer>
        </el-dialog>
@@ -67,7 +67,7 @@
   let pages = {};
   export default {
        name: "SegRecords",
-       components: {
+       components: {   
           VideoPlayer,
        },
        props: {
@@ -115,7 +115,7 @@
                 },
                 dialogImgVisible:false,
                 dialogImageUrl: '',
-                dialogVideoVisible:false,
+                dialogVideoVisible:false, 
                 dialogVideoUrl:'',
                 dialogVideoCover:'',
                 imageArr:[],
@@ -142,11 +142,11 @@
                 },
                 isLoaded:false,
                 sendText:"",
-
+               
            }
        },
        created(){
-
+            
        },
        mounted(){
             //this.deviceId=this.qwUser.deviceId;
@@ -155,20 +155,20 @@
             this.UserData.avatar=this.qwUser.avatar;
             this.queryParams.msgType=this.msgType;
             //this.open();
-
+           
        },
        methods: {
                 /** 搜索按钮操作 */
                 handleQuery() {
-
+                   
                 },
                 handleClose(){
-
-                },
+                   
+                }, 
                 init(){
                     const IMUI = this.$refs.IMUI;
                     this.getConversation();   //获取会话信息
-                    ////IMUI.initEmoji(EmojiData);
+                    ////IMUI.initEmoji(EmojiData); 
                 },
                 open(converChanged){
                       if(this.isLoaded && !converChanged){
@@ -192,7 +192,7 @@
                     }, 100);
                 },
                 //查看上下文
-                openContextBox(message){
+                openContextBox(message){ 
                       pages=[];
                       this.queryParams.messageId=message.id;
                       this.queryParams.pullAct=0;//下拉刷新
@@ -294,7 +294,7 @@
                 handleChangeContact(contact, instance) {
 
                 },
-
+            
                 handleMenuAvatarClick() {
                    console.log("Event:menu-avatar-click");
                 },
@@ -302,7 +302,7 @@
                     this.dialogVideoVisible=true;
                     this.dialogVideoUrl=data.content;
                     this.dialogVideoCover=data.url;
-                    const player = this.$refs.player.instance
+                    const player = this.$refs.player.instance 
                     player && player.play()
                 },
                 handleMessageClick(e, key, message, instance) {
@@ -312,11 +312,11 @@
                         var url=!!message.url?message.url:message.content;
                         this.handlePicturePreview(url);
                     }
-                    else if(message.type=="video"){
+                    else if(message.type=="video"){  
                         this.tooglePlayVideo(message);
                     }
-                    else if(message.type=="file"){
-
+                    else if(message.type=="file"){  
+                        
                     }
                     if (key == "status") {
                         instance.updateMessage({
@@ -348,7 +348,7 @@
                 changeMessageTimeVisible() {
                    this.hideMessageTime = !this.hideMessageTime;
                 },
-
+    
                 updateContact() {
                     this.$refs.IMUI.updateContact({
                         id: "contact-3",
@@ -377,7 +377,7 @@
        padding-top:0;
     }
     .imui-center{
-        margin-bottom:"60px";
+        margin-bottom:"60px"; 
         display: flex;
         align-items: center;
         justify-content: center;
@@ -429,7 +429,7 @@
     // /deep/.el-dialog__header{
     //     padding-bottom: 0;
     // }
-
+    
 </style>
 
 
@@ -442,7 +442,7 @@
         padding:0 0 10px 0;
         .slot-group-notice
            padding: 10px 10px;
-
+     
       .slot-search
            width:calc(100% - 20px) ;
            margin:5px 10px;
@@ -459,11 +459,11 @@
               height: 30px;
               line-height: 30px;
               margin-right:5px;
-              img
+              img 
                 vertical-align: middle;
                 border-style: none;
                 width: 100%;
                 height: 100%;
                 line-height: 30px;
                 border-radius: 50%;
-</style>
+</style>

+ 28 - 2
src/components/LemonUI/components/contact.vue

@@ -21,7 +21,7 @@ export default {
     timeFormat: {
       type: Function,
       default(val) {
-        return timeFormat(val, isToday(val) ? "h:i" : "y/m/d");
+        return timeFormat(val, isToday(val) ? "h:i" : "m/d");
       },
     },
   },
@@ -56,7 +56,11 @@ export default {
         </lemon-badge>,
         <div class="lemon-contact__inner">
           <p class="lemon-contact__label">
-            <span class="lemon-contact__name">{contact.displayName}</span>
+            <span class="lemon-contact__name">
+              {contact.displayName}
+              {contact.isRepeat && <span class="lemon-contact__chong">重</span>}
+              {contact.isBlack && <span class="lemon-contact__hei">黑</span>}
+            </span>
             {!this.simple && (
               <span class="lemon-contact__time">
                 {this.timeFormat(contact.lastSendTime)}
@@ -142,4 +146,26 @@ export default {
     +e(label)
       padding-bottom 0
       line-height 38px
+  +e(chong)
+    display inline-flex
+    align-items center
+    justify-content center
+    width 20px
+    height 20px
+    font-size 12px
+    border-radius 5px
+    background-image: linear-gradient(-225deg, #231557 0%, #44107A 29%, #FF1361 67%, #FFF800 100%);
+    color white
+    margin-left 5px
+  +e(hei)
+    display inline-flex
+    align-items center
+    justify-content center
+    width 20px
+    height 20px
+    font-size 12px
+    border-radius 5px
+    background-image: linear-gradient(-20deg, #2b5876 0%, #4e4376 100%);
+    color white
+    margin-left 5px
 </style>

+ 6 - 7
src/components/LemonUI/components/editor.vue

@@ -296,11 +296,11 @@ export default {
       this._checkSubmitDisabled();
     },
     async selectFile(accept) {
-        if(accept=="*"){
-            this.accept = accept;
-            await this.$nextTick();
-            this.$refs.fileInput.click();
-        }else{
+        this.accept = accept;
+        await this.$nextTick();
+        this.$refs.fileInput.click();
+
+        if(accept === "image/*") {
             this.$emit("pickImg", this.clipboardBlob);
         }
     },
@@ -473,8 +473,7 @@ gap = 10px;
     p,div
       margin 0
     img
-      height 20px
-      padding 0 2px
+      height 18px
       pointer-events none
       position relative
       top -1px

+ 27 - 11
src/components/LemonUI/components/index.vue

@@ -234,7 +234,7 @@ export default {
     },
     _handleSend(text) {
       const message = this._createMessage({ content: text });
-      this.appendMessage(message, true);
+      // this.appendMessage(message, true);
       this._emitSend(message, () => {
         this.updateContact({
           id: message.toContactId,
@@ -394,6 +394,9 @@ export default {
         [
           useScopedSlot(this.$scopedSlots["sidebar-message-top"], null, this),
           this.lastMessages.map(contact => {
+            if (contact.type !== 'text' && isFunction(lastContentRender[contact.type])) {
+              contact.lastContent =  lastContentRender[contact.type].call(this, contact);
+            }
             return this._renderContact(
               {
                 contact,
@@ -448,7 +451,7 @@ export default {
           useScopedSlot(this.$scopedSlots["sidebar-contact-top"], null, this),
           this.contacts.map(contact => {
             if (!contact.index) return;
-            contact.index = contact.index.replace(/\[[0-9]*\]/, "");
+            contact.index = contact.index.replace(/\[[0-9]*]/, "");
             const node = [
               contact.index !== prevIndex && (
                 <p class="lemon-sidebar__label">{contact.index}</p>
@@ -574,6 +577,18 @@ export default {
           </div>
         </div>,
       );
+
+      // 拓展
+      nodes.push(
+        <div
+          class={cls}
+          style="flex: 1 1 0;width: 0;"
+          v-show={this._menuIsMessages() && defIsShow && curact.id}
+        >
+          {useScopedSlot(this.$scopedSlots["message-extend"], null, curact)}
+        </div>,
+      );
+
       nodes.push(
         <div class={cls} v-show={!curact.id && this.currentIsDefSidebar}>
           {this.$slots.cover}
@@ -646,7 +661,7 @@ export default {
         );
         return "";
       }
-      return lastContentRender[message.type].call(this, message);
+      return lastContentRender[message.type].call(this, message.content);
     },
     /**
      * 将字符串内的 EmojiItem.name 替换为 img
@@ -654,15 +669,15 @@ export default {
      * @return {String} 替换后的字符串
      */
     emojiNameToImage(str) {
-      return str.replace(/\[!(\w+)\]/gi, (str, match) => {
+      return str.replace(/\[([^\]]+)]/gi, (str, match) => {
         const file = match;
         return emojiMap[file]
           ? `<img emoji-name="${match}" src="${emojiMap[file]}" />`
-          : `[!${match}]`;
+          : `[${match}]`;
       });
     },
     emojiImageToName(str) {
-      return str.replace(/<img emoji-name=\"([^\"]*?)\" [^>]*>/gi, "[!$1]");
+      return str.replace(/<img emoji-name="([^"]*?)" [^>]*>/gi, "[$1]");
     },
     updateCurrentMessages() {
       if (!allMessages[this.currentContactId]){
@@ -762,13 +777,15 @@ export default {
       if (!allMessages[contactId]) {
           this.updateCurrentMessages();
           this._emitPullMessages(isEnd => {
+            setTimeout(() => {
               this.messageViewToBottom();
+            }, 10);
           });
       } else {
-          setTimeout(() => {
-              this.updateCurrentMessages();
-              this.messageViewToBottom();
-          }, 0);
+        this.updateCurrentMessages();
+        setTimeout(() => {
+            this.messageViewToBottom();
+        }, 10);
       }
     },
     /**
@@ -926,7 +943,6 @@ export default {
     initConversations(data) {
       this.conversations = data;
       this.sortConversations();
-
     },
 
     /**

+ 1 - 1
src/components/LemonUI/components/message/elise-audio.vue

@@ -83,7 +83,7 @@
     &.active {
 		  opacity: 0.8;
     }
-
+    
 	}
 	.flex {
 		display: flex;

+ 29 - 0
src/components/LemonUI/components/message/emotionDynamic.vue

@@ -0,0 +1,29 @@
+<script>
+export default {
+  name: 'lemonMessageEmotionDynamic',
+  inheritAttrs: false,
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-emotion-dynamic"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => <img src={props.content} />
+        }}
+      />
+    )
+  }
+}
+</script>
+
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-emotion-dynamic)
+  +b(lemon-message)
+    +e(content)
+      width 100px
+      height 100px
+      img
+        width 100%
+        height 100%
+</style>

+ 3 - 4
src/components/LemonUI/components/message/image.vue

@@ -20,13 +20,12 @@ export default {
 +b(lemon-message-image)
   +b(lemon-message)
     +e(content)
-      padding 0
+      background-color #FFF !important
       cursor pointer
       overflow hidden
-      max-width 20%
-      min-width 100px
+      min-width 38px
       img
-        width: 100%
+        max-width: 150px
         height: auto
         display block
 </style>

+ 118 - 0
src/components/LemonUI/components/message/miniprogram.vue

@@ -0,0 +1,118 @@
+<script>
+import SvgIcon from '@/components/SvgIcon'
+
+export default {
+  name: "lemonMessageMiniprogram",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  components: {
+    SvgIcon
+  },
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-miniprogram"
+        props={{...this.$attrs}}
+        scopedSlots={{
+          content: props => {
+            return this.renderHtml(props.content)
+          }
+        }}
+      />
+    );
+  },
+  methods: {
+    renderHtml(data) {
+      let content = data
+      if (typeof data === 'string') {
+        try {
+          // 尝试将字符串解析为JSON对象
+          content = JSON.parse(data)
+        } catch (e) {
+          console.error('解析小程序消息内容失败:', e)
+          content = { title: '小程序' }
+        }
+      }
+      
+      return (
+        <div>
+          <div class="lemon-message-miniprogram__header">
+            <div class="lemon-message-miniprogram__title">{content.title || '小程序'}</div>
+          </div>
+          <div class="lemon-message-miniprogram__body">
+            {content.thumbnail && (
+              <div class="lemon-message-miniprogram__cover">
+                <img src={content.thumbnail} alt=""/>
+              </div>
+            )}
+          </div>
+          <div class="lemon-message-miniprogram__footer">
+            <svg-icon icon-class="miniprogram" class="lemon-message-miniprogram__icon"></svg-icon>
+            <span>小程序</span>
+          </div>
+        </div>
+      )
+    }
+  }
+};
+</script>
+
+<style lang="stylus">
+@import '../../styles/utils/index'
+
++b(lemon-message-miniprogram)
+  +b(lemon-message)
+    +e(content)
+      width 240px // 设置小程序卡片宽度
+      border-radius 4px // 设置圆角
+      background-color #fff // 设置背景色
+      overflow hidden // 防止内容溢出
+      box-shadow 0 2px 8px rgba(0, 0, 0, 0.1) // 添加阴影效果
+      padding 0 // 重置内边距
+
+  +e(header)
+    padding 10px // 设置内边距
+    border-bottom 1px solid #f0f0f0 // 添加底部边框
+
+  +e(title)
+    font-size 14px // 设置字体大小
+    font-weight 500 // 设置字体粗细
+    color #333 // 设置字体颜色
+    line-height 1.4 // 设置行高
+    overflow hidden // 防止文本溢出
+    text-overflow ellipsis // 文本溢出显示省略号
+    white-space nowrap // 不换行
+
+  +e(body)
+    position relative // 设置相对定位
+
+  +e(cover)
+    width 100% // 设置图片容器宽度
+    height 120px // 设置图片容器高度
+    overflow hidden // 防止图片溢出
+
+    img
+      width 100% // 设置图片宽度
+      height 100% // 设置图片高度
+      object-fit cover // 图片填充方式
+      display block // 设置为块级元素
+      background transparent // 设置背景透明
+
+  +e(footer)
+    display flex // 使用弹性布局
+    align-items center // 垂直居中
+    padding 8px 10px // 设置内边距
+    font-size 12px // 设置字体大小
+    color #999 // 设置字体颜色
+    background-color #f8f8f8 // 设置背景色
+
+    i
+      margin-right 4px // 设置图标右边距
+      font-size 14px // 设置图标大小
+      color color-primary // 使用主题色变量
+
+  +e(icon)
+    margin-right 4px // 设置右边距
+    font-size 16px // 设置图标大小
+    color color-primary // 使用主题色变量
+</style>

+ 0 - 1
src/components/LemonUI/components/message/text.vue

@@ -31,6 +31,5 @@ export default {
         background transparent
         position relative
         top -1px
-        padding 0 2px
         vertical-align middle
 </style>

+ 3 - 3
src/components/LemonUI/components/message/video.vue

@@ -9,7 +9,7 @@ export default {
     };
   },
   created() {
-
+    
   },
   render() {
     return (
@@ -25,8 +25,8 @@ export default {
     );
   },
    methods: {
-      renderHtml(data){
-        return <div><img class="imgc" src={data.imageUrl} /><img class="btnPlay" src={this.url} /> </div>;
+      renderHtml(data){       
+        return <div><img class="imgc" src={data.imageUrl} /><img class="btnPlay" src={this.url} /> </div>;  
         // return <span>{props.content}&nbsp;🔈</span>;
       }
   }

+ 30 - 3
src/components/LemonUI/components/message/voice.vue

@@ -26,8 +26,16 @@ export default {
   },
    methods: {
       renderHtml(data){
-        return <audio block="true" showDuration='false' duration={data.duration} controls src={data.content}  ></audio>;
-        // return <span>{props.content}&nbsp;🔈</span>;
+        const {url, content} = JSON.parse(data.content)
+        return <div class="lemon-message-voice-context">
+            <audio block="true" showDuration='false' controls src={url}
+            controlslist='nodownload noplaybackrate'/>
+            <div class="lemon-message-voice-context-text">
+              <span>{content}</span>
+              <span class="el-icon-check lemon-message-voice-context-text-transfer">转换完成</span>
+            </div>
+          </div>
+          ;
       }
   }
 };
@@ -38,10 +46,29 @@ export default {
 .lemon-message.lemon-message-voice
   user-select none
   .lemon-message__content
+    background-color unset
     border 0px solid #000
     font-size 12px
     cursor pointer
     &::before
       display none
-
+    .lemon-message-voice-context
+      display flex
+      flex-direction: column
+      audio
+        width 250px
+        height 35px
+        border solid 1px #CCC
+        border-radius 15px
+      .lemon-message-voice-context-text
+        display flex
+        flex-direction: column
+        background-color #FFF
+        padding 5px 10px
+        margin-top 10px
+        border-radius 5px
+        .lemon-message-voice-context-text-transfer
+          margin 10px 0
+          font-size x-small
+          color #999
 </style>

+ 9 - 9
src/components/LemonUI/components/records.vue

@@ -194,7 +194,7 @@ export default {
         if (message.type == 'event'){
             // if(this.user.id == message.fromUser.id){}
             unread = 0;
-        }
+        } 
         if (messageList === undefined) {
           //console.log("qxj appendMessage messageList:"+messageList+",message"+JSON.stringify(message));
           conversation.id=message.toContactId;
@@ -207,7 +207,7 @@ export default {
           let hasMsg = messageList.some(({id})=>id == message.id);
           if (hasMsg) return;
           this._addMessage(message, message.toContactId, 1);
-
+          
           conversation.lastContent=this.lastContentRender(message);
           conversation.lastSendTime=message.sendTime;
           if (message.toContactId == this.currentContactId) {
@@ -543,7 +543,7 @@ export default {
             </div>,
           );
       }
-
+      
       //聊天消息View
       nodes.push(
         <div
@@ -574,9 +574,9 @@ export default {
                 on-reach-bottom={this._emitPullUpMessages}
                 messages={this.currentMessages}
               />
-
+          
             </div>
-
+           
           </div>
         </div>,
       );
@@ -698,7 +698,7 @@ export default {
         editorValue,
         lastContent,
       });
-
+      
       var tempDraft=this.CacheDraft.get(cid);
       //console.log("qxj editorValue:"+editorValue+",tempDraft:"+JSON.stringify(tempDraft)+"lastContent:"+lastContent);
 
@@ -932,7 +932,7 @@ export default {
     initConversations(data) {
       this.conversations = data;
       this.sortConversations();
-
+      
     },
 
     /**
@@ -1023,7 +1023,7 @@ export default {
      */
     updateContact(data) {
       const conversationId = data.conversationId;
-      const index = this.findConversationIndexById(conversationId);
+      const index = this.findConversationIndexById(conversationId);  
       if (index !== -1) {
         // if(isString(data.unread)) {
         //     if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
@@ -1039,7 +1039,7 @@ export default {
     updateContact1(data) {
       const contactId = data.id;
       delete data.id;
-      const index = this.findContactIndexById(contactId);
+      const index = this.findContactIndexById(contactId);  
       if (index !== -1) {
         const { unread } = data;
         if (isString(unread)) {

+ 482 - 167
src/components/LemonUI/database/emoji.js

@@ -3,274 +3,549 @@ export default [
     label: "表情",
     children: [
       {
-        name: "1f600",
+        name: "微笑",
         title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f600.png",
+        src: "https://www.emojiall.com/images/60/wechat/1f642.png",
       },
       {
-        name: "1f62c",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f62c.png",
+        name: "撇嘴",
+        title: "撇嘴",
+        src: "https://www.emojiall.com/images/60/wechat/1f61f.png",
       },
       {
-        name: "1f601",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f601.png",
+        name: "",
+        title: "",
+        src: "https://www.emojiall.com/images/60/wechat/1f60d.png",
       },
       {
-        name: "1f602",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f602.png",
+        name: "发呆",
+        title: "发呆",
+        src: "https://www.emojiall.com/images/60/wechat/1f626.png",
       },
       {
-        name: "1f923",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f923.png",
+        name: "得意",
+        title: "得意",
+        src: "https://www.emojiall.com/images/60/wechat/1f60e.png",
       },
       {
-        name: "1f973",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f973.png",
+        name: "流泪",
+        title: "流泪",
+        src: "https://www.emojiall.com/images/60/wechat/1f62d.png",
       },
       {
-        name: "1f603",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f603.png",
+        name: "害羞",
+        title: "害羞",
+        src: "https://www.emojiall.com/images/60/wechat/1f60a.png",
       },
       {
-        name: "1f604",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f604.png",
+        name: "闭嘴",
+        title: "闭嘴",
+        src: "https://www.emojiall.com/images/60/wechat/1f910.png",
       },
       {
-        name: "1f605",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f605.png",
+        name: "",
+        title: "",
+        src: "https://www.emojiall.com/images/60/wechat/1f62a.png",
       },
       {
-        name: "1f606",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f606.png",
+        name: "大哭",
+        title: "大哭",
+        src: "https://www.emojiall.com/images/60/wechat/1f622.png",
       },
       {
-        name: "1f607",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f607.png",
+        name: "尴尬",
+        title: "尴尬",
+        src: "https://www.emojiall.com/images/60/wechat/wx046.png",
       },
       {
-        name: "1f609",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f609.png",
+        name: "发怒",
+        title: "发怒",
+        src: "https://www.emojiall.com/images/60/wechat/1f621.png",
       },
       {
-        name: "1f60a",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60a.png",
+        name: "调皮",
+        title: "调皮",
+        src: "https://www.emojiall.com/images/60/wechat/1f61b.png",
       },
       {
-        name: "1f642",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f642.png",
+        name: "呲牙",
+        title: "呲牙",
+        src: "https://www.emojiall.com/images/60/wechat/1f601.png",
       },
       {
-        name: "1f643",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f643.png",
+        name: "惊讶",
+        title: "惊讶",
+        src: "https://www.emojiall.com/images/60/wechat/1f632.png",
       },
       {
-        name: "1263a",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/263a.png",
+        name: "难过",
+        title: "难过",
+        src: "https://www.emojiall.com/images/60/wechat/1f641.png",
       },
       {
-        name: "1f60b",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60b.png",
+        name: "冷汗",
+        title: "冷汗",
+        src: "https://www.emojiall.com/images/60/wechat/1f630.png",
       },
       {
-        name: "1f60c",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60c.png",
+        name: "抓狂",
+        title: "抓狂",
+        src: "https://www.emojiall.com/images/60/wechat/1f62b.png",
       },
       {
-        name: "1f60d",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60d.png",
+        name: "",
+        title: "",
+        src: "https://www.emojiall.com/images/60/wechat/1f92e.png",
       },
       {
-        name: "1f970",
-        title: "笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f970.png",
+        name: "偷笑",
+        title: "笑",
+        src: "https://www.emojiall.com/images/60/wechat/1f92d.png",
       },
       {
-        name: "1f618",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f618.png",
+        name: "愉快",
+        title: "愉快",
+        src: "https://www.emojiall.com/images/60/wechat/1f60a-new.png",
       },
       {
-        name: "1f617",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f617.png",
+        name: "白眼",
+        title: "白眼",
+        src: "https://www.emojiall.com/images/60/wechat/1f644-new.png",
       },
       {
-        name: "1f619",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f619.png",
+        name: "傲慢",
+        title: "傲慢",
+        src: "https://www.emojiall.com/images/60/wechat/1f615.png",
       },
       {
-        name: "1f61a",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61a.png",
+        name: "",
+        title: "",
+        src: "https://www.emojiall.com/images/60/wechat/1f62a-new.png",
       },
       {
-        name: "1f61c",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61c.png",
+        name: "惊恐",
+        title: "惊恐",
+        src: "https://www.emojiall.com/images/60/wechat/1f628.png",
       },
       {
-        name: "1f92a",
-        title: "笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92a.png",
+        name: "憨笑",
+        title: "笑",
+        src: "https://www.emojiall.com/images/60/wechat/1f600.png",
       },
       {
-        name: "1f928",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f928.png",
+        name: "悠闲",
+        title: "悠闲",
+        src: "https://www.emojiall.com/images/60/wechat/1f6ac.png",
       },
       {
-        name: "1f9d0",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f9d0.png",
+        name: "咒骂",
+        title: "咒骂",
+        src: "https://www.emojiall.com/images/60/wechat/1f92c.png",
       },
       {
-        name: "1f61d",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61d.png",
+        name: "疑问",
+        title: "疑问",
+        src: "https://www.emojiall.com/images/60/wechat/wx068.png",
       },
       {
-        name: "1f61b",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61b.png",
+        name: "",
+        title: "",
+        src: "https://www.emojiall.com/images/60/wechat/1f92b.png",
       },
       {
-        name: "1f911",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f911.png",
+        name: "",
+        title: "",
+        src: "https://www.emojiall.com/images/60/wechat/1f635.png",
       },
       {
-        name: "1f913",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f913.png",
+        name: "",
+        title: "",
+        src: "https://www.emojiall.com/images/60/wechat/1f622-new.png",
       },
       {
-        name: "1f60e",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60e.png",
+        name: "骷髅",
+        title: "骷髅",
+        src: "https://www.emojiall.com/images/60/wechat/1f480.png",
       },
       {
-        name: "1f929",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f929.png",
+        name: "敲打",
+        title: "敲打",
+        src: "https://www.emojiall.com/images/60/wechat/1f915.png",
       },
       {
-        name: "1f921",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f921.png",
+        name: "再见",
+        title: "再见",
+        src: "https://www.emojiall.com/images/60/wechat/1f44b.png",
       },
       {
-        name: "1f920",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f920.png",
+        name: "擦汗",
+        title: "擦汗",
+        src: "https://www.emojiall.com/images/60/wechat/wx076.png",
       },
       {
-        name: "1f917",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f917.png",
+        name: "抠鼻",
+        title: "抠鼻",
+        src: "https://www.emojiall.com/images/60/wechat/wx077.png",
       },
       {
-        name: "1f60f",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60f.png",
+        name: "鼓掌",
+        title: "鼓掌",
+        src: "https://www.emojiall.com/images/60/wechat/1f44f.png",
       },
       {
-        name: "1f636",
-        title: "笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f636.png",
+        name: "坏笑",
+        title: "笑",
+        src: "https://www.emojiall.com/images/60/wechat/1f62c.png",
       },
       {
-        name: "1f610",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f610.png",
+        name: "右哼哼",
+        title: "右哼哼",
+        src: "https://www.emojiall.com/images/60/wechat/1f624-new.png",
       },
       {
-        name: "1f611",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f611.png",
+        name: "鄙视",
+        title: "鄙视",
+        src: "https://www.emojiall.com/images/60/wechat/wx084.png",
       },
       {
-        name: "1f612",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f612.png",
+        name: "委屈",
+        title: "委屈",
+        src: "https://www.emojiall.com/images/60/wechat/1f641-new.png",
       },
       {
-        name: "1f644",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f644.png",
+        name: "快哭了",
+        title: "快哭了",
+        src: "https://www.emojiall.com/images/60/wechat/1f625.png",
       },
       {
-        name: "1f914",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f914.png",
+        name: "阴险",
+        title: "阴险",
+        src: "https://www.emojiall.com/images/60/wechat/1f60f.png",
       },
       {
-        name: "1f925",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f925.png",
+        name: "亲亲",
+        title: "亲亲",
+        src: "https://www.emojiall.com/images/60/wechat/1f61a.png",
       },
       {
-        name: "1f92d",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92d.png",
+        name: "可怜",
+        title: "可怜",
+        src: "https://www.emojiall.com/images/60/wechat/1f97a.png",
       },
       {
-        name: "1f92b",
-        title: "笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92b.png",
+        name: "笑脸",
+        title: "笑",
+        src: "https://www.emojiall.com/images/60/wechat/1f604.png",
       },
       {
-        name: "1f92c",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92c.png",
+        name: "生病",
+        title: "生病",
+        src: "https://www.emojiall.com/images/60/wechat/1f637.png",
       },
       {
-        name: "1f92f",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92f.png",
+        name: "脸红",
+        title: "脸红",
+        src: "https://www.emojiall.com/images/60/wechat/1f633.png",
       },
       {
-        name: "1f633",
-        title: "笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f633.png",
+        name: "破涕为笑",
+        title: "破涕为笑",
+        src: "https://www.emojiall.com/images/60/wechat/1f602.png",
       },
       {
-        name: "1f61e",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61e.png",
+        name: "恐惧",
+        title: "恐惧",
+        src: "https://www.emojiall.com/images/60/wechat/1f631-new.png",
       },
       {
-        name: "1f61f",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61f.png",
+        name: "失望",
+        title: "失望",
+        src: "https://www.emojiall.com/images/60/wechat/1f614.png",
       },
       {
-        name: "1f620",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f620.png",
+        name: "无语",
+        title: "无语",
+        src: "https://www.emojiall.com/images/60/wechat/1f612.png",
       },
       {
-        name: "1f621",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f621.png",
+        name: "嘿哈",
+        title: "嘿哈",
+        src: "https://www.emojiall.com/images/60/wechat/wx004.png",
+      },
+      {
+        name: "捂脸",
+        title: "捂脸",
+        src: "https://www.emojiall.com/images/60/wechat/wx005.png",
+      },
+      {
+        name: "奸笑",
+        title: "奸笑",
+        src: "https://www.emojiall.com/images/60/wechat/wx003.png",
+      },
+      {
+        name: "机智",
+        title: "机智",
+        src: "https://www.emojiall.com/images/60/wechat/wx006.png",
+      },
+      {
+        name: "皱眉",
+        title: "皱眉",
+        src: "https://www.emojiall.com/images/60/wechat/1f97a-new.png",
+      },
+      {
+        name: "耶",
+        title: "耶",
+        src: "https://www.emojiall.com/images/60/wechat/270c-new.png",
+      },
+      {
+        name: "吃瓜",
+        title: "吃瓜",
+        src: "https://www.emojiall.com/images/60/wechat/wx033.png",
+      },
+      {
+        name: "加油",
+        title: "加油",
+        src: "https://www.emojiall.com/images/60/wechat/wx011.png",
+      },
+      {
+        name: "汗",
+        title: "汗",
+        src: "https://www.emojiall.com/images/60/wechat/1f613-new.png",
+      },
+      {
+        name: "天啊",
+        title: "天啊",
+        src: "https://www.emojiall.com/images/60/wechat/1f632-new.png",
+      },
+      {
+        name: "Emm",
+        title: "Emm",
+        src: "https://www.emojiall.com/images/60/wechat/1f611-new.png",
+      },
+      {
+        name: "社会社会",
+        title: "社会社会",
+        src: "https://www.emojiall.com/images/60/wechat/wx018.png",
+      },
+      {
+        name: "旺柴",
+        title: "旺柴",
+        src: "https://www.emojiall.com/images/60/wechat/1f436.png",
+      },
+      {
+        name: "好的",
+        title: "好的",
+        src: "https://www.emojiall.com/images/60/wechat/1f44c-new.png",
+      },
+      {
+        name: "打脸",
+        title: "打脸",
+        src: "https://www.emojiall.com/images/60/wechat/1f915-new.png",
+      },
+      {
+        name: "哇",
+        title: "哇",
+        src: "https://www.emojiall.com/images/60/wechat/1f929.png",
+      },
+      {
+        name: "翻白眼",
+        title: "翻白眼",
+        src: "https://www.emojiall.com/images/60/wechat/1f644.png",
+      },
+      {
+        name: "666",
+        title: "666",
+        src: "https://www.emojiall.com/images/60/wechat/wx145.png",
+      },
+      {
+        name: "让我看看",
+        title: "让我看看",
+        src: "https://www.emojiall.com/images/60/wechat/wx144.png",
+      },
+      {
+        name: "叹气",
+        title: "叹气",
+        src: "https://www.emojiall.com/images/60/wechat/wx143.png",
+      },
+      {
+        name: "苦涩",
+        title: "苦涩",
+        src: "https://www.emojiall.com/images/60/wechat/wx142.png",
+      },
+      {
+        name: "裂开",
+        title: "裂开",
+        src: "https://www.emojiall.com/images/60/wechat/wx141.png",
+      },
+      {
+        name: "嘴唇",
+        title: "嘴唇",
+        src: "https://www.emojiall.com/images/60/wechat/1f444.png",
+      },
+      {
+        name: "爱心",
+        title: "爱心",
+        src: "https://www.emojiall.com/images/60/wechat/2764.png",
+      },
+      {
+        name: "心碎",
+        title: "心碎",
+        src: "https://www.emojiall.com/images/60/wechat/1f494.png",
+      },
+      {
+        name: "拥抱",
+        title: "拥抱",
+        src: "https://www.emojiall.com/images/60/wechat/1f917.png",
+      },
+      {
+        name: "强",
+        title: "强",
+        src: "https://www.emojiall.com/images/60/wechat/1f44d.png",
+      },
+      {
+        name: "弱",
+        title: "弱",
+        src: "https://www.emojiall.com/images/60/wechat/1f44e.png",
+      },
+      {
+        name: "握手",
+        title: "握手",
+        src: "https://www.emojiall.com/images/60/wechat/1f91d.png",
+      },
+      {
+        name: "胜利",
+        title: "胜利",
+        src: "https://www.emojiall.com/images/60/wechat/270c.png",
+      },
+      {
+        name: "抱拳",
+        title: "抱拳",
+        src: "https://www.emojiall.com/images/60/wechat/1f64f.png",
+      },
+      {
+        name: "勾引",
+        title: "勾引",
+        src: "https://www.emojiall.com/images/60/wechat/wx120.png",
+      },
+      {
+        name: "拳头",
+        title: "拳头",
+        src: "https://www.emojiall.com/images/60/wechat/270a.png",
+      },
+      {
+        name: "OK",
+        title: "OK",
+        src: "https://www.emojiall.com/images/60/wechat/1f44c.png",
+      },
+      {
+        name: "合十",
+        title: "合十",
+        src: "https://www.emojiall.com/images/60/wechat/1f64f-new.png",
+      },
+      {
+        name: "啤酒",
+        title: "啤酒",
+        src: "https://www.emojiall.com/images/60/wechat/1f37a.png",
+      },
+      {
+        name: "咖啡",
+        title: "咖啡",
+        src: "https://www.emojiall.com/images/60/wechat/2615.png",
+      },
+      {
+        name: "蛋糕",
+        title: "蛋糕",
+        src: "https://www.emojiall.com/images/60/wechat/1f381.png",
+      },
+      {
+        name: "玫瑰",
+        title: "玫瑰",
+        src: "https://www.emojiall.com/images/60/wechat/1f339.png",
+      },
+      {
+        name: "凋谢",
+        title: "凋谢",
+        src: "https://www.emojiall.com/images/60/wechat/1f940.png",
+      },
+      {
+        name: "菜刀",
+        title: "菜刀",
+        src: "https://www.emojiall.com/images/60/wechat/1f52a.png",
+      },
+      {
+        name: "炸弹",
+        title: "炸弹",
+        src: "https://www.emojiall.com/images/60/wechat/1f4a3.png",
+      },
+      {
+        name: "便便",
+        title: "便便",
+        src: "https://www.emojiall.com/images/60/wechat/1f4a9.png",
+      },
+      {
+        name: "月亮",
+        title: "月亮",
+        src: "https://www.emojiall.com/images/60/wechat/1f319.png",
+      },
+      {
+        name: "太阳",
+        title: "太阳",
+        src: "https://www.emojiall.com/images/60/wechat/1f31e.png",
+      },
+      {
+        name: "庆祝",
+        title: "庆祝",
+        src: "https://www.emojiall.com/images/60/wechat/1f389.png",
+      },
+      {
+        name: "礼物",
+        title: "礼物",
+        src: "https://www.emojiall.com/images/60/wechat/1f381-new.png",
+      },
+      {
+        name: "红包",
+        title: "红包",
+        src: "https://www.emojiall.com/images/60/wechat/1f9e7.png",
+      },
+      {
+        name: "發",
+        title: "發",
+        src: "https://www.emojiall.com/images/60/wechat/1f005.png",
+      },
+      {
+        name: "福",
+        title: "福",
+        src: "https://www.emojiall.com/images/60/wechat/wx001.png",
+      },
+      {
+        name: "烟花",
+        title: "烟花",
+        src: "https://www.emojipic.cn/Pics/72/apple/fireworks_1f386.png",
+      },
+      {
+        name: "爆竹",
+        title: "爆竹",
+        src: "https://www.emojipic.cn/Pics/72/apple/firecracker_1f9e8.png",
+      },
+      {
+        name: "猪头",
+        title: "猪头",
+        src: "https://www.emojiall.com/images/60/wechat/1f437.png",
+      },
+      {
+        name: "跳跳",
+        title: "跳跳",
+        src: "https://www.emojiall.com/images/60/wechat/wx128.png",
+      },
+      {
+        name: "发抖",
+        title: "发抖",
+        src: "https://www.emojiall.com/images/60/wechat/1f976.png",
+      },
+      {
+        name: "转圈",
+        title: "转圈",
+        src: "https://www.emojiall.com/images/60/wechat/wx131.png",
       },
     ],
   },
@@ -278,14 +553,54 @@ export default [
     label: "收藏",
     children: [
       {
-        name: "1f62c",
-        title: "笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f62c.png",
+        name: "破涕为笑",
+        title: "破涕为笑",
+        src: "https://www.emojiall.com/images/60/wechat/1f602.png",
       },
       {
-        name: "1f621",
-        title: "微笑",
-        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f621.png",
+        name: "恐惧",
+        title: "恐惧",
+        src: "https://www.emojiall.com/images/60/wechat/1f631-new.png",
+      },
+      {
+        name: "失望",
+        title: "失望",
+        src: "https://www.emojiall.com/images/60/wechat/1f614.png",
+      },
+      {
+        name: "无语",
+        title: "无语",
+        src: "https://www.emojiall.com/images/60/wechat/1f612.png",
+      },
+      {
+        name: "嘿哈",
+        title: "嘿哈",
+        src: "https://www.emojiall.com/images/60/wechat/wx004.png",
+      },
+      {
+        name: "捂脸",
+        title: "捂脸",
+        src: "https://www.emojiall.com/images/60/wechat/wx005.png",
+      },
+      {
+        name: "奸笑",
+        title: "奸笑",
+        src: "https://www.emojiall.com/images/60/wechat/wx003.png",
+      },
+      {
+        name: "机智",
+        title: "机智",
+        src: "https://www.emojiall.com/images/60/wechat/wx006.png",
+      },
+      {
+        name: "皱眉",
+        title: "皱眉",
+        src: "https://www.emojiall.com/images/60/wechat/1f97a-new.png",
+      },
+      {
+        name: "耶",
+        title: "耶",
+        src: "https://www.emojiall.com/images/60/wechat/270c-new.png",
       },
     ],
   },

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
src/components/LemonUI/index.css


+ 5 - 1
src/components/LemonUI/index.js

@@ -14,6 +14,8 @@ import lemonMessageFile from "./components/message/file";
 import lemonMessageEvent from "./components/message/event";
 import LemonMessageVoice from "./components/message/voice";
 import LemonMessageVideo from "./components/message/video";
+import LemonMessageEmotionDynamic from "./components/message/emotionDynamic.vue";
+import LemonMessageMiniprogram from "./components/message/miniprogram.vue";
 
 
 import LemonIMUI from "./components/index";
@@ -39,7 +41,9 @@ const components = [
   lemonMessageEvent,
   LemonMessageVoice,
   LemonMessageVideo,
-  LemonRecords
+  LemonRecords,
+  LemonMessageEmotionDynamic,
+  LemonMessageMiniprogram
 ];
 const install = (Vue) => {
   Vue.directive("LemonContextmenu", Contextmenu);

+ 7 - 1
src/components/LemonUI/lastContentRender.js

@@ -13,9 +13,15 @@ export default {
     return "[视频]";
   },
   text(message) {
-    return this.emojiNameToImage(clearHtml(message.content));
+    return this.emojiNameToImage(clearHtml(message));
   },
   event(message){
     return '[通知]';
   },
+  emotionDynamic(message) {
+    return '[自定义表情]';
+  },
+  miniprogram(message) {
+    return '[小程序]';
+  }
 };

+ 2 - 2
src/components/LemonUI/styles/common/icons.styl

@@ -1,5 +1,5 @@
 // @font-face {
-//   font-family: 'lemon-icons';
+//   font-family: 'lemon-icons'; 
 //   src: url('//at.alicdn.com/t/font_1312162_neqltsj20an.eot');
 //   src: url('//at.alicdn.com/t/font_1312162_neqltsj20an.eot?#iefix') format('embedded-opentype'),
 //   url('//at.alicdn.com/t/font_1312162_neqltsj20an.woff2') format('woff2'),
@@ -8,7 +8,7 @@
 //   url('//at.alicdn.com/t/font_1312162_neqltsj20an.svg#iconfont') format('svg');
 // }
 @font-face {
-  font-family: 'lemon-icons';
+  font-family: 'lemon-icons'; 
   src:url('../fonts/icon.woff') format('woff');
 }
 [class^='lemon-icon-'],

+ 1 - 1
src/components/LemonUI/styles/common/index.styl

@@ -1,3 +1,3 @@
 //@import './normalize';
 @import './animate';
-@import './icons';
+@import './icons';

+ 7 - 7
src/components/LemonUI/styles/common/normalize.styl

@@ -1,23 +1,23 @@
-html
+html 
   -webkit-tap-highlight-color transparent
-body
+body 
   margin 0
-a
+a 
   text-decoration none
 a
 input
 button
-textarea
-  &:focus
+textarea 
+  &:focus 
     outline none
 ol
-ul
+ul 
   margin 0
   padding 0
   list-style none
 input
 button
-textarea
+textarea 
   font inherit
   color inherit
 

+ 1 - 1
src/components/LemonUI/styles/utils/bem.styl

@@ -64,4 +64,4 @@ e(element)
         {block}
     else
       &{element}
-        {block}
+        {block}

+ 9 - 9
src/components/LemonUI/styles/utils/functional.styl

@@ -1,27 +1,27 @@
 flex-column()
   display flex
   flex-direction column
-
+  
 scrollbar-theme($color=#1f252d, $background=#6d6d6d)
   &::-webkit-scrollbar
     width 5px
     height 5px
-
+  
   &::-webkit-scrollbar-track-piece
     background-color $background
-
+  
   &::-webkit-scrollbar-thumb:vertical
     height 5px
     background-color $color
-
-  &::-webkit-scrollbar-thumb:horizontal
+  
+  &::-webkit-scrollbar-thumb:horizontal 
     width 5px
     background-color $background
 
-scrollbar-dark()
+scrollbar-dark() 
   scrollbar-theme()
 
-scrollbar-light()
+scrollbar-light() 
   scrollbar-theme(#aaa, transparent)
 
 
@@ -32,7 +32,7 @@ vertical-center()
     height 100%
     vertical-align middle
 
-position-center($type fixed)
+position-center($type fixed) 
   position $type
   top 50%
   left 50%
@@ -44,4 +44,4 @@ ellipsis()
 word-break()
   word-break break-all
   word-wrap break-word
-  white-space pre-wrap
+  white-space pre-wrap

+ 1 - 1
src/components/LemonUI/utils/constant.js

@@ -6,7 +6,7 @@ export const DEFAULT_MENUS = [DEFAULT_MENU_LASTMESSAGES, DEFAULT_MENU_CONTACTS];
 /**
  * 聊天消息类型
  */
-export const MESSAGE_TYPE = ["voice", "file", "video", "image", "text"];
+export const MESSAGE_TYPE = ["voice", "file", "video", "image", "text", "emotionDynamic", "miniprogram"];
 
 /**
  * 聊天消息状态

+ 80 - 2
src/layout/index.vue

@@ -11,7 +11,26 @@
       <right-panel>
         <settings />
       </right-panel>
+
+      <div class="qw-im">
+        <el-badge :value="totalUnreadCount" :max="99" :hidden="totalUnreadCount < 1">
+          <img alt="" class="qw-im-img" @click="() => {this.qw.open = !this.qw.open}" src="../assets/image/qw-im.png"/>
+        </el-badge>
+      </div>
     </div>
+
+    <el-dialog append-to-body
+               :width="qw.isMaximized ? '100vw' : '80vw'"
+               custom-class="im-dialog"
+               :fullscreen="qw.isMaximized"
+               :visible.sync="qw.open"
+               :style="{visibility: !qw.initLoad ? 'hidden' : 'unset'}"
+               :modal="qw.initLoad"
+               :title="qw.title">
+      <div class="qw-im-content">
+        <QwIM :showQw="qw.open" :isMaximized="qw.isMaximized" @close="() => qw.open = false"/>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -21,6 +40,7 @@ import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
 import ResizeMixin from './mixin/ResizeHandler'
 import { mapState } from 'vuex'
 import variables from '@/assets/styles/variables.scss'
+import QwIM from '@/views/qw/qwChat/qq.vue'
 
 export default {
   name: 'Layout',
@@ -30,7 +50,8 @@ export default {
     RightPanel,
     Settings,
     Sidebar,
-    TagsView
+    TagsView,
+    QwIM
   },
   mixins: [ResizeMixin],
   computed: {
@@ -40,7 +61,9 @@ export default {
       sidebar: state => state.app.sidebar,
       device: state => state.app.device,
       needTagsView: state => state.settings.tagsView,
-      fixedHeader: state => state.settings.fixedHeader
+      fixedHeader: state => state.settings.fixedHeader,
+      companyUser:state => state.user.user,
+      totalUnreadCount: state => state.qwIm.totalUnreadCount
     }),
     classObj() {
       return {
@@ -54,6 +77,27 @@ export default {
       return variables;
     }
   },
+  data() {
+    return {
+      qw: {
+        open: false,
+        title: '',
+        isMaximized: false,
+        initLoad: false
+      }
+    }
+  },
+  mounted() {
+    if (!this.qw.initLoad) {
+      this.qw.open = true
+      this.$nextTick(() => {
+        this.qw.open = false
+        setTimeout(() => {
+          this.qw.initLoad = true
+        },  500)
+      })
+    }
+  },
   methods: {
     handleClickOutside() {
       this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
@@ -104,4 +148,38 @@ export default {
   .mobile .fixed-header {
     width: 100%;
   }
+
+  ::v-deep .qw-im {
+    z-index: 2000;
+    position: fixed;
+    right: 30px;
+    bottom: 30px;
+    width: 54px;
+    height: 54px;
+    .qw-im-img {
+      width: 100%;
+    }
+    .el-badge__content {
+      border: unset;
+    }
+  }
+
+  ::v-deep .im-dialog {
+    max-width: 100vw;
+    max-height: 100vh;
+    .el-dialog__header {
+      display: none;
+    }
+    .el-dialog__body {
+      padding: 0 !important;
+    }
+  }
+
+  .qw-im-content {
+    width: 100%;
+    height: 100%;
+    /* 可选:让内容区自动撑满弹窗 */
+    display: flex;
+    flex-direction: column;
+  }
 </style>

+ 5 - 1
src/main.js

@@ -16,7 +16,7 @@ import './assets/icons' // icon
 import './permission' // permission control
 import { getDicts } from "@/api/system/dict/data";
 import { getConfigKey } from "@/api/system/config";
-import {cloneObject, parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, download, handleTree } from "@/utils/common";
+import {cloneObject, parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, download, handleTree, friendlyDate } from "@/utils/common";
 import Pagination from "@/components/Pagination";
 // 自定义表格工具组件
 import RightToolbar from "@/components/RightToolbar"
@@ -37,6 +37,9 @@ import * as echarts from "echarts";
 import { VueJsonp } from 'vue-jsonp'
 Vue.use(VueJsonp)
 
+import LemonIMUI from '@/components/LemonUI';
+Vue.use(LemonIMUI);
+
 // 全局配置
 Vue.prototype.logImg = require(process.env.VUE_APP_LOG_URL)
 Vue.prototype.cloneObject = cloneObject
@@ -50,6 +53,7 @@ Vue.prototype.selectDictLabels = selectDictLabels
 Vue.prototype.download = download
 Vue.prototype.handleTree = handleTree
 Vue.prototype.echarts = echarts
+Vue.prototype.friendlyDate=friendlyDate
 
 Vue.prototype.msgSuccess = function (msg) {
   this.$message({ showClose: true, message: msg, type: "success" });

+ 3 - 1
src/store/index.js

@@ -6,6 +6,7 @@ import tagsView from './modules/tagsView'
 import permission from './modules/permission'
 import settings from './modules/settings'
 import getters from './getters'
+import qwIm from './modules/qwIm'
 
 Vue.use(Vuex)
 
@@ -15,7 +16,8 @@ const store = new Vuex.Store({
     user,
     tagsView,
     permission,
-    settings
+    settings,
+    qwIm
   },
   getters
 })

+ 29 - 0
src/store/modules/qwIm.js

@@ -0,0 +1,29 @@
+const state = {
+  shareCourse: null,
+  totalUnreadCount: 0
+}
+
+const mutations = {
+  SHARE_COURSE: (state, data) => {
+    state.shareCourse = data
+  },
+  TOTAL_UNREAD_COUNT: (state, data) => {
+    state.totalUnreadCount = data
+  }
+}
+
+const actions = {
+  shareCourse({ commit }, data) {
+    commit('SHARE_COURSE', data)
+  },
+  totalUnreadCount({ commit }, data) {
+    commit('TOTAL_UNREAD_COUNT', data)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 67 - 0
src/utils/ImSocket.js

@@ -0,0 +1,67 @@
+export class ImSocket {
+  /**
+   * @param {string} url - WebSocket 服务器地址
+   * @param {number} checkInterval - 检查连接状态的时间间隔,单位毫秒
+   */
+  constructor(url, checkInterval = 5000) {
+    this.url = url;
+    this.checkInterval = checkInterval;
+    this.ws = null;
+    this.onMessageCallback = null;
+    this.isConnecting = false;
+    this.connect();
+    this.startHeartbeat();
+  }
+
+  connect() {
+    if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
+      return;
+    }
+
+    if (this.isConnecting) {
+      return;
+    }
+
+    this.isConnecting = true;
+
+    this.ws = new WebSocket(this.url);
+
+    this.ws.onmessage = (event) => {
+      // 根据需要处理消息
+      if (this.onMessageCallback) this.onMessageCallback(event.data);
+    };
+
+    this.ws.onclose = (event) => {
+      this.isConnecting = false;
+    };
+  }
+
+  // 定时检查连接状态
+  startHeartbeat() {
+    if (this.heartbeatTimer) return;
+
+    this.heartbeatTimer = setInterval(() => {
+      if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
+        this.connect();
+      }
+    }, this.checkInterval);
+  }
+
+   // 清除重连定时器的方法
+  stopHeartbeat() {
+    if (this.heartbeatTimer) {
+      clearInterval(this.heartbeatTimer);
+      this.heartbeatTimer = null;
+    }
+  }
+
+  // 主动关闭 WebSocket 连接,并清除定时任务
+  close() {
+    this.stopHeartbeat();
+    this.ws?.close();
+    this.ws = null;
+  }
+  onMessage(callback) {
+    this.onMessageCallback = callback;
+  }
+}

+ 43 - 1
src/utils/common.js

@@ -290,7 +290,7 @@ export function formatTime(timer) {
 
 /**
  * 获取日期范围
- * @param {number} days - 从今天往前推的天数  
+ * @param {number} days - 从今天往前推的天数
  * @returns {Array} 返回格式化的日期数组 [开始日期, 结束日期]
  * @example
  * // 获取最近7天的日期范围
@@ -316,6 +316,48 @@ export function getDateRange(days) {
 	];
 }
 
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function friendlyDate(time, option) {
+  if (('' + time).length === 10) {
+    time = parseInt(time) * 1000
+  } else {
+    time = +time
+  }
+  const d = new Date(time)
+  const now = Date.now()
+  const diff = (now - d) / 1000
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) {
+    // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return (
+      d.getMonth() +
+      1 +
+      '-' +
+      d.getDate() +
+      ' ' +
+      d.getHours() +
+      ':' +
+      d.getMinutes() +
+      ''
+    )
+  }
+}
+
 
 // export function callNumber(mobile){
 // 	var that=this;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 575 - 470
src/views/qw/qwChat/qq.vue


+ 423 - 0
src/views/qw/qwChat/userDetail/courseManage.vue

@@ -0,0 +1,423 @@
+<template>
+  <div>
+    <!-- 顶部标题栏 -->
+    <div>
+      <div style="position: relative; display: flex; align-items: center; justify-content: center; height: 48px; background: #fff;">
+        <span style="position: absolute; left: 16px; cursor: pointer;" @click="handleBack">
+          <i class="el-icon-arrow-left" style="font-size: 20px; color: #333;"></i>
+        </span>
+        <span style="font-weight: bold; font-size: 16px;">课程管理</span>
+      </div>
+
+      <el-select
+        v-model="courseId"
+        placeholder="请点击选择课程"
+        class="courseSelect"
+        popper-class="courseSelectList"
+        @visible-change="handleSelectOpen"
+        @change="courseChange"
+        ref="courseSelect"
+      >
+        <el-option
+          v-for="item in courseList"
+          :key="item.courseId"
+          :label="item.courseName"
+          :value="item.courseId">
+        </el-option>
+        <div
+          v-if="courseQueryParams.hasNextPage"
+          class="load-more"
+          v-loading="courseQueryParams.loading"
+        >
+          <span v-if="!courseQueryParams.loading">加载更多</span>
+        </div>
+      </el-select>
+
+      <el-input
+        placeholder="请输入课程名称"
+        v-model="videoQueryParams.keyword"
+        class="videoSearch"
+        @keyup.enter.native="handleSearch"
+      >
+      </el-input>
+    </div>
+
+    <!-- 列表 -->
+    <div :class="extend ? 'infinite-list-wrapper-extend' : 'infinite-list-wrapper'" style="overflow:auto">
+      <ul class="video-list" v-infinite-scroll="loadMoreVideo">
+        <li v-for="item in videoList" :key="item.videoId" class="video-item">
+          <div class="video-thumbnail">
+            <img :src="item.thumbnail || defaultImage" alt="" @error="handleImgError">
+            <div class="video-duration">{{formatDuration(item.duration)}}</div>
+          </div>
+          <div class="video-info">
+            <div class="video-title">{{item.title}}</div>
+            <div class="video-meta">
+              <i class="el-icon-s-order" style="font-size: 18px;margin-right: 5px"></i>
+              <span class="video-date">{{formatDate(item.createTime)}}</span>
+            </div>
+          </div>
+          <div class="video-actions">
+            <el-button
+              type="primary"
+              size="mini"
+              round
+              :loading="sharingVideoId === item.videoId"
+              :disabled="sharingVideoId === item.videoId"
+              @click="handleShare(item)">分享课程</el-button>
+          </div>
+        </li>
+      </ul>
+      <div v-if="videoList.length === 0 && !videoQueryParams.loading" class="empty-tip">
+        <i class="el-icon-video-camera"></i>
+        <p>暂无视频</p>
+      </div>
+      <div v-if="videoQueryParams.loading" class="loading-more">
+        <i class="el-icon-loading"></i>
+        <span>加载中...</span>
+      </div>
+      <div v-if="!videoQueryParams.hasNextPage && videoList.length > 0" class="no-more">
+        <span>没有更多了</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {createMiniLink, getFsCourseListBySidebar, getFsCourseVideoListBySidebar} from "@/api/qw/im";
+
+export default {
+  name: "courseManage",
+  props: {
+    userId: {
+      type: Number,
+      default: () => null
+    },
+    extend: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      courseId: null,
+      courseQueryParams: {
+        extId: this.userId,
+        page: 1,
+        limit: 10,
+        hasNextPage: false,
+        loading: false,
+      },
+      courseList: [],
+      scrollEvent: null,
+      videoQueryParams: {
+        courseId: null,
+        keyword: '',
+        page: 1,
+        limit: 10,
+        hasNextPage: false,
+        loading: false,
+      },
+      videoList: [],
+      defaultImage: require('@/assets/image/default-image.png'),
+      sharingVideoId: null
+    }
+  },
+  created() {
+    this.getFsCourseListBySidebar()
+  },
+  methods: {
+    handleBack() {
+      this.$emit('back')
+    },
+    getFsCourseListBySidebar() {
+      this.courseQueryParams.loading = true;
+      getFsCourseListBySidebar(this.courseQueryParams).then(response => {
+        if (this.courseQueryParams.page === 1) {
+          this.courseList = response.data.list;
+        } else {
+          this.courseList = [...this.courseList, ...response.data.list];
+        }
+        this.courseQueryParams.hasNextPage = response.data.hasNextPage;
+        this.courseQueryParams.loading = false;
+      }).catch(() => {
+        this.courseQueryParams.loading = false;
+      });
+    },
+    loadMore() {
+      if (!this.courseQueryParams.hasNextPage || this.courseQueryParams.loading) return;
+
+      this.courseQueryParams.page += 1;
+      this.getFsCourseListBySidebar();
+    },
+    handleSelectOpen(visible) {
+      if (visible) {
+        // 下拉框打开时,添加滚动监听
+        this.$nextTick(() => {
+          const selectDropdown = document.querySelector('.el-select-dropdown.courseSelectList');
+          if (selectDropdown) {
+            const dropdownList = selectDropdown.querySelector('.el-select-dropdown__wrap');
+            if (dropdownList) {
+              // 移除之前可能存在的事件监听
+              if (this.scrollEvent) {
+                dropdownList.removeEventListener('scroll', this.scrollEvent);
+              }
+
+              // 添加新的滚动监听
+              this.scrollEvent = () => {
+                const { scrollTop, scrollHeight, clientHeight } = dropdownList;
+                // 当滚动到距离底部20px时,加载更多
+                if (scrollHeight - scrollTop - clientHeight < 20) {
+                  this.loadMore();
+                }
+              };
+
+              dropdownList.addEventListener('scroll', this.scrollEvent);
+            }
+          }
+        });
+      } else {
+        // 下拉框关闭时,移除滚动监听
+        if (this.scrollEvent) {
+          const selectDropdown = document.querySelector('.el-select-dropdown.courseSelectList');
+          if (selectDropdown) {
+            const dropdownList = selectDropdown.querySelector('.el-select-dropdown__wrap');
+            if (dropdownList) {
+              dropdownList.removeEventListener('scroll', this.scrollEvent);
+            }
+          }
+        }
+      }
+    },
+    courseChange(courseId) {
+      this.videoQueryParams.courseId = courseId;
+      this.handleSearch()
+    },
+    handleSearch() {
+      if (!this.videoQueryParams.courseId) {
+        return
+      }
+      this.videoQueryParams.page = 1;
+      this.getFsCourseVideoListBySidebar()
+    },
+    getFsCourseVideoListBySidebar() {
+      this.videoQueryParams.loading = true;
+      getFsCourseVideoListBySidebar(this.videoQueryParams).then(response => {
+        if (this.videoQueryParams.page === 1) {
+          this.videoList = response.data.list;
+        } else {
+          this.videoList = [...this.videoList, ...response.data.list];
+        }
+        this.videoQueryParams.hasNextPage = response.data.hasNextPage;
+        this.videoQueryParams.loading = false;
+        console.log(this.videoList)
+      }).catch(() => {
+        this.videoQueryParams.loading = false;
+      })
+    },
+    loadMoreVideo() {
+      if (!this.videoQueryParams.hasNextPage || this.videoQueryParams.loading) return;
+      this.videoQueryParams.page += 1;
+      this.getFsCourseVideoListBySidebar()
+    },
+    handleImgError(e) {
+      e.target.src = this.defaultImage;
+    },
+    formatDate(timestamp) {
+      if (!timestamp) return '';
+      const date = new Date(timestamp);
+      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+    },
+    formatDuration(seconds) {
+      if (!seconds) return '00:00';
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = seconds % 60;
+      return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
+    },
+    handleShare(item) {
+      const params = {
+        extId: this.userId,
+        courseId: item.courseId,
+        videoId: item.videoId,
+        title: item.title,
+      }
+      this.sharingVideoId = item.videoId;
+      createMiniLink(params).then(response => {
+        const {code, data} = response;
+        if (code === 200) {
+          const content = {
+            url: data,
+            title: item.title,
+            thumbnail: item.thumbnail,
+          }
+          this.$store.dispatch('qwIm/shareCourse', content)
+        }
+        this.sharingVideoId = null;
+      }).catch(() => {
+        this.$message.error('分享课程失败');
+        this.sharingVideoId = null;
+      })
+    }
+  }
+}
+
+</script>
+
+<style scoped>
+.courseSelect {
+  width: 100%;
+}
+.courseSelectList li {
+  text-align: center;
+}
+::v-deep .courseSelect input {
+  border: unset;
+  text-align: center;
+}
+.load-more {
+  text-align: center;
+  height: 34px;
+  line-height: 34px;
+  color: #909399;
+  font-size: 12px;
+  cursor: pointer;
+  background-color: #f5f7fa;
+}
+.load-more:hover {
+  background-color: #e4e7ed;
+}
+.videoSearch {
+  padding: 10px;
+  border-radius: 8px;
+}
+::v-deep .videoSearch input {
+  border-radius: 22px;
+}
+
+.infinite-list-wrapper {
+  height: calc(100vh - 200px);
+  overflow-y: auto;
+  padding: 0 10px;
+  background-color: #f5f7fa;
+}
+.infinite-list-wrapper-extend {
+  height: calc(100vh - 330px);
+  overflow-y: auto;
+  padding: 0 10px;
+  background-color: #f5f7fa;
+}
+
+
+.video-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.video-item {
+  display: flex;
+  padding: 15px;
+  margin-bottom: 5px;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+  transition: transform 0.2s;
+}
+
+.video-item:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.video-thumbnail {
+  position: relative;
+  flex: 0 0 120px;
+  height: 80px;
+  margin-right: 15px;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.video-thumbnail img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.video-duration {
+  position: absolute;
+  bottom: 5px;
+  right: 5px;
+  background-color: rgba(0, 0, 0, 0.7);
+  color: #fff;
+  padding: 2px 4px;
+  border-radius: 2px;
+  font-size: 12px;
+}
+
+.video-info {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+}
+
+.video-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 5px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.video-meta {
+  display: flex;
+  align-items: flex-end;
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 5px;
+}
+
+.video-date {
+  margin-right: 15px;
+}
+
+.video-actions {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  gap: 8px;
+  margin-left: 10px;
+}
+
+.loading-more {
+  text-align: center;
+  padding: 15px 0;
+  color: #909399;
+}
+
+.no-more {
+  text-align: center;
+  padding: 15px 0;
+  color: #909399;
+  font-size: 13px;
+}
+
+.empty-tip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+  color: #909399;
+}
+
+.empty-tip i {
+  font-size: 48px;
+  margin-bottom: 10px;
+  color: #dcdfe6;
+}
+</style>

+ 335 - 0
src/views/qw/qwChat/userDetail/index.vue

@@ -0,0 +1,335 @@
+<template>
+  <div :style="{backgroundColor: '#f0f2f5', paddingBottom: !extend && '20px', minHeight: '100%'}">
+    <!-- 客户详情 -->
+    <div v-if="showDetail">
+      <div v-if="!extend" style="padding: 20px; background-color: #fff;">
+        客户详情
+      </div>
+      <!--  基本信息 -->
+      <el-descriptions :class="extend ? 'detail-description-extend' : 'detail-description'" title="基本信息" :column="extend ? 2 : 3" size="medium" border>
+        <template slot="extra">
+          <el-button size="mini" round @click="updateQwDetail">修改用户信息</el-button>
+        </template>
+        <el-descriptions-item label="姓名">{{qwUserInfo.name || '未知'}}</el-descriptions-item>
+        <el-descriptions-item label="性别">{{qwUserDetail.sex || '未知'}}</el-descriptions-item>
+        <el-descriptions-item label="年龄">{{qwUserDetail.age || '未知'}}</el-descriptions-item>
+        <el-descriptions-item label="行为习惯">{{qwUserDetail.habits || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="患病时间">{{qwUserDetail.illnessTime || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="疾病">{{qwUserDetail.disease || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="家人的疾病">{{qwUserDetail.familyDisease || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="是否线下就诊">{{qwUserDetail.isLine || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="体质">{{qwUserDetail.constitution || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="使用药品">{{qwUserDetail.medicine || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="咨询产品">{{qwUserDetail.consultProduct || '无'}}</el-descriptions-item>
+        <el-descriptions-item label="是否已购产品">{{qwUserDetail.isBuy || '无'}}</el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 看课记录 -->
+      <div :style="{backgroundColor: '#FFF',margin: !extend ? '10px' : '5px 0'}">
+        <div style="padding: 10px;font-weight: bold">
+          看课记录
+        </div>
+        <!-- 状态 -->
+        <div class="status-container">
+          <div
+            v-for="(item, index) in logTypeList"
+            :key="index"
+            class="status-item"
+          >
+            <div
+              class="status-color-block"
+              :style="{ backgroundColor: item.color }"
+            ></div>
+            <span class="status-text">{{ item.text }}</span>
+          </div>
+        </div>
+        <!-- 近七天看课记录 -->
+        <div>
+          <div style="display: flex;justify-content: space-between;align-items: center;flex-direction: row;background: #FFF;padding: 10px 10px 20px">
+            <div class="section-title">
+              近七天看课记录
+            </div>
+            <el-button size="mini" round @click="courseManage">课程管理</el-button>
+          </div>
+          <div class="course-record-container">
+            <div
+              v-for="item in courseWatch7day"
+              :key="item.id"
+              class="course-record-item"
+            >
+              <span class="course-date">{{ item.date.split('-')[2] }}日</span>
+              <div
+                class="course-status-block"
+                :style="{ backgroundColor: getLogTypeColor(item.logType) }"
+                :title="getLogTypeText(item.logType)"
+              ></div>
+            </div>
+            <div v-if="courseWatch7day.length === 0" class="empty-record">
+              暂无看课记录
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 购买记录 -->
+      <div :style="{backgroundColor: '#FFF',margin: !extend ? '10px' : '5px 0'}" v-if="qwUserInfo.fsUserId">
+        <div style="padding: 10px;font-weight: bold">
+          用户订单
+        </div>
+        <userStorerDetails  ref="userDetails" />
+      </div>
+
+      <!-- 已看过课程 -->
+      <div :style="{backgroundColor: '#FFF',margin: extend ? '5px 0' : '10px'}" v-if="qwUserInfo.fsUserId">
+        <div style="padding: 10px;font-weight: bold">
+          用户看课记录
+        </div>
+        <userCourseWatchLog  ref="userWatchLog" />
+      </div>
+
+    </div>
+
+    <!-- 修改用户信息 -->
+    <user-info-edit
+      v-if="showUpdate"
+      :userDetail="qwUserDetail"
+      @back="handleBack"
+      @save-success="handleSaveSuccess"
+    />
+
+    <!-- 课程管理 -->
+    <course-manage
+      v-if="showCourseManage"
+      :extend="extend"
+      :userId="qwUserInfo.id"
+      @back="handleBack"
+    />
+  </div>
+</template>
+
+<script>
+import userStorerDetails from "@/views/qw/qwChat/userDetail/userStorerDetails.vue"
+import userCourseWatchLog from "@/views/qw/qwChat/userDetail/userCourseWatchLog.vue";
+import userInfoEdit from "@/views/qw/qwChat/userDetail/userInfoEdit.vue";
+import courseManage from "@/views/qw/qwChat/userDetail/courseManage.vue";
+import {getQwExternalContactDetails, getQwUserInfo, queryCourseWatchStatistics} from "@/api/qw/im";
+
+export default {
+  name: "userDetail",
+  props: {
+    extId: {
+      type: String,
+      default: null
+    },
+    extend: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {
+    userStorerDetails,
+    userCourseWatchLog,
+    userInfoEdit,
+    courseManage,
+  },
+  data() {
+    // 定义颜色常量
+    const logTypeColors = {
+      notWatched: '#909399', // 未看课
+      watching: '#0bc6ff',   // 看课中
+      completed: '#67c23a',  // 完课
+      pending: '#f55a4f',    // 待看课
+      interrupted: '#ffd700' // 看课中断
+    };
+
+    return {
+      localExtId: this.extId,
+      qwUserInfo: {},
+      qwUserDetail: {},
+      showDetail: true,
+      showUpdate: false,
+      courseWatch7day: [],
+      logTypeColors,
+      // 定义状态列表用于渲染
+      logTypeList: [
+        { type: 0, text: '未看课', color: logTypeColors.notWatched },
+        { type: 1, text: '看课中', color: logTypeColors.watching },
+        { type: 2, text: '完课', color: logTypeColors.completed },
+        { type: 3, text: '待看课', color: logTypeColors.pending },
+        { type: 4, text: '看课中断', color: logTypeColors.interrupted }
+      ],
+      showCourseManage: false,
+    }
+  },
+  created() {
+    if (this.extId) {
+      this.getDetail(this.extId)
+    }
+  },
+  watch: {
+    extId(newVal) {
+      if (newVal) {
+        this.getDetail(newVal);
+      }
+    }
+  },
+  methods: {
+    getDetail(extId) {
+      this.localExtId = extId
+      this.getQwExternalContactDetails()
+      this.getQwUserInfo()
+      this.queryCourseWatchStatistics()
+    },
+    queryCourseWatchStatistics() {
+      const data = {
+        type: 0,
+        qwExternalContactId: this.localExtId
+      }
+      queryCourseWatchStatistics(data).then(response => {
+        this.courseWatch7day = response.data.data
+      })
+    },
+    getQwExternalContactDetails() {
+      const query = {
+        qwExternalContactId: this.localExtId
+      }
+      getQwExternalContactDetails(query).then(response => {
+        this.qwUserInfo = response.data
+        if (this.qwUserInfo.fsUserId) {
+          this.$nextTick(() => {
+            this.$refs.userDetails.getUserDetails(this.qwUserInfo.fsUserId)
+            this.$refs.userWatchLog.getUserWatchLog(this.qwUserInfo.fsUserId)
+          })
+        }
+      })
+    },
+    getQwUserInfo() {
+      const query = {
+        qwExternalContactId: this.localExtId
+      }
+      getQwUserInfo(query).then(response => {
+        this.qwUserDetail = response.moreInfo
+      })
+    },
+    updateQwDetail() {
+      this.showDetail = false
+      this.showUpdate = true
+    },
+    handleBack() {
+      this.getDetail(this.localExtId)
+      this.showDetail = true
+      this.showUpdate = false
+      this.showCourseManage = false
+    },
+    handleSaveSuccess() {
+      this.handleBack()
+    },
+    getLogTypeColor(logType) {
+      const type = parseInt(logType);
+      // 使用对象映射替代switch语句
+      const colorMap = {
+        0: this.logTypeColors.notWatched,
+        1: this.logTypeColors.watching,
+        2: this.logTypeColors.completed,
+        3: this.logTypeColors.pending,
+        4: this.logTypeColors.interrupted
+      };
+      return colorMap[type] || '';
+    },
+    getLogTypeText(logType) {
+      const type = parseInt(logType);
+      // 使用对象映射替代switch语句
+      const textMap = {
+        0: '未看课',
+        1: '看课中',
+        2: '完课',
+        3: '待看课',
+        4: '看课中断'
+      };
+      return textMap[type] || '';
+    },
+    courseManage() {
+      this.showDetail = false
+      this.showCourseManage = true
+    },
+  }
+}
+</script>
+<style scoped>
+.status-container {
+  display: flex;
+  gap: 20px;
+  padding: 10px 0 0 20px;
+  color: #8c939d;
+}
+
+.status-item {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+
+.status-color-block {
+  border-radius: 4px;
+  width: 20px;
+  height: 20px;
+}
+
+.status-text {
+  font-size: 12px;
+}
+
+.section-title {
+  padding: 10px;
+  font-size: 12px;
+  color: #8c939d;
+}
+
+.course-record-container {
+  display: flex;
+  padding: 0 10px 15px 10px;
+  gap: 10px;
+  overflow-x: auto;
+}
+
+.course-record-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  min-width: 40px;
+}
+
+.course-date {
+  font-size: 12px;
+  color: #606266;
+  margin-bottom: 5px;
+}
+
+.course-status-block {
+  width: 20px;
+  height: 20px;
+  border-radius: 4px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.empty-record {
+  width: 100%;
+  text-align: center;
+  color: #909399;
+  font-size: 14px;
+}
+.detail-description {
+  margin: 10px;
+  padding: 10px;
+  background-color: #FFF;
+}
+.detail-description-extend {
+  padding: 10px;
+  background-color: #FFF;
+}
+::v-deep .detail-description-extend .el-descriptions__title {
+  font-size: 14px;
+}
+</style>

+ 111 - 0
src/views/qw/qwChat/userDetail/userCourseWatchLog.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="apcontainer">
+    <el-table border v-loading="loading" :data="courseWatchLogList">
+      <el-table-column label="记录编号" align="center" prop="logId" />
+      <el-table-column label="用户账号" align="center" prop="userName" />
+      <el-table-column label="会员昵称" align="center" prop="fsNickName">
+        <template slot-scope="scope">
+          <div style="display: flex;white-space: nowrap">
+            <div style="margin: auto">
+              {{scope.row.fsNickName}}
+            </div>
+            <el-popover
+              placement="right"
+              title=""
+              trigger="hover">
+              <img slot="reference" :src="scope.row.fsAvatar" style="width: 30px;height: 30px">
+              <img :src="scope.row.fsAvatar" style="max-width: 200px;max-height: 200px">
+            </el-popover>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="项目" align="center" prop="projectName" />
+      <el-table-column label="课程名称" align="center" prop="courseName" />
+      <el-table-column label="小节名称" align="center" prop="videoName" />
+      <el-table-column label="记录类型" align="center" prop="logType">
+        <template slot-scope="scope">
+          <dict-tag :options="logTypeOptions" :value="scope.row.logType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="播放时长" align="center" prop="duration" />
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="更新时间" align="center" prop="updateTime" />
+      <el-table-column label="完课时间" align="center" prop="finishTime" />
+      <el-table-column label="营期时间" align="center" prop="campPeriodTime" />
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+  </div>
+</template>
+
+<script>
+import { listCourseWatchLog } from "@/api/course/courseWatchLog";
+import {mapState} from "vuex";
+export default {
+  name: "CourseWatchLog",
+  data() {
+    return {
+      activeName:"00",
+      logTypeOptions:[],
+      // 遮罩层
+      loading: true,
+      // 总条数
+      total: 0,
+      // 短链课程看课记录表格数据
+      courseWatchLogList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyUserId: null,
+        userId: null,
+        logType: null,
+      },
+    };
+  },
+  computed: {
+    ...mapState({
+      companyUserId: state => state.user.user.userId,
+    })
+  },
+  created() {
+    this.getDicts("sys_course_watch_log_type").then(response => {
+      this.logTypeOptions = response.data;
+    });
+    this.queryParams.companyUserId = this.companyUserId
+  },
+  methods: {
+    getUserWatchLog(id) {
+      this.queryParams.userId = id
+      this.getList()
+    },
+    /** 查询短链课程看课记录列表 */
+    getList() {
+      this.loading = true;
+      if(this.queryParams.logType == "10"){
+        this.queryParams.logType = null;
+      }
+
+      listCourseWatchLog(this.queryParams).then(response => {
+        this.courseWatchLogList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+  }
+};
+</script>
+
+<style scoped>
+.apcontainer {
+  padding: 10px;
+  font-size: 12px;
+}
+</style>

+ 245 - 0
src/views/qw/qwChat/userDetail/userInfoEdit.vue

@@ -0,0 +1,245 @@
+<template>
+  <div>
+    <!-- 顶部标题栏 -->
+    <div>
+      <div style="position: relative; display: flex; align-items: center; justify-content: center; height: 48px; background: #fff; border-bottom: 1px solid #eee;">
+        <span style="position: absolute; left: 16px; cursor: pointer;" @click="handleBack">
+          <i class="el-icon-arrow-left" style="font-size: 20px; color: #333;"></i>
+        </span>
+        <span style="font-weight: bold; font-size: 16px;">修改用户信息</span>
+      </div>
+    </div>
+    <!-- 表单内容 -->
+    <div class="user-form">
+      <div class="form-row">
+        <div class="form-label">性别</div>
+        <div class="form-input">
+          <input
+            v-model="formData.sex"
+            placeholder="请输入性别"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">年龄</div>
+        <div class="form-input">
+          <input
+            v-model="formData.age"
+            placeholder="请输入年龄"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">行为习惯</div>
+        <div class="form-input">
+          <input
+            v-model="formData.habits"
+            placeholder="请输入行为习惯"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">患病时间</div>
+        <div class="form-input">
+          <input
+            v-model="formData.illnessTime"
+            placeholder="请输入患病时间"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">疾病</div>
+        <div class="form-input">
+          <input
+            v-model="formData.disease"
+            placeholder="请输入疾病"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">家人的疾病</div>
+        <div class="form-input">
+          <input
+            v-model="formData.familyDisease"
+            placeholder="请输入家人的疾病"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">是否线下就诊</div>
+        <div class="form-input">
+          <input
+            v-model="formData.isLine"
+            placeholder="请输入是否线下就诊"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">体质</div>
+        <div class="form-input">
+          <input
+            v-model="formData.constitution"
+            placeholder="请输入体质"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">使用药品</div>
+        <div class="form-input">
+          <input
+            v-model="formData.medicine"
+            placeholder="请输入使用药品"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">咨询产品</div>
+        <div class="form-input">
+          <input
+            v-model="formData.consultProduct"
+            placeholder="请输入咨询产品"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">是否已购产品</div>
+        <div class="form-input">
+          <input
+            v-model="formData.isBuy"
+            placeholder="请输入是否已购产品"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+    </div>
+    <!-- 底部按钮 -->
+    <div class="user-edit-footer">
+      <button class="save-btn" v-loading="loading" :disabled="loading" @click="handleSave">保存修改</button>
+    </div>
+  </div>
+</template>
+
+<script>
+import {updateQwUserInfo} from "@/api/qw/im";
+
+export default {
+  name: "UserInfoEdit",
+  props: {
+    // 用户详情数据
+    userDetail: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      // 表单数据
+      formData: {},
+      // 加载状态
+      loading: false
+    }
+  },
+  created() {
+    // 初始化表单数据
+    this.formData = Object.assign({}, this.userDetail);
+  },
+  methods: {
+    // 返回
+    handleBack() {
+      this.$emit('back');
+    },
+    // 保存修改
+    handleSave() {
+      this.loading = true;
+      updateQwUserInfo(this.formData).then(response => {
+        if (response.code === 200) {
+          this.loading = false;
+          this.$message.success("修改成功");
+          this.$emit('save-success');
+        } else {
+          this.$message.error(response.msg || "修改失败");
+          this.loading = false;
+        }
+      }).catch(() => {
+        this.loading = false;
+      });
+    }
+  }
+}
+</script>
+
+<style scoped>
+.user-form {
+  margin-top: 10px;
+  background: #fff;
+}
+.form-row {
+  display: flex;
+  align-items: center;
+  border-bottom: 1px solid #f2f2f2;
+  padding: 0 16px;
+  height: 44px;
+}
+.form-label {
+  flex: 0 0 150px;
+  color: #333;
+  font-size: 15px;
+}
+.form-input {
+  flex: 1;
+  text-align: right;
+}
+.input {
+  border: none;
+  outline: none;
+  background: transparent;
+  font-size: 15px;
+  color: #999;
+  width: 100%;
+  text-align: right;
+}
+.input::placeholder {
+  color: #ccc;
+}
+.user-edit-footer {
+  width: 100%;
+  padding: 10px;
+  background: #fff;
+  border-top: 1px solid #f2f2f2;
+}
+.save-btn {
+  width: 100%;
+  height: 35px;
+  background: #1890ff;
+  color: #fff;
+  border: none;
+  border-radius: 22px;
+  font-size: 12px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+.save-btn:hover {
+  background: #40a9ff;
+}
+</style>

+ 173 - 0
src/views/qw/qwChat/userDetail/userStorerDetails.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="aacontainer">
+    <el-table v-loading="loading" :data="orderList" border>
+      <el-table-column label="订单号" align="center" prop="orderCode" width="180px"/>
+      <el-table-column label="用户昵称" align="center" prop="nickName" show-overflow-tooltip width="100px"/>
+      <el-table-column label="收货人" align="center" prop="userName" />
+      <el-table-column label="医生姓名" align="center" prop="doctorName" />
+      <el-table-column label="患者姓名" align="center" prop="patientName" />
+      <el-table-column label="订单总价" align="center" prop="totalPrice" />
+      <el-table-column label="应付金额" align="center" prop="payPrice" />
+      <el-table-column label="实付金额" align="center" prop="payMoney" />
+      <el-table-column label="订单状态" align="center" prop="status" >
+        <template slot-scope="scope">
+              <dict-tag :options="orderOptions" :value="scope.row.status"/>
+         </template>
+      </el-table-column>
+      <el-table-column label="支付状态 " align="center" prop="isPay" >
+        <template slot-scope="scope">
+              <dict-tag :options="payStatusOptions" :value="scope.row.isPay"/>
+            </template>
+      </el-table-column>
+      <el-table-column label="支付时间" align="center" prop="payTime" width="180" />
+      <el-table-column label="支付方式" align="center" prop="payType" >
+        <template slot-scope="scope">
+              <dict-tag :options="PayOptions" :value="scope.row.payType"/>
+         </template>
+      </el-table-column>
+      <el-table-column label="订单类型" align="center" prop="orderType" >
+        <template slot-scope="scope">
+              <dict-tag :options="orderTypeOptions" :value="scope.row.orderType"/>
+         </template>
+      </el-table-column>
+      <el-table-column label="退款状态" align="center" prop="refundStatus" >
+        <template slot-scope="scope">
+              <dict-tag :options="refundOptions" :value="scope.row.refundStatus"/>
+         </template>
+      </el-table-column>
+      <el-table-column label="快递名称" align="center" prop="deliveryName" />
+      <el-table-column label="快递单号" align="center" prop="deliverySn" />
+      <el-table-column label="是否开处方" align="center" prop="isPrescribe" >
+        <template slot-scope="scope">
+              <dict-tag :options="orOptions" :value="scope.row.isPrescribe"/>
+         </template>
+      </el-table-column>
+      <el-table-column label="处方编号" align="center" prop="prescribeCode" width="170px"/>
+      <el-table-column label="发货时间" align="center" prop="deliveryTime" />
+      <el-table-column label="创建时间" align="center" prop="createTime"  width="150px"/>
+      <el-table-column label="修改时间" align="center" prop="updateTime"  width="150px"/>
+      <el-table-column label="操作" fixed="right" width="100px" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleDetails(scope.row)"
+            v-hasPermi="['store:storeOrder:query']"
+          >查看</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-drawer
+      size="75%"
+      :title="show.title" :visible.sync="show.open"
+      :modal="false"
+    >
+      <product-order  ref="order" />
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import {listOrder} from "@/api/store/storeOrder";
+import productOrder from "@/views/store/components/productOrder.vue";
+import {mapState} from "vuex";
+export default {
+  name: "userInquir",
+  components: {productOrder},
+  props:["data"],
+  data() {
+    return {
+      actName:"10",
+      // 遮罩层
+      loading: true,
+      // 总条数
+      total: 0,
+      // 订单表格数据
+      orderList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyUserId: null,
+        userId: null,
+        status: null,
+      },
+       PayOptions:[],
+       orderOptions:[],
+       payStatusOptions:[],
+       refundOptions:[],
+       orderTypeOptions:[],
+       orOptions:[],
+      show: {
+        title: "订单详情",
+        open: false
+      }
+    };
+  },
+  computed: {
+    ...mapState({
+      companyUserId: state => state.user.user.userId,
+    })
+  },
+  created() {
+    this.getDicts("sys_inquiry_pay").then(response => {
+        this.PayOptions = response.data;
+      });
+    this.getDicts("store_order_type").then(response => {
+        this.orderTypeOptions = response.data;
+      });
+    this.getDicts("sys_order_status").then(response => {
+        this.orderOptions = response.data;
+      });
+    this.getDicts("sys_order_pay").then(response => {
+        this.payStatusOptions = response.data;
+      });
+    this.getDicts("sys_refund_status").then(response => {
+        this.refundOptions = response.data;
+      });
+    this.getDicts("sys_company_or").then(response => {
+          this.orOptions = response.data;
+        });
+    this.queryParams.companyUserId = this.companyUserId
+  },
+  methods: {
+    getUserDetails(id) {
+      this.queryParams.userId = id
+      this.getList();
+    },
+    /** 查询订单列表 */
+    getList() {
+      this.loading = true;
+      listOrder(this.queryParams).then(response => {
+        this.orderList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+
+    },
+    handleDetails(row){
+      this.show.open=true;
+      const orderId = row.id ;
+      setTimeout(() => {
+        this.$refs.order.getOrder(orderId);
+      }, 500);
+    },
+  }
+};
+</script>
+
+<style scoped>
+.aacontainer {
+  padding: 10px;
+  font-size: 12px;
+}
+</style>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است