wansfa hai 1 ano
pai
achega
3dd2a2637f
Modificáronse 61 ficheiros con 10623 adicións e 11 borrados
  1. 3 0
      package.json
  2. 77 0
      src/api/qw/im.js
  3. 42 0
      src/api/qw/login.js
  4. BIN=BIN
      src/assets/image/video.png
  5. 81 0
      src/components/LemonUI/components/avatar.vue
  6. 74 0
      src/components/LemonUI/components/badge.vue
  7. 70 0
      src/components/LemonUI/components/button.vue
  8. 584 0
      src/components/LemonUI/components/chatrecords/basic.vue
  9. 592 0
      src/components/LemonUI/components/chatrecords/basic1.vue
  10. 499 0
      src/components/LemonUI/components/chatrecords/segrecords.vue
  11. 145 0
      src/components/LemonUI/components/contact.vue
  12. 505 0
      src/components/LemonUI/components/editor.vue
  13. 1384 0
      src/components/LemonUI/components/index.vue
  14. 30 0
      src/components/LemonUI/components/lemon-message-voice.vue
  15. 251 0
      src/components/LemonUI/components/message/basic.vue
  16. 158 0
      src/components/LemonUI/components/message/elise-audio.vue
  17. 38 0
      src/components/LemonUI/components/message/event.vue
  18. 59 0
      src/components/LemonUI/components/message/file.vue
  19. 32 0
      src/components/LemonUI/components/message/image.vue
  20. 36 0
      src/components/LemonUI/components/message/text.vue
  21. 61 0
      src/components/LemonUI/components/message/video.vue
  22. 47 0
      src/components/LemonUI/components/message/voice.vue
  23. 200 0
      src/components/LemonUI/components/messages.vue
  24. 146 0
      src/components/LemonUI/components/popover.vue
  25. 1384 0
      src/components/LemonUI/components/records.vue
  26. 77 0
      src/components/LemonUI/components/tabs.vue
  27. 148 0
      src/components/LemonUI/database/contacts.js
  28. 167 0
      src/components/LemonUI/database/conversations.js
  29. 292 0
      src/components/LemonUI/database/emoji.js
  30. 360 0
      src/components/LemonUI/database/messages.js
  31. 220 0
      src/components/LemonUI/database/messages1.js
  32. 5 0
      src/components/LemonUI/database/user.js
  33. 83 0
      src/components/LemonUI/directives/contextmenu.js
  34. 0 0
      src/components/LemonUI/index.css
  35. 56 0
      src/components/LemonUI/index.js
  36. 21 0
      src/components/LemonUI/lastContentRender.js
  37. 13 0
      src/components/LemonUI/styles/common/animate.styl
  38. 45 0
      src/components/LemonUI/styles/common/icons.styl
  39. 3 0
      src/components/LemonUI/styles/common/index.styl
  40. 23 0
      src/components/LemonUI/styles/common/normalize.styl
  41. BIN=BIN
      src/components/LemonUI/styles/fonts/icon.woff
  42. 67 0
      src/components/LemonUI/styles/utils/bem.styl
  43. 47 0
      src/components/LemonUI/styles/utils/functional.styl
  44. 5 0
      src/components/LemonUI/styles/utils/index.styl
  45. 26 0
      src/components/LemonUI/styles/utils/var.styl
  46. 24 0
      src/components/LemonUI/utils/cache/memory.js
  47. 16 0
      src/components/LemonUI/utils/constant.js
  48. 134 0
      src/components/LemonUI/utils/index.js
  49. 37 0
      src/components/LemonUI/utils/validate.js
  50. 393 0
      src/components/Material/chat.vue
  51. 4 4
      src/components/Material/index.vue
  52. 758 0
      src/components/VideoPlayer/VueAliplayer.vue
  53. 12 0
      src/main.js
  54. 117 0
      src/utils/WebsocketHeartbeat.js
  55. 49 0
      src/utils/webSocket.js
  56. 2 0
      src/views/crm/customer/index.vue
  57. 0 0
      src/views/qw/components
  58. 914 0
      src/views/qw/qwChat/index.vue
  59. 3 0
      src/views/store/components/productOrder.vue
  60. 2 0
      src/views/store/statistics/storeOrder.vue
  61. 2 7
      src/views/store/storeOrder/index.vue

+ 3 - 0
package.json

@@ -62,12 +62,15 @@
     "quill": "1.3.7",
     "screenfull": "4.2.0",
     "sortablejs": "1.8.4",
+    "stylus": "^0.54.7",
+    "stylus-loader": "^3.0.2",
     "v-clipboard": "^2.2.3",
     "vue": "2.6.10",
     "vue-clipboard2": "^0.3.1",
     "vue-count-to": "1.0.13",
     "vue-cropper": "0.4.9",
     "vue-full-calendar": "^2.8.1-0",
+    "vue-mobile-audio": "^0.1.3",
     "vue-router": "3.0.2",
     "vue-splitpane": "1.0.4",
     "vue2-ace-editor": "0.0.15",

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

@@ -0,0 +1,77 @@
+import request from '@/utils/request'
+
+export function getContacts(deviceId) {
+  return request({
+    url: '/qw/contact/list/'+deviceId,
+    method: 'get'
+  })
+}
+
+
+export function getContactsByUser(userId) {
+  return request({
+    url: '/qw/contact/listByUser/'+userId,
+    method: 'get'
+  })
+}
+
+
+export function getConversations(userId) {
+  return request({
+    url: '/qw/contact/conversationList/'+userId,
+    method: 'get'
+  })
+}
+
+
+export function getConversation(query) {
+  return request({
+    url: '/qw/contact/conversation',
+    method: 'get',
+    params:query
+  })
+}
+
+
+
+export function getMessageList(query) {
+  return request({
+    url: '/qw/message/getList',
+    method: 'get',
+    params: query
+  })
+}
+
+
+// 发送文本消息
+export function sendTextMsg(data) {
+  return request({
+    url: '/qw/message/text',
+    method: 'post',
+    data: data
+  })
+}
+
+
+
+// 发送文本消息
+export function sendImageMsg(data) {
+  return request({
+    url: '/qw/message/image',
+    method: 'post',
+    data: data
+  })
+}
+
+
+export function getRoomInfo(roomId) {
+  return request({
+    url: '/qw/room/roomDetail/'+roomId,
+    method: 'get'
+  })
+}
+
+
+
+
+

+ 42 - 0
src/api/qw/login.js

@@ -0,0 +1,42 @@
+import request from '@/utils/request'
+
+
+//获取二维码
+export function getQrCode(deviceId) {
+  return request({
+    url: '/qw/login/getQrCode/' + deviceId,
+    method: 'get'
+  })
+}
+
+
+// 二维码
+export function verify(data) {
+  return request({
+    url: '/qw/login/verify',
+    method: 'post',
+    data:data 
+  })
+}
+
+
+export function getQwUser(deviceId) {
+  return request({
+    url: '/qw/login/getQwUserInfo/' + deviceId,
+    method: 'get'
+  })
+}
+
+
+//企微退出登录
+export function qwLoginOut(deviceId) {
+  return request({
+    url: '/qw/login/ban',
+    method: 'post',
+    data:deviceId 
+  })
+}
+
+
+
+

BIN=BIN
src/assets/image/video.png


+ 81 - 0
src/components/LemonUI/components/avatar.vue

@@ -0,0 +1,81 @@
+<script>
+export default {
+  name: "LemonAvatar",
+  inject: ["IMUI"],
+  props: {
+    src: String,
+    icon: {
+      type: String,
+      default: "lemon-icon-people",
+    },
+    circle: {
+      type: Boolean,
+      default() {
+        return this.IMUI ? this.IMUI.avatarCricle : false;
+      },
+    },
+    size: {
+      type: Number,
+      default: 32,
+    },
+  },
+  data() {
+    return {
+      imageFinishLoad: true,
+    };
+  },
+  render() {
+    return (
+      <span
+        style={this.style}
+        class={["lemon-avatar", { "lemon-avatar--circle": this.circle }]}
+        on-click={e => this.$emit("click", e)}
+      >
+        {(this.imageFinishLoad || !this.src) && <i class={this.icon} />}
+        <img src={this.src} onLoad={this._handleLoad} />
+      </span>
+    );
+  },
+  computed: {
+    style() {
+      const size = `${this.size}px`;
+      return {
+        width: size,
+        height: size,
+        lineHeight: size,
+        fontSize: `${this.size / 2}px`,
+      };
+    },
+  },
+  methods: {
+    _handleLoad() {
+      this.imageFinishLoad = false;
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-avatar)
+  font-variant tabular-nums
+  line-height 1.5
+  box-sizing border-box
+  margin 0
+  padding 0
+  list-style none
+  display inline-block
+  text-align center
+  background #ccc
+  color rgba(255,255,255,0.7)
+  white-space nowrap
+  position relative
+  overflow hidden
+  vertical-align middle
+  border-radius 4px
+  +m(circle)
+    border-radius 50%
+  img
+    width 100%
+    height 100%
+    display block
+</style>

+ 74 - 0
src/components/LemonUI/components/badge.vue

@@ -0,0 +1,74 @@
+<script>
+export default {
+  name: "LemonBadge",
+  props: {
+    count: [Number, Boolean],
+    overflowCount: {
+      type: Number,
+      default: 99
+    }
+  },
+  render() {
+    return (
+      <span class="lemon-badge">
+        {this.$slots.default}
+        {this.count !== 0 && this.count !== undefined && (
+          <span
+            class={[
+              "lemon-badge__label",
+              this.isDot && "lemon-badge__label--dot"
+            ]}
+          >
+            {this.label}
+          </span>
+        )}
+      </span>
+    );
+  },
+  computed: {
+    isDot() {
+      return this.count === true;
+    },
+    label() {
+      if (this.isDot) return "";
+      return this.count > this.overflowCount
+        ? `${this.overflowCount}+`
+        : this.count;
+    }
+  },
+  methods: {}
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-badge)
+  position relative
+  display inline-block
+  +e(label)
+    border-radius 10px
+    background #f5222d
+    color #fff
+    text-align center
+    font-size 12px
+    font-weight normal
+    white-space nowrap
+    box-shadow 0 0 0 1px #fff
+    z-index 10
+    position absolute
+    transform  translateX(50%)
+    transform-origin  100%
+    display inline-block
+    padding 0 4px
+    height 18px
+    line-height 17px
+    min-width 10px
+    top -4px
+    right 6px
+    +m(dot)
+      width 10px
+      height 10px
+      min-width auto
+      padding 0
+      top -3px
+      right 2px
+</style>

+ 70 - 0
src/components/LemonUI/components/button.vue

@@ -0,0 +1,70 @@
+<script>
+export default {
+  name: "LemonButton",
+  props: {
+    color: {
+      type: String,
+      default: "default"
+    },
+    disabled: Boolean
+  },
+  render() {
+    return (
+      <button
+        class={["lemon-button", `lemon-button--color-${this.color}`]}
+        disabled={this.disabled}
+        type="button"
+        on-click={this._handleClick}
+      >
+        {this.$slots.default}
+      </button>
+    );
+  },
+  methods: {
+    _handleClick(e) {
+      this.$emit("click", e);
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-button)
+  outline none
+  line-height 1.499
+  display inline-block
+  font-weight 400
+  text-align center
+  touch-action manipulation
+  cursor pointer
+  background-image none
+  border 1px solid #ddd
+  box-sizing border-box
+  white-space nowrap
+  padding 0 15px
+  font-size 14px
+  border-radius 4px
+  height 32px
+  user-select none
+  transition all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1)
+  color rgba(0, 0, 0, 0.65)
+  background-color #fff
+  box-shadow 0 2px 0 rgba(0, 0, 0, 0.015)
+  text-shadow 0 -1px 0 rgba(0, 0, 0, 0.12)
+  +m(color-default)
+    &:hover:not([disabled])
+      border-color #666
+      color #333
+    &:active
+      background-color #ddd
+    &[disabled]
+      cursor not-allowed
+      color #aaa
+      background #eee
+  +m(color-grey)
+    background #e1e1e1
+    border-color #e1e1e1
+    color #666
+    &:hover:not([disabled])
+      border-color #bbb
+</style>

+ 584 - 0
src/components/LemonUI/components/chatrecords/basic.vue

@@ -0,0 +1,584 @@
+<template>
+<div>
+
+    <el-dialog   :visible.sync="dialogVisible" width="70%">
+        <div class="app-container">  
+             <el-form ref="form" :model="form" :inline="true"  label-width="80px">
+                <el-row>
+                    <el-col :span="22">
+                        <el-form-item label="搜索内容" prop="content" style="width:100%" >
+                            <el-input @input="serchInput"  style="width:100%"  v-model="form.content"  placeholder="请输入搜索消息内容"  clearable />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="2">
+                        <el-form-item >
+                            <el-button type="primary" @click="handleQuery">搜索</el-button>
+                        </el-form-item>
+                    </el-col>
+                 </el-row>
+              </el-form>
+
+             <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>
+
+                <el-tab-pane label="图片与视频" name="second">
+                     <SegRecords :qwUser="qwUser" :conversationId="conversationId" :conversationData="conversationData" :msgType=1   ref="sIMUI1"></SegRecords>
+                </el-tab-pane>
+
+                <el-tab-pane label="语音" name="third">
+                    <SegRecords :qwUser="qwUser" :conversationId="conversationId" :conversationData="conversationData"  :msgType=2  ref="sIMUI2"></SegRecords>
+                </el-tab-pane>
+
+                <el-tab-pane label="文件" name="fourth">
+                    <SegRecords :qwUser="qwUser" :conversationId="conversationId" :conversationData="conversationData" :msgType=3  ref="sIMUI3"></SegRecords>
+                </el-tab-pane>
+            </el-tabs>
+          
+        </div>
+    </el-dialog>
+
+     <el-dialog :visible.sync="dialogContextVisible" width="70%">
+            <SegRecords :qwUser="qwUser" :conversationId="conversationId" :conversationData="conversationData" :isContext=true :msgType=0  ref="sIMUI4"></SegRecords>
+      </el-dialog>
+
+
+</div>
+
+</template>
+
+<script>
+
+  import LemonMessageVoice from "@/components/LemonUI/components/message/voice";
+  import { getContactsByUser,getConversations,getMessageList,getConversation,sendTextMsg,sendImageMsg,getRoomInfo} from '@/api/qw/im';
+  import Conversations from "@/components/LemonUI/database/conversations";
+  import EmojiData from "@/components/LemonUI/database/emoji";
+  import '@/components/LemonUI/index.css';
+  import VideoPlayer from '@/components/VideoPlayer/VueAliplayer'
+  import SegRecords from './segrecords.vue'
+  let pages = {};
+  export default {
+       name: "ChatRecords",
+       components: {   
+          VideoPlayer,
+          SegRecords
+       },
+       props: {
+            qwUser: {
+                type: Object,
+                default: () => {
+                    return {
+                          //"userId":1688854331463712,"avatar":"https://wework.qpic.cn/wwpic/258414_QSUkkN5rTMGzwIc_1692169272/0","corpId":1970326754004217,"corpName":"重庆润方数字科技","nickName":"秦先觉","deviceId":"aeff5e72448d4d299e03d7291a21c2ea","phone":null,"account":"1261818888"
+                     };
+                },
+          },
+       },
+       data(){
+           return {
+                activeName: 'first',
+                dialogVisible:false,
+                dialogContextVisible:false,
+                theme: "default",
+                IMUI:null,
+                hideMenuAvatar: false,
+                hideMenu: false,
+                hideMessageName: false,
+                hideMessageTime: true,
+                showSearch:false,
+                UserData: {
+                    id: "1000",
+                    displayName: "June",
+                    avatar: "https://p.qqan.com/up/2018-4/15244505348390471.jpg",
+                },
+                contactData:null,
+                conversationId:null,
+                conversationData:null,
+                isFromLogin:false,
+                dialogImgVisible:false,
+                dialogImageUrl: '',
+                dialogVideoVisible:false, 
+                dialogVideoUrl:'',
+                dialogVideoCover:'',
+                imageArr:[],
+                pickUploadImgData:null,
+                aplayer: {
+                    vid: "bf9b7e4a36d84aea8cee769765fbc28b",
+                    pWidth:"1040px",
+                    width:"1000px",
+                    height:"900px",
+                    videoWidth:"1000px",
+                    videoHeight:"900px"
+                },
+                player: null,
+                roomMembers:[],
+                roomAdmins:[],
+                roomInfo:null,
+                windowWidth: document.documentElement.clientWidth*0.95,  //实时屏幕宽度
+                windowHeight: document.documentElement.clientHeight*0.7,   //实时屏幕高度
+                queryParams: {
+                    pageNum: 1,
+                    pageSize: 10,
+                    conversationId: null,
+                },
+                form: { content:"" },
+                // 表单校验
+                rules: {
+                    content: [
+                        { required: true, message: "请输入关键字", trigger: "blur" }
+                    ],
+                }
+           }
+       },
+       created(){
+           
+       },
+       mounted(){
+           
+       },
+       methods: {
+
+                handleTabClick(tab, event){
+                    console.log(tab,tab.index);
+                    const sIMUI = this.$refs["sIMUI"+tab.index];
+                    sIMUI.open();
+                },
+                serchInput(value){
+                   this.showSearch=value!="";
+                   if(value!=""){
+                        const sIMUI = this.$refs["sIMUI"];
+                        sIMUI.openSearch(value);
+                   }else{
+                   }
+                   console.log("qxj serchInput:"+JSON.stringify(value));
+                },
+                /** 搜索按钮操作 */
+                handleQuery() {
+                    if(this.form.content!=""){
+                         const sIMUI = this.$refs["sIMUI"];
+                         sIMUI.openSearch(this.form.content);
+                    }
+                },
+                showContextBox(message){
+                    this.dialogContextVisible=true;
+                    setTimeout(() => {
+                         const sIMUI = this.$refs["sIMUI4"];
+                         console.log("点击了上下文", sIMUI);
+                         sIMUI.openContextBox(message);
+                    }, 50);
+                },
+                init(){
+                     this.deviceId=this.qwUser.deviceId;
+                     this.UserData.id=this.qwUser.userId;
+                     this.UserData.displayName=this.qwUser.nickName;
+                     this.UserData.avatar=this.qwUser.avatar;
+                     const IMUI = this.$refs.IMUI;
+                    //IMUI.changeContact(this.queryParams.conversationId);
+                    //this.getContacts();  //获取联系人信息
+                    this.getConversation();   //获取会话信息
+                    ////IMUI.initEmoji(EmojiData); 
+                },
+                open(qwUser,conversationId,conversationData){
+                     this.dialogVisible=true;
+                     var converChanged=this.conversationId!=conversationId;
+                     this.qwUser=qwUser;
+                     this.conversationId=conversationId;
+                     this.conversationData=conversationData;
+                     this.queryParams.conversationId=conversationId;
+                     setTimeout(() => {
+                         const sIMUI = this.$refs.sIMUI0;
+                         sIMUI.open(converChanged);
+                    }, 50);
+
+                },
+                getRoomInfo(roomId){
+                    getRoomInfo(roomId).then(response => {
+                        this.roomMembers=response.members;
+                        this.admins=response.members;
+                        this.roomInfo=response.detail;
+                    });
+                },
+                getContacts(){  
+                    getContactsByUser(this.qwUser.userId).then(response => {
+                        this.contactData = response.data;
+                        this.$refs.IMUI.initContacts(this.contactData);
+                        
+                    });
+                },
+                getConversation(){
+                    let that=this;
+                    const IMUI = this.$refs.IMUI;
+                    //getConversations(this.qwUser.userId).then(response => {
+                    //this.conversationData = response.data;
+                        IMUI.initConversations(this.conversationData);
+                        var fstConversation=this.conversationData[0];
+                        if(fstConversation){
+                            IMUI.changeContact(fstConversation.conversationId);
+                        }
+                    //});
+                },
+                messageTimeFormat(time) {
+                   return this.friendlyDate(time);
+                },
+                changeTheme() {
+                  this.theme = this.theme == "default" ? "blue" : "default";
+                },
+                scrollToTop() {
+                  document.body.scrollIntoView();
+                },
+                openDrawer(position) {
+                        const IMUI = this.$refs.IMUI;
+                        const params = {
+                            position,
+                            render: contact => {
+                            return (
+                                <div style="padding:15px">
+                                <h5>{contact.displayName}</h5>
+                                <span style="cursor:pointer;" on-click={IMUI.closeDrawer}>关闭抽屉</span>
+                                </div>
+                            );
+                            },
+                };
+                if (position == "center") {
+                    params.width = "50%";
+                    params.height = "50%";
+                } else if (position == "rightInside") {
+                    params.height = "90%";
+                    params.offsetY = "10%";
+                }
+                this.IMUI.openDrawer(params);
+                },
+                handlePullMessages(contact, next,instance) {
+                    const { IMUI } = this.$refs;
+                    let isEnd = false;
+                    getMessageList(this.queryParams).then(response => {
+                        if(response.code==200){
+                            isEnd=response.data.isLastPage;
+                            next(response.data.list, isEnd);
+                            if(!isEnd){
+                                pages[contact.conversationId]++;
+                            }
+                        }
+                    });
+                },
+                handleChangeConversation(conversation, instance) {
+                    console.log("qxj ChangeConversation:"+JSON.stringify(conversation));
+                    if (!pages[conversation.conversationId]){
+                        pages[conversation.conversationId] =1;
+                    }
+                    this.queryParams.pageNum=pages[conversation.conversationId];
+                    this.queryParams.conversationId=conversation.conversationId;
+                    if(conversation.unread>0){
+                        conversation.unread=0;
+                        instance.updateContact(conversation);
+                    }
+                    instance.closeDrawer();
+                },
+                handleChangeContact(contact, instance) {
+
+                },
+                //收到消息后添加消息显示
+                appendMessageAction(msgData){
+                    if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){     //文本   text  image  video  voice 
+                        const message = {
+                                id: msgData.id,
+                                status: msgData.status,
+                                type: msgData.type,
+                                sendTime: msgData.sendTime,
+                                content: msgData.content,
+                                params1: "1",
+                                params2: "2",
+                                toContactId: msgData.toContactId,
+                                fromUser: msgData.fromUser,
+                        };
+                        this.appendRemoteMessage(message);
+                    }
+                    else if(msgData.type=="file"){
+                        const message = {
+                                id: msgData.id,
+                                status: msgData.status,
+                                type: msgData.type,
+                                sendTime: msgData.sendTime,
+                                content: msgData.content,
+                                toContactId: msgData.toContactId,
+                                fromUser: msgData.fromUser,
+                                fileName:msgData.fileName,
+                                fileSize:msgData.fileSize
+                        };
+                        this.appendRemoteMessage(message);
+                    }
+                    else{
+                        const message = {
+                                id: msgData.id,
+                                status: msgData.status,
+                                type: msgData.type,
+                                sendTime: msgData.sendTime,
+                                content: msgData.content,
+                                toContactId: msgData.toContactId,
+                                fromUser: msgData.fromUser,
+                        };
+                        this.appendRemoteMessage(message);
+                    }
+                },
+                handleMenuAvatarClick() {
+                console.log("Event:menu-avatar-click");
+                },
+                //聊天工具栏点击图片
+                handleImageClick() { 
+                
+                },
+                //选择图片框确定按钮回调
+                handlePickImageDone(data){
+                    console.log("handlePickImageDone:"+JSON.stringify(data));
+                    this.pickUploadImgData=data;
+                    const IMUI = this.$refs.IMUI;
+                    IMUI._handleRemoteImage(data.url);
+                },
+                tooglePlayVideo(data){
+                    this.dialogVideoVisible=true;
+                    this.dialogVideoUrl=data.content;
+                    this.dialogVideoCover=data.url;
+                    const player = this.$refs.player.instance
+                    player && player.play()
+                },
+                handleMessageClick(e, key, message, instance) {
+                    console.log("点击了消息", e, key, message);
+                    //console.log("qxj message:"+JSON.stringify(message));
+                    if(message.type=="image"){
+                        var url=!!message.url?message.url:message.content;
+                        this.handlePicturePreview(url);
+                    }
+                    else if(message.type=="video"){  
+                        this.tooglePlayVideo(message);
+                    }
+                    else if(message.type=="file"){  
+                        
+                    }
+                    if (key == "status") {
+                        instance.updateMessage({
+                        id: message.id,
+                        status: "going",
+                        content: "正在重新发送消息...",
+                        });
+                        setTimeout(() => {
+                        instance.updateMessage({
+                            id: message.id,
+                            status: "succeed",
+                            content: "发送成功",
+                        });
+                        }, 2000);
+                    }
+                },
+
+                changeMenuAvatarVisible() {
+                   this.hideMenuAvatar = !this.hideMenuAvatar;
+                },
+                changeMenuVisible() {
+                   this.hideMenu = !this.hideMenu;
+                },
+                changeMessageNameVisible() {
+                   this.hideMessageName = !this.hideMessageName;
+                },
+                changeMessageTimeVisible() {
+                   this.hideMessageTime = !this.hideMessageTime;
+                },
+                removeMessage() {
+                    const { IMUI } = this.$refs;
+                    const messages = IMUI.getCurrentMessages();
+                    const id = messages[messages.length - 1].id;
+                    if (messages.length > 0) {
+                        IMUI.removeMessage(id);
+                    }
+                },
+                updateMessage() {
+                    const { IMUI } = this.$refs;
+                    const messages = IMUI.getCurrentMessages();
+                    const message = messages[messages.length - 1];
+                    if (messages.length > 0) {
+                        const update = {
+                        id: message.id,
+                        status: "succeed",
+                        type: "file",
+                        fileName: "被修改成文件了.txt",
+                        fileSize: "4200000",
+                        };
+                        if (message.type == "event") {
+                        update.fromUser = this.user;
+                        }
+                        IMUI.updateMessage(update);
+                        IMUI.messageViewToBottom();
+                }
+                },
+                appendCustomMessage() {
+                    const { IMUI } = this.$refs;
+                    const message = {
+                        id: generateRandId(),
+                        status: "succeed",
+                        type: "voice",
+                        sendTime: getTime(),
+                        content: "语音消息",
+                        params1: "1",
+                        params2: "2",
+                        toContactId: "contact-1",
+                        fromUser: this.user,
+                    };
+                    IMUI.appendMessage(message, true);
+                },
+                appendMessage() {
+                    const { IMUI } = this.$refs;
+                    const contact = IMUI.currentContact;
+                    const message = generateMessage("contact-3");
+                    message.fromUser = {
+                        ...message.fromUser,
+                        ...this.user,
+                    };
+                    IMUI.appendMessage(message, true);
+                    console.log("🚀 ~ file: App.vue ~ line 1508 ~ appendMessage ~ message", message)
+                },
+                appendEventMessage() {
+                    const { IMUI } = this.$refs;
+                    const message = {
+                        id: generateRandId(),
+                        type: "event",
+                        content: (
+                        <span>
+                            邀请你加入群聊{" "}
+                            <span
+                            style="color:#333;cursor:pointer"
+                            on-click={() => alert("OK")}>
+                            接受
+                            </span>
+                        </span>
+                        ),
+                        toContactId: "contact-3",
+                        sendTime: getTime(),
+                    };
+                    IMUI.appendMessage(message, true);
+                },
+                appendRemoteMessage(message) {  //从服务端返回的消息
+                    const { IMUI } = this.$refs;
+                    IMUI.appendMessage(message, true);
+                },
+                updateContact() {
+                    this.$refs.IMUI.updateContact({
+                        id: "contact-3",
+                        unread: 10,
+                        displayName: generateRandWord(),
+                        lastSendTime: getTime(),
+                        lastContent: "修改昵称为随机字母",
+                     });
+                },
+                handleChangeMenu() {
+                     console.log("Event:change-menu");
+                },
+                openCustomContainer() {
+
+                },
+                handlePicturePreview(url) {
+                    this.dialogImageUrl = url;
+                    this.dialogImgVisible=true;
+                },
+       }
+  }
+
+</script>
+
+
+
+<style lang="scss" scoped>
+    .app-container{
+       padding-top:0;
+    }
+    .imui-center{
+        margin-bottom:"60px"; 
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        // height: 80vh;
+    }
+    .lemon-wrapper{
+        border:"1px solid #ddd";
+        height: "60%" !important;
+    }
+    .lemon-drawer{
+        border:"1px solid #ddd";
+        border-left:"0";
+    }
+    .more {
+        font-size: 12px;
+        line-height: 24px;
+        height: 24px;
+        position: absolute;
+        top: 14px;
+        right: 14px;
+        cursor: pointer;
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+        color: #f1f1f1;
+        display: inline-block;
+        border-radius: 4px;
+        background: #111;
+        padding: 0 8px;
+    }
+
+    /deep/.el-dialog__headerbtn{
+         top:10px;
+    }
+
+    /deep/.el-form--inline .el-form-item{
+        display: flex;
+    }
+    /deep/.el-form-item--medium .el-form-item__content{
+        flex: 1;
+        margin-right: 20px;
+    }
+
+    /deep/.el-dialog:not(.is-fullscreen) {
+        margin-top: 0 !important;
+    }
+
+    // /deep/.el-dialog__header{
+    //     padding-bottom: 0;
+    // }
+    
+</style>
+
+
+<style lang="stylus">
+   .lemon-container__title
+     padding-bottom:10px;
+   .slot-group
+      width:200px;
+      .slot-group-title
+        padding:0 0 10px 0;
+        .slot-group-notice
+           padding: 10px 10px;
+     
+      .slot-search
+           width:calc(100% - 20px) ;
+           margin:5px 10px;
+           padding:3px 0px;
+
+      .slot-group-member
+          display: flex;
+          padding: 5px 0;
+          flex:1;
+          font-size: 14px;
+          align-items: center;
+          .avatar
+              width: 30px;
+              height: 30px;
+              line-height: 30px;
+              margin-right:5px;
+              img 
+                vertical-align: middle;
+                border-style: none;
+                width: 100%;
+                height: 100%;
+                line-height: 30px;
+                border-radius: 50%;
+</style>

+ 592 - 0
src/components/LemonUI/components/chatrecords/basic1.vue

@@ -0,0 +1,592 @@
+<template>
+<div>
+
+    <el-dialog   :visible.sync="dialogVisible" width="70%">
+        <div class="app-container">  
+             <el-form ref="form" :model="form" :inline="true"  label-width="80px">
+                <el-row>
+                    <el-col :span="22">
+                        <el-form-item label="搜索内容" prop="content" style="width:100%" >
+                            <el-input  style="width:100%"  v-model="form.content"  placeholder="请输入搜索消息内容" />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="2">
+                        <el-form-item >
+                            <el-button type="primary" @click="handleQuery">搜索</el-button>
+                        </el-form-item>
+                    </el-col>
+                 </el-row>
+              </el-form>
+
+            <div class="imui-center qq-lemon-imui">
+                <lemon-records  class="lemon-slot"  
+                    :width="windowWidth"
+                    :height="windowHeight"
+                    :user="UserData"
+                    ref="IMUI"  
+                    :theme="theme"
+                    :hide-menu="hideMenu"
+                    :hide-menu-avatar="hideMenuAvatar"
+                    :hide-message-name="hideMessageName"
+                    :hide-message-time="hideMessageTime"
+                    :messageTimeFormat="messageTimeFormat"
+                    @change-menu="handleChangeMenu"
+                    @pull-messages="handlePullMessages"
+                    @change-contact="handleChangeContact"
+                    @change-conversation="handleChangeConversation"
+                    @message-click="handleMessageClick"
+                    @menu-avatar-click="handleMenuAvatarClick"
+                    @pick-image="handleImageClick">
+
+                    <template #cover>
+                            <div class="cover">
+                                <!-- <i class="lemon-icon-message"></i>
+                                <p>
+                                    <b>自定义封面 Lemon</b>IMUI
+                                </p> -->
+                            </div>
+                    </template>
+
+                    <template #message-title="contact">
+                        <div>
+                            <div style="display:flex;justify-content:space-between">
+                                <span>{{contact.displayName}}</span>
+                                <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')">
+                                    居中
+                                </span> -->
+                                </span>
+                            </div>
+                            <!-- <div v-if="contact.isGroup" class="slot-group-menu">
+                                <span>聊天</span>
+                                <span>公告</span>
+                                <span>相册</span>
+                                <span>文件</span>
+                                <span>活动</span>
+                                <span>设置(左键弹出菜单)</span> 
+                            </div> -->
+                        </div>
+                    </template>
+                </lemon-records>
+
+            </div>
+
+            
+        </div>
+    </el-dialog>
+
+    <el-dialog :visible.sync="dialogImgVisible">
+        <img width="100%" :src="dialogImageUrl" alt="" />
+    </el-dialog>
+
+     <el-dialog :visible.sync="dialogVideoVisible"
+               :close-on-click-modal="false"
+               :destroy-on-close="true"
+               :width="aplayer.pWidth"
+               :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" 
+                :source="dialogVideoUrl" :cover="dialogVideoCover" ref="player">
+              </videoPlayer>
+       </el-dialog>
+
+</div>
+
+</template>
+
+<script>
+
+  import LemonMessageVoice from "@/components/LemonUI/components/message/voice";
+  import { getContactsByUser,getConversations,getMessageList,getConversation,sendTextMsg,sendImageMsg,getRoomInfo} from '@/api/qw/im';
+  import Conversations from "@/components/LemonUI/database/conversations";
+  import EmojiData from "@/components/LemonUI/database/emoji";
+  import '@/components/LemonUI/index.css';
+  import VideoPlayer from '@/components/VideoPlayer/VueAliplayer'
+  let pages = {};
+  export default {
+       name: "ChatRecords",
+       components: {   
+          VideoPlayer,
+       },
+       data(){
+           return {
+                dialogVisible:false,
+                theme: "default",
+                IMUI:null,
+                hideMenuAvatar: false,
+                hideMenu: false,
+                hideMessageName: false,
+                hideMessageTime: true,
+                qwUser:{
+                    "userId":1688854331463712,"avatar":"https://wework.qpic.cn/wwpic/258414_QSUkkN5rTMGzwIc_1692169272/0","corpId":1970326754004217,"corpName":"重庆润方数字科技","nickName":"秦先觉","deviceId":"aeff5e72448d4d299e03d7291a21c2ea","phone":null,"account":"1261818888"
+                },
+                showQW:false,
+                UserData: {
+                    id: "1000",
+                    displayName: "June",
+                    avatar: "https://p.qqan.com/up/2018-4/15244505348390471.jpg",
+                },
+                contactData:null,
+                conversationData:null,
+                isFromLogin:false,
+                dialogImgVisible:false,
+                dialogImageUrl: '',
+                dialogVideoVisible:false, 
+                dialogVideoUrl:'',
+                dialogVideoCover:'',
+                imageArr:[],
+                pickUploadImgData:null,
+                aplayer: {
+                    vid: "bf9b7e4a36d84aea8cee769765fbc28b",
+                    pWidth:"1040px",
+                    width:"1000px",
+                    height:"900px",
+                    videoWidth:"1000px",
+                    videoHeight:"900px"
+                    
+                },
+                player: null,
+                roomMembers:[],
+                roomAdmins:[],
+                roomInfo:null,
+                windowWidth: document.documentElement.clientWidth*0.95,  //实时屏幕宽度
+                windowHeight: document.documentElement.clientHeight*0.7,   //实时屏幕高度
+                queryParams: {
+                    pageNum: 1,
+                    pageSize: 10,
+                    conversationId: null,
+                },
+                form: { content:"" },
+                // 表单校验
+                rules: {
+                    content: [
+                        { required: true, message: "请输入关键字", trigger: "blur" }
+                    ],
+                }
+           }
+       },
+        created(){
+           
+       },
+       mounted(){
+            this.deviceId=this.qwUser.deviceId;
+            this.UserData.id=this.qwUser.userId;
+            this.UserData.displayName=this.qwUser.nickName;
+            this.UserData.avatar=this.qwUser.avatar;
+       },
+       methods: {
+                /** 搜索按钮操作 */
+                handleQuery() {
+                   
+                },
+                init(){
+                    const IMUI = this.$refs.IMUI;
+                    //IMUI.changeContact(this.queryParams.conversationId);
+                    //this.getContacts();  //获取联系人信息
+                    this.getConversation();   //获取会话信息
+                    ////IMUI.initEmoji(EmojiData); 
+                },
+                open(conversationId,conversationData){
+                     this.conversationData=conversationData;
+                     this.queryParams.conversationId=conversationId;
+                     console.log("qxj conversationId:"+conversationId);
+                     this.dialogVisible=true;
+                      setTimeout(() => {
+                         const IMUI = this.$refs.IMUI;
+                         console.log("qxj records imui:"+IMUI);
+                         this.init();
+                    }, 500);
+                },
+                getRoomInfo(roomId){
+                    getRoomInfo(roomId).then(response => {
+                        this.roomMembers=response.members;
+                        this.admins=response.members;
+                        this.roomInfo=response.detail;
+                    });
+                },
+                getContacts(){  
+                    getContactsByUser(this.qwUser.userId).then(response => {
+                        this.contactData = response.data;
+                        this.$refs.IMUI.initContacts(this.contactData);
+                        
+                    });
+                },
+                getConversation(){
+                    let that=this;
+                    const IMUI = this.$refs.IMUI;
+                    //getConversations(this.qwUser.userId).then(response => {
+                    //    this.conversationData = response.data;
+                        IMUI.initConversations(this.conversationData);
+                        var fstConversation=this.conversationData[0];
+                        if(fstConversation){
+                            IMUI.changeContact(fstConversation.conversationId);
+                        }
+                    //});
+                },
+                messageTimeFormat(time) {
+                   return this.friendlyDate(time);
+                },
+                changeTheme() {
+                  this.theme = this.theme == "default" ? "blue" : "default";
+                },
+                scrollToTop() {
+                  document.body.scrollIntoView();
+                },
+                openDrawer(position) {
+                        const IMUI = this.$refs.IMUI;
+                        const params = {
+                            position,
+                            render: contact => {
+                            return (
+                                <div style="padding:15px">
+                                <h5>{contact.displayName}</h5>
+                                <span style="cursor:pointer;" on-click={IMUI.closeDrawer}>关闭抽屉</span>
+                                </div>
+                            );
+                            },
+                };
+                if (position == "center") {
+                    params.width = "50%";
+                    params.height = "50%";
+                } else if (position == "rightInside") {
+                    params.height = "90%";
+                    params.offsetY = "10%";
+                }
+                this.IMUI.openDrawer(params);
+                },
+                handlePullMessages(contact, next,instance) {
+                    const { IMUI } = this.$refs;
+                    let isEnd = false;
+                    getMessageList(this.queryParams).then(response => {
+                        if(response.code==200){
+                            isEnd=response.data.isLastPage;
+                            next(response.data.list, isEnd);
+                            if(!isEnd){
+                                pages[contact.conversationId]++;
+                            }
+                        }
+                    });
+                },
+                handleChangeConversation(conversation, instance) {
+                    console.log("qxj ChangeConversation:"+JSON.stringify(conversation));
+                    if (!pages[conversation.conversationId]){
+                        pages[conversation.conversationId] =1;
+                    }
+                    this.queryParams.pageNum=pages[conversation.conversationId];
+                    this.queryParams.conversationId=conversation.conversationId;
+                    if(conversation.unread>0){
+                        conversation.unread=0;
+                        instance.updateContact(conversation);
+                    }
+                    instance.closeDrawer();
+                },
+                handleChangeContact(contact, instance) {
+
+                },
+                //收到消息后添加消息显示
+                appendMessageAction(msgData){
+                    if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){     //文本   text  image  video  voice 
+                        const message = {
+                                id: msgData.id,
+                                status: msgData.status,
+                                type: msgData.type,
+                                sendTime: msgData.sendTime,
+                                content: msgData.content,
+                                params1: "1",
+                                params2: "2",
+                                toContactId: msgData.toContactId,
+                                fromUser: msgData.fromUser,
+                        };
+                        this.appendRemoteMessage(message);
+                    }
+                    else if(msgData.type=="file"){
+                        const message = {
+                                id: msgData.id,
+                                status: msgData.status,
+                                type: msgData.type,
+                                sendTime: msgData.sendTime,
+                                content: msgData.content,
+                                toContactId: msgData.toContactId,
+                                fromUser: msgData.fromUser,
+                                fileName:msgData.fileName,
+                                fileSize:msgData.fileSize
+                        };
+                        this.appendRemoteMessage(message);
+                    }
+                    else{
+                        const message = {
+                                id: msgData.id,
+                                status: msgData.status,
+                                type: msgData.type,
+                                sendTime: msgData.sendTime,
+                                content: msgData.content,
+                                toContactId: msgData.toContactId,
+                                fromUser: msgData.fromUser,
+                        };
+                        this.appendRemoteMessage(message);
+                    }
+                },
+                handleMenuAvatarClick() {
+                console.log("Event:menu-avatar-click");
+                },
+                //聊天工具栏点击图片
+                handleImageClick() { 
+                
+                },
+                //选择图片框确定按钮回调
+                handlePickImageDone(data){
+                    console.log("handlePickImageDone:"+JSON.stringify(data));
+                    this.pickUploadImgData=data;
+                    const IMUI = this.$refs.IMUI;
+                    IMUI._handleRemoteImage(data.url);
+                },
+                tooglePlayVideo(data){
+                    this.dialogVideoVisible=true;
+                    this.dialogVideoUrl=data.content;
+                    this.dialogVideoCover=data.url;
+                    const player = this.$refs.player.instance
+                    player && player.play()
+                },
+                handleMessageClick(e, key, message, instance) {
+                    console.log("点击了消息", e, key, message);
+                    //console.log("qxj message:"+JSON.stringify(message));
+                    if(message.type=="image"){
+                        var url=!!message.url?message.url:message.content;
+                        this.handlePicturePreview(url);
+                    }
+                    else if(message.type=="video"){  
+                        this.tooglePlayVideo(message);
+                    }
+                    else if(message.type=="file"){  
+                        
+                    }
+                    if (key == "status") {
+                        instance.updateMessage({
+                        id: message.id,
+                        status: "going",
+                        content: "正在重新发送消息...",
+                        });
+                        setTimeout(() => {
+                        instance.updateMessage({
+                            id: message.id,
+                            status: "succeed",
+                            content: "发送成功",
+                        });
+                        }, 2000);
+                    }
+                },
+
+                changeMenuAvatarVisible() {
+                   this.hideMenuAvatar = !this.hideMenuAvatar;
+                },
+                changeMenuVisible() {
+                   this.hideMenu = !this.hideMenu;
+                },
+                changeMessageNameVisible() {
+                   this.hideMessageName = !this.hideMessageName;
+                },
+                changeMessageTimeVisible() {
+                   this.hideMessageTime = !this.hideMessageTime;
+                },
+                removeMessage() {
+                    const { IMUI } = this.$refs;
+                    const messages = IMUI.getCurrentMessages();
+                    const id = messages[messages.length - 1].id;
+                    if (messages.length > 0) {
+                        IMUI.removeMessage(id);
+                    }
+                },
+                updateMessage() {
+                    const { IMUI } = this.$refs;
+                    const messages = IMUI.getCurrentMessages();
+                    const message = messages[messages.length - 1];
+                    if (messages.length > 0) {
+                        const update = {
+                        id: message.id,
+                        status: "succeed",
+                        type: "file",
+                        fileName: "被修改成文件了.txt",
+                        fileSize: "4200000",
+                        };
+                        if (message.type == "event") {
+                        update.fromUser = this.user;
+                        }
+                        IMUI.updateMessage(update);
+                        IMUI.messageViewToBottom();
+                }
+                },
+                appendCustomMessage() {
+                    const { IMUI } = this.$refs;
+                    const message = {
+                        id: generateRandId(),
+                        status: "succeed",
+                        type: "voice",
+                        sendTime: getTime(),
+                        content: "语音消息",
+                        params1: "1",
+                        params2: "2",
+                        toContactId: "contact-1",
+                        fromUser: this.user,
+                    };
+                    IMUI.appendMessage(message, true);
+                },
+                appendMessage() {
+                    const { IMUI } = this.$refs;
+                    const contact = IMUI.currentContact;
+                    const message = generateMessage("contact-3");
+                    message.fromUser = {
+                        ...message.fromUser,
+                        ...this.user,
+                    };
+                    IMUI.appendMessage(message, true);
+                    console.log("🚀 ~ file: App.vue ~ line 1508 ~ appendMessage ~ message", message)
+                },
+                appendEventMessage() {
+                    const { IMUI } = this.$refs;
+                    const message = {
+                        id: generateRandId(),
+                        type: "event",
+                        content: (
+                        <span>
+                            邀请你加入群聊{" "}
+                            <span
+                            style="color:#333;cursor:pointer"
+                            on-click={() => alert("OK")}>
+                            接受
+                            </span>
+                        </span>
+                        ),
+                        toContactId: "contact-3",
+                        sendTime: getTime(),
+                    };
+                    IMUI.appendMessage(message, true);
+                },
+                appendRemoteMessage(message) {  //从服务端返回的消息
+                    const { IMUI } = this.$refs;
+                    IMUI.appendMessage(message, true);
+                },
+                updateContact() {
+                    this.$refs.IMUI.updateContact({
+                        id: "contact-3",
+                        unread: 10,
+                        displayName: generateRandWord(),
+                        lastSendTime: getTime(),
+                        lastContent: "修改昵称为随机字母",
+                     });
+                },
+                handleChangeMenu() {
+                     console.log("Event:change-menu");
+                },
+                openCustomContainer() {
+
+                },
+                handlePicturePreview(url) {
+                    this.dialogImageUrl = url;
+                    this.dialogImgVisible=true;
+                },
+       }
+  }
+
+</script>
+
+
+
+<style lang="scss" scoped>
+    .app-container{
+       padding-top:0;
+    }
+    .imui-center{
+        margin-bottom:"60px"; 
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        // height: 80vh;
+    }
+    .lemon-wrapper{
+        border:"1px solid #ddd";
+        height: "60%" !important;
+    }
+    .lemon-drawer{
+        border:"1px solid #ddd";
+        border-left:"0";
+    }
+    .more {
+        font-size: 12px;
+        line-height: 24px;
+        height: 24px;
+        position: absolute;
+        top: 14px;
+        right: 14px;
+        cursor: pointer;
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+        color: #f1f1f1;
+        display: inline-block;
+        border-radius: 4px;
+        background: #111;
+        padding: 0 8px;
+    }
+
+    /deep/.el-dialog__headerbtn{
+         top:10px;
+    }
+
+    /deep/.el-form--inline .el-form-item{
+        display: flex;
+    }
+    /deep/.el-form-item--medium .el-form-item__content{
+        flex: 1;
+        margin-right: 20px;
+    }
+
+    // /deep/.el-dialog__header{
+    //     padding-bottom: 0;
+    // }
+    
+</style>
+
+
+<style lang="stylus">
+   .lemon-container__title
+     padding-bottom:10px;
+   .slot-group
+      width:200px;
+      .slot-group-title
+        padding:0 0 10px 0;
+        .slot-group-notice
+           padding: 10px 10px;
+     
+      .slot-search
+           width:calc(100% - 20px) ;
+           margin:5px 10px;
+           padding:3px 0px;
+
+      .slot-group-member
+          display: flex;
+          padding: 5px 0;
+          flex:1;
+          font-size: 14px;
+          align-items: center;
+          .avatar
+              width: 30px;
+              height: 30px;
+              line-height: 30px;
+              margin-right:5px;
+              img 
+                vertical-align: middle;
+                border-style: none;
+                width: 100%;
+                height: 100%;
+                line-height: 30px;
+                border-radius: 50%;
+</style>

+ 499 - 0
src/components/LemonUI/components/chatrecords/segrecords.vue

@@ -0,0 +1,499 @@
+<template>
+<div>
+
+    <div class="imui-center qq-lemon-imui">
+        <lemon-records  class="lemon-slot"  
+            :width="windowWidth"
+            :height="windowHeight"
+            :user="UserData"
+            ref="IMUI"  
+            :sendText="sendText"
+            :theme="theme"
+            :show-context="showContext"
+            :hide-menu="hideMenu"
+            :hide-menu-avatar="hideMenuAvatar"
+            :hide-message-name="hideMessageName"
+            :hide-message-time="hideMessageTime"
+            :messageTimeFormat="messageTimeFormat"
+            @change-menu="handleChangeMenu"
+            @pull-messages="handlePullMessages"
+            @pull-up-messages="handleMoreMessages"
+            @change-contact="handleChangeContact"
+            @change-conversation="handleChangeConversation"
+            @message-click="handleMessageClick"
+            @context-click="handleContextClick"
+            @menu-avatar-click="handleMenuAvatarClick"
+            >
+
+            <template #cover>
+                    <div class="cover">
+                        <!-- <i class="lemon-icon-message"></i>
+                        <p>
+                            <b>自定义封面 Lemon</b>IMUI
+                        </p> -->
+                    </div>
+            </template>
+
+            <template #message-title="contact">
+                <div>
+                    <div style="display:flex;justify-content:space-between">
+                        <span>{{contact.displayName}}</span>
+                        <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')">
+                            居中
+                        </span> -->
+                        </span>
+                    </div>
+                    <!-- <div v-if="contact.isGroup" class="slot-group-menu">
+                        <span>聊天</span>
+                        <span>公告</span>
+                        <span>相册</span>
+                        <span>文件</span>
+                        <span>活动</span>
+                        <span>设置(左键弹出菜单)</span> 
+                    </div> -->
+                </div>
+            </template>
+        </lemon-records>
+
+    </div>
+
+    <el-dialog :visible.sync="dialogImgVisible" append-to-body width="35%">
+        <img style="width:100%;height:auto" :src="dialogImageUrl" alt="" />
+    </el-dialog>
+
+     <el-dialog :visible.sync="dialogVideoVisible" 
+                append-to-body 
+               :close-on-click-modal="false"
+               :destroy-on-close="true"
+               :width="aplayer.pWidth"
+               :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" 
+                :source="dialogVideoUrl" :cover="dialogVideoCover" ref="player">
+              </videoPlayer>
+       </el-dialog>
+</div>
+
+</template>
+
+<script>
+
+  import LemonMessageVoice from "@/components/LemonUI/components/message/voice";
+  import { getContactsByUser,getConversations,getMessageList} from '@/api/qw/im';
+  import Conversations from "@/components/LemonUI/database/conversations";
+  import EmojiData from "@/components/LemonUI/database/emoji";
+  import '@/components/LemonUI/index.css';
+  import VideoPlayer from '@/components/VideoPlayer/VueAliplayer'
+  let pages = {};
+  export default {
+       name: "SegRecords",
+       components: {   
+          VideoPlayer,
+       },
+       props: {
+          qwUser: {
+                type: Object,
+                default: () => {
+                    return { };
+                },
+          },
+          conversationId: {
+                type: String,
+                default: "default",
+          },
+          conversationData: {
+                type: Array,
+                default: () => {
+                    return [];
+                },
+          },
+          msgType: {
+                type: Number,
+                default: 0,
+          },
+          isContext: {
+                type: Boolean,
+                default: false,
+          },
+
+      },
+       data(){
+           return {
+                theme: "default",
+                IMUI:null,
+                hideMenuAvatar: false,
+                hideMenu: false,
+                hideMessageName: false,
+                hideMessageTime: true,
+                showContext:false,
+                showQW:false,
+                messageId:null,
+                UserData: {
+                    id: "1000",
+                    displayName: "June",
+                    avatar: "https://p.qqan.com/up/2018-4/15244505348390471.jpg",
+                },
+                dialogImgVisible:false,
+                dialogImageUrl: '',
+                dialogVideoVisible:false, 
+                dialogVideoUrl:'',
+                dialogVideoCover:'',
+                imageArr:[],
+                pickUploadImgData:null,
+                aplayer: {
+                    vid: "bf9b7e4a36d84aea8cee769765fbc28b",
+                    pWidth:"1040px",
+                    width:"1000px",
+                    height:"900px",
+                    videoWidth:"1000px",
+                    videoHeight:"900px"
+
+                },
+                player: null,
+                roomMembers:[],
+                roomAdmins:[],
+                roomInfo:null,
+                windowWidth: document.documentElement.clientWidth*0.95,  //实时屏幕宽度
+                windowHeight: document.documentElement.clientHeight*0.7,   //实时屏幕高度
+                queryParams: {
+                    pageNum: 1,
+                    pageSize: 10,
+                    conversationId: null,
+                },
+                isLoaded:false,
+                sendText:"",
+               
+           }
+       },
+       created(){
+            
+       },
+       mounted(){
+            this.deviceId=this.qwUser.deviceId;
+            this.UserData.id=this.qwUser.userId;
+            this.UserData.displayName=this.qwUser.nickName;
+            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); 
+                },
+                open(converChanged){
+                      if(this.isLoaded && !converChanged){
+                          return;
+                      }
+                      pages=[];
+                      setTimeout(() => {
+                          const IMUI = this.$refs.IMUI;
+                          this.init();
+                          this.isLoaded=true;
+                    }, 0);
+                },
+                //消息关键字搜索
+                openSearch(searchVal){
+                      pages=[];
+                      this.queryParams.searchVal=searchVal;
+                      this.showContext=true;
+                      setTimeout(() => {
+                          const IMUI = this.$refs.IMUI;
+                          this.init();
+                    }, 100);
+                },
+                //查看上下文
+                openContextBox(message){ 
+                      pages=[];
+                      this.queryParams.messageId=message.id;
+                      this.queryParams.pullAct=0;//下拉刷新
+                      setTimeout(() => {
+                          const IMUI = this.$refs.IMUI;
+                          this.init();
+                    }, 100);
+                },
+                getConversation(){
+                      const IMUI = this.$refs.IMUI;
+                      if(this.conversationData.length==0){
+                          getConversations(this.qwUser.userId).then(response => {
+                                this.conversationData = response.data;
+                                IMUI.initConversations(this.conversationData);
+                                IMUI.clearMessages(this.conversationId);
+                                IMUI.currentContactId="";
+                                IMUI.changeContact(this.conversationId);
+                          });
+                       }else{
+                                IMUI.initConversations(this.conversationData);
+                                IMUI.clearMessages(this.conversationId);
+                                IMUI.currentContactId="";
+                                IMUI.changeContact(this.conversationId);
+                        }
+                },
+                messageTimeFormat(time) {
+                   return this.friendlyDate(time);
+                },
+                changeTheme() {
+                  this.theme = this.theme == "default" ? "blue" : "default";
+                },
+                scrollToTop() {
+                  document.body.scrollIntoView();
+                },
+                handlePullMessages(contact, next,instance) {
+                    const { IMUI } = this.$refs;
+                    let isEnd = false;
+                    if(this.isContext){
+                         this.queryParams.pullAct=0;//下拉刷新
+                    }
+                    console.log("qxj handlePullMessages:"+JSON.stringify(this.queryParams));
+                    getMessageList(this.queryParams).then(response => {
+                        if(response.code==200){
+                            isEnd=response.data.isLastPage;
+                            next(response.data.list, isEnd);
+                            if(!isEnd){
+                                if(this.isContext){
+                                    pages[contact.conversationId+this.queryParams.pullAct]++;
+                                    this.queryParams.pageNum=pages[contact.conversationId+this.queryParams.pullAct];
+                                }else{
+                                    pages[contact.conversationId]++;
+                                    this.queryParams.pageNum=pages[contact.conversationId];
+                                }
+                            }
+                        }
+                    });
+                },
+                handleMoreMessages(contact, next,instance) {
+                    const { IMUI } = this.$refs;
+                    let isEnd = false;
+                    console.log("qxj handleMoreMessages:"+JSON.stringify(this.queryParams));
+                     if(this.isContext){
+                         this.queryParams.pullAct=1;//上拉加载更多
+                    }
+                    getMessageList(this.queryParams).then(response => {
+                        if(response.code==200){
+                            isEnd=response.data.isLastPage;
+                            next(response.data.list, isEnd);
+                            if(this.isContext){
+                                pages[conversation.conversationId+this.queryParams.pullAct]++;
+                            }else{
+                                pages[contact.conversationId]++;
+                            }
+                        }
+                    });
+                },
+
+                handleChangeConversation(conversation, instance) {
+                    console.log("qxj ChangeConversation:"+JSON.stringify(conversation));
+                    if(this.isContext){
+                        if (!pages[conversation.conversationId+this.queryParams.pullAct]){
+                              pages[conversation.conversationId+this.queryParams.pullAct] =1;
+                        }
+                        this.queryParams.pageNum=pages[conversation.conversationId+this.queryParams.pullAct];
+                    }
+                    else{
+                        if (!pages[conversation.conversationId]){
+                              pages[conversation.conversationId] =1;
+                        }
+                        this.queryParams.pageNum=pages[conversation.conversationId];
+                    }
+                    this.queryParams.conversationId=conversation.conversationId;
+                    if(conversation.unread>0){
+                        conversation.unread=0;
+                        instance.updateContact(conversation);
+                    }
+                    instance.closeDrawer();
+                },
+                handleChangeContact(contact, instance) {
+
+                },
+            
+                handleMenuAvatarClick() {
+                   console.log("Event:menu-avatar-click");
+                },
+                tooglePlayVideo(data){
+                    this.dialogVideoVisible=true;
+                    this.dialogVideoUrl=data.content;
+                    this.dialogVideoCover=data.url;
+                    const player = this.$refs.player.instance 
+                    player && player.play()
+                },
+                handleMessageClick(e, key, message, instance) {
+                    console.log("点击了消息", e, key, message);
+                    //console.log("qxj message:"+JSON.stringify(message));
+                    if(message.type=="image"){
+                        var url=!!message.url?message.url:message.content;
+                        this.handlePicturePreview(url);
+                    }
+                    else if(message.type=="video"){  
+                        this.tooglePlayVideo(message);
+                    }
+                    else if(message.type=="file"){  
+                        
+                    }
+                    if (key == "status") {
+                        instance.updateMessage({
+                        id: message.id,
+                        status: "going",
+                        content: "正在重新发送消息...",
+                        });
+                        setTimeout(() => {
+                        instance.updateMessage({
+                            id: message.id,
+                            status: "succeed",
+                            content: "发送成功",
+                        });
+                        }, 2000);
+                    }
+                },
+                handleContextClick(e, key, message, instance){
+                    this.$emit("showContextBox",message);
+                },
+                changeMenuAvatarVisible() {
+                   this.hideMenuAvatar = !this.hideMenuAvatar;
+                },
+                changeMenuVisible() {
+                   this.hideMenu = !this.hideMenu;
+                },
+                changeMessageNameVisible() {
+                   this.hideMessageName = !this.hideMessageName;
+                },
+                changeMessageTimeVisible() {
+                   this.hideMessageTime = !this.hideMessageTime;
+                },
+    
+                updateContact() {
+                    this.$refs.IMUI.updateContact({
+                        id: "contact-3",
+                        unread: 10,
+                        displayName: generateRandWord(),
+                        lastSendTime: getTime(),
+                        lastContent: "修改昵称为随机字母",
+                     });
+                },
+                handleChangeMenu() {
+                     console.log("Event:change-menu");
+                },
+                handlePicturePreview(url) {
+                    this.dialogImageUrl = url;
+                    this.dialogImgVisible=true;
+                },
+       }
+  }
+
+</script>
+
+
+
+<style lang="scss" scoped>
+    .app-container{
+       padding-top:0;
+    }
+    .imui-center{
+        margin-bottom:"60px"; 
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        // height: 80vh;
+    }
+    .lemon-wrapper{
+        border:"1px solid #ddd";
+        height: "60%" !important;
+    }
+    .lemon-drawer{
+        border:"1px solid #ddd";
+        border-left:"0";
+    }
+    .more {
+        font-size: 12px;
+        line-height: 24px;
+        height: 24px;
+        position: absolute;
+        top: 14px;
+        right: 14px;
+        cursor: pointer;
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+        color: #f1f1f1;
+        display: inline-block;
+        border-radius: 4px;
+        background: #111;
+        padding: 0 8px;
+    }
+
+    /deep/.el-dialog__headerbtn{
+         top:10px;
+    }
+
+    /deep/.el-form--inline .el-form-item{
+        display: flex;
+    }
+    /deep/.el-form-item--medium .el-form-item__content{
+        flex: 1;
+        margin-right: 20px;
+    }
+
+    /deep/.el-dialog:not(.is-fullscreen) {
+        margin-top: 0 !important;
+    }
+
+    // /deep/.el-dialog__header{
+    //     padding-bottom: 0;
+    // }
+    
+</style>
+
+
+<style lang="stylus">
+   .lemon-container__title
+     padding-bottom:10px;
+   .slot-group
+      width:200px;
+      .slot-group-title
+        padding:0 0 10px 0;
+        .slot-group-notice
+           padding: 10px 10px;
+     
+      .slot-search
+           width:calc(100% - 20px) ;
+           margin:5px 10px;
+           padding:3px 0px;
+
+      .slot-group-member
+          display: flex;
+          padding: 5px 0;
+          flex:1;
+          font-size: 14px;
+          align-items: center;
+          .avatar
+              width: 30px;
+              height: 30px;
+              line-height: 30px;
+              margin-right:5px;
+              img 
+                vertical-align: middle;
+                border-style: none;
+                width: 100%;
+                height: 100%;
+                line-height: 30px;
+                border-radius: 50%;
+</style>

+ 145 - 0
src/components/LemonUI/components/contact.vue

@@ -0,0 +1,145 @@
+<script>
+import { isString, isToday } from "../utils/validate";
+import { timeFormat, useScopedSlot } from "../utils";
+export default {
+  name: "LemonContact",
+  components: {},
+  inject: {
+    IMUI: {
+      from: "IMUI",
+      default() {
+        return this;
+      },
+    },
+  },
+  data() {
+    return {};
+  },
+  props: {
+    contact: Object,
+    simple: Boolean,
+    timeFormat: {
+      type: Function,
+      default(val) {
+        return timeFormat(val, isToday(val) ? "h:i" : "y/m/d");
+      },
+    },
+  },
+  render() {
+    return (
+      <div
+        class={["lemon-contact", { "lemon-contact--name-center": this.simple }]}
+        title={this.contact.displayName}
+        on-click={e => this._handleClick(e, this.contact)}
+      >
+        {useScopedSlot(
+          this.$scopedSlots.default,
+          this._renderInner(),
+          this.contact,
+        )}
+      </div>
+    );
+  },
+  created() {},
+  mounted() {},
+  computed: {},
+  watch: {},
+  methods: {
+    _renderInner() {
+      const { contact } = this;
+      return [
+        <lemon-badge
+          count={!this.simple ? contact.unread : 0}
+          class="lemon-contact__avatar"
+        >
+          <lemon-avatar size={40} src={contact.avatar} />
+        </lemon-badge>,
+        <div class="lemon-contact__inner">
+          <p class="lemon-contact__label">
+            <span class="lemon-contact__name">{contact.displayName}</span>
+            {!this.simple && (
+              <span class="lemon-contact__time">
+                {this.timeFormat(contact.lastSendTime)}
+              </span>
+            )}
+          </p>
+          {!this.simple && (
+            <p class="lemon-contact__content">
+              {isString(contact.lastContent) ? (
+                <span domProps={{ innerHTML: contact.lastContent }} />
+              ) : (
+                contact.lastContent
+              )}
+            </p>
+          )}
+        </div>,
+      ];
+    },
+    _handleClick(e, data) {
+      this.$emit("click", data);
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-contact)
+  padding 10px 14px
+  cursor pointer
+  user-select none
+  box-sizing border-box
+  overflow hidden
+  background #efefef
+  text-align left
+  p
+    margin 0
+  +m(active)
+    background #bebdbd
+  &:hover:not(.lemon-contact--active)
+    background #e3e3e3
+    .el-badge__content
+      border-color #ddd
+  +e(avatar)
+    float left
+    margin-right 10px
+    img
+      display block
+    .ant-badge-count
+      display inline-block
+      padding 0 4px
+      height 18px
+      line-height 18px
+      min-width 18px
+      top -4px
+      right 7px
+  +e(label)
+    display flex
+  +e(time)
+    font-size 12px
+    line-height 18px
+    padding-left 6px
+    color #999
+    white-space nowrap
+  +e(name)
+    display block
+    width 100%
+    ellipsis()
+  +e(content)
+    font-size 12px
+    color #999
+    height 18px
+    line-height 18px
+    margin-top 1px !important
+    ellipsis()
+    img
+      height 14px
+      display inline-block
+      vertical-align middle
+      margin 0 1px
+      position relative
+      top -1px
+  +m(name-center)
+    +e(label)
+      padding-bottom 0
+      line-height 38px
+</style>

+ 505 - 0
src/components/LemonUI/components/editor.vue

@@ -0,0 +1,505 @@
+<script>
+import { useScopedSlot, messageToHtml, clearHtmlExcludeImg } from "../utils";
+const command = (command, val) => {
+  document.execCommand(command, false, val);
+};
+const selection = window.getSelection();
+let range;
+let emojiData = [];
+let isInitTool = false;
+export default {
+  name: "LemonEditor",
+  inject: {
+    IMUI: {
+      from: "IMUI",
+      default() {
+        return this;
+      },
+    },
+  },
+  components: {},
+  props: {
+    tools: {
+      type: Array,
+      default: () => [],
+    },
+    sendText: {
+      type: String,
+      default: "发 送",
+    },
+    wrapKey: {
+      type: Function,
+      default: function(e) {
+        return e.keyCode == 13 && e.ctrlKey == false && e.shiftKey == false;
+      },
+    },
+    sendKey: {
+      type: Function,
+      default(e) {
+        return e.keyCode == 13 && e.ctrlKey === true;
+      },
+    },
+  },
+  data() {
+    this.clipboardBlob = null;
+    return {
+      //剪切板图片URL
+      clipboardUrl: "",
+      submitDisabled: true,
+      //proxyTools: [],
+      accept: "",
+    };
+  },
+  created() {
+    this.IMUI.$on("change-contact", () => {
+      this.closeClipboardImage();
+    });
+  },
+  render() {
+    const toolLeft = [];
+    const toolRight = [];
+    this.proxyTools.forEach(({ name, title, render, click, isRight }) => {
+      click = click || new Function();
+      const classes = [
+        "lemon-editor__tool-item",
+        { "lemon-editor__tool-item--right": isRight },
+      ];
+      let node;
+      if (name == "emoji") {
+        node =
+          emojiData.length == 0 ? (
+            ""
+          ) : (
+            <lemon-popover class="lemon-editor__emoji">
+              <template slot="content">{this._renderEmojiTabs()}</template>
+              <div class={classes} title={title}>
+                {render()}
+              </div>
+            </lemon-popover>
+          );
+      } else {
+        node = (
+          <div class={classes} on-click={click} title={title}>
+            {render()}
+          </div>
+        );
+      }
+      if (isRight) {
+        toolRight.push(node);
+      } else {
+        toolLeft.push(node);
+      }
+    });
+
+    return (
+      <div class="lemon-editor">
+        {this.clipboardUrl && (
+          <div class="lemon-editor__clipboard-image">
+            <img src={this.clipboardUrl} />
+            <div>
+              <lemon-button
+                style={{ marginRight: "10px" }}
+                on-click={this.closeClipboardImage}
+                color="grey"
+              >
+                取消
+              </lemon-button>
+              <lemon-button on-click={this.sendClipboardImage}>
+                发送图片
+              </lemon-button>
+            </div>
+          </div>
+        )}
+        <input
+          style="display:none"
+          type="file"
+          multiple="multiple"
+          ref="fileInput"
+          accept={this.accept}
+          onChange={this._handleChangeFile}
+        />
+        <div class="lemon-editor__tool">
+          <div class="lemon-editor__tool-left">{toolLeft}</div>
+          <div class="lemon-editor__tool-right">{toolRight}</div>
+        </div>
+        <div class="lemon-editor__inner">
+          <div
+            class="lemon-editor__input"
+            ref="textarea"
+            contenteditable="true"
+            on-keyup={this._handleKeyup}
+            on-keydown={this._handleKeydown}
+            on-paste={this._handlePaste}
+            on-click={this._handleClick}
+            spellcheck="false"
+          />
+        </div>
+        <div class="lemon-editor__footer">
+          <div class="lemon-editor__tip">
+            {useScopedSlot(
+              this.IMUI.$scopedSlots["editor-footer"],
+              "使用 ctrl + enter 快捷发送消息",
+            )}
+          </div>
+          <div class="lemon-editor__submit">
+            <lemon-button
+              disabled={this.submitDisabled}
+              on-click={this._handleSend}
+            >
+              {this.sendText}
+            </lemon-button>
+          </div>
+        </div>
+      </div>
+    );
+  },
+  computed: {
+    proxyTools() {
+      if (!this.tools) return [];
+      const defaultTools = [
+        {
+          name: "emoji",
+          title: "表情",
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-emoji" />;
+          },
+        },
+        {
+          name: "uploadFile",
+          title: "文件上传",
+          click: () => this.selectFile("*"),
+          render: menu => {
+            return <i class="lemon-icon-folder" />;
+          },
+        },
+        {
+          name: "uploadImage",
+          title: "图片上传",
+          click: () => this.selectFile("image/*"),
+          render: menu => {
+            return <i class="lemon-icon-image" />;
+          },
+        },
+        {
+          name: "msgRecords",
+          title: "聊天消息",
+          click: () => this.pickMsgBox(),
+          render: menu => {
+            return <i class="lemon-icon-message" />;
+          },
+        },
+      ];
+      let tools = [];
+      if (Array.isArray(this.tools)) {
+        const indexMap = {
+          emoji: 0,
+          uploadFile: 1,
+          uploadImage: 2,
+          msgRecords: 3,
+        };
+        const indexKeys = Object.keys(indexMap);
+        tools = this.tools.map(item => {
+          if (indexKeys.includes(item.name)) {
+            return {
+              ...defaultTools[indexMap[item.name]],
+              ...item,
+            };
+          }
+          return item;
+        });
+      } else {
+        tools = defaultTools;
+      }
+      return tools;
+    },
+  },
+  methods: {
+    closeClipboardImage() {
+      this.clipboardUrl = "";
+      this.clipboardBlob = null;
+    },
+    sendClipboardImage() {
+      if (!this.clipboardBlob) return;
+      this.$emit("upload", this.clipboardBlob);
+      this.closeClipboardImage();
+    },
+    saveRangeToLast() {
+      if (!range) {
+        range = document.createRange();
+      }
+      range.selectNodeContents(textarea.value);
+      range.collapse(false);
+      selection.removeAllRanges();
+      selection.addRange(range);
+    },
+    inertContent(val, toLast = false) {
+      if (toLast) saveRangeToLast();
+      this.focusRange();
+      command("insertHTML", val);
+      this.saveRange();
+    },
+    saveRange() {
+      range = selection.getRangeAt(0);
+    },
+    focusRange() {
+      this.$refs.textarea.focus();
+      if (range) {
+        selection.removeAllRanges();
+        selection.addRange(range);
+      }
+    },
+    _handleClick() {
+      this.saveRange();
+    },
+    _renderEmojiTabs() {
+      const renderImageGrid = items => {
+        return items.map(item => (
+          <img
+            src={item.src}
+            title={item.title}
+            class="lemon-editor__emoji-item"
+            on-click={() => this._handleSelectEmoji(item)}
+          />
+        ));
+      };
+      if (emojiData[0].label) {
+        const nodes = emojiData.map((item, index) => {
+          return (
+            <div slot="tab-pane" index={index} tab={item.label}>
+              {renderImageGrid(item.children)}
+            </div>
+          );
+        });
+        return <lemon-tabs style="width: 412px">{nodes}</lemon-tabs>;
+      } else {
+        return (
+          <div class="lemon-tabs-content" style="width:406px">
+            {renderImageGrid(emojiData)}
+          </div>
+        );
+      }
+    },
+    _handleSelectEmoji(item) {
+      this.inertContent(
+        `<img emoji-name="${item.name}" src="${item.src}"></img>`,
+      );
+      this._checkSubmitDisabled();
+    },
+    async selectFile(accept) {
+        if(accept=="*"){
+            this.accept = accept;
+            await this.$nextTick();
+            this.$refs.fileInput.click();
+        }else{
+            this.$emit("pickImg", this.clipboardBlob);
+        }
+    },
+     async pickMsgBox() {
+         this.$emit("pickMsgRecords", this.clipboardBlob);
+    },
+
+    _handlePaste(e) {
+      e.preventDefault();
+      const clipboardData = e.clipboardData || window.clipboardData;
+      const text = clipboardData.getData("Text");
+      if (text) {
+        this.submitDisabled = false;
+        if (window.clipboardData) {
+          this.$refs.textarea.innerHTML = text;
+        } else {
+          command("insertText", text);
+        }
+      } else {
+        const { blob, blobUrl } = this._getClipboardBlob(clipboardData);
+        this.clipboardBlob = blob;
+        this.clipboardUrl = blobUrl;
+      }
+    },
+    _getClipboardBlob(clipboard) {
+      let blob, blobUrl;
+      for (var i = 0; i < clipboard.items.length; ++i) {
+        if (
+          clipboard.items[i].kind == "file" &&
+          clipboard.items[i].type.indexOf("image/") !== -1
+        ) {
+          blob = clipboard.items[i].getAsFile();
+          blobUrl = (window.URL || window.webkitURL).createObjectURL(blob);
+        }
+      }
+      return { blob, blobUrl };
+    },
+    _handleKeyup(e) {
+      this.saveRange();
+      this._checkSubmitDisabled();
+    },
+    _handleKeydown(e) {
+      if (e.keyCode == 13 || (e.keyCode == 13 && e.shiftKey)) {
+        e.preventDefault();
+      }
+      if (this.wrapKey(e)) {
+        e.preventDefault();
+        command("insertLineBreak");
+      }
+      if (this.submitDisabled == false && this.sendKey(e)) {
+        this._handleSend();
+      }
+    },
+    getFormatValue() {
+      // return toEmojiName(
+      //   this.$refs.textarea.innerHTML
+      //     .replace(/<br>|<\/br>/, "")
+      //     .replace(/<div>|<p>/g, "\r\n")
+      //     .replace(/<\/div>|<\/p>/g, "")
+      // );
+      return this.IMUI.emojiImageToName(this.$refs.textarea.innerHTML);
+    },
+    _checkSubmitDisabled() {
+      this.submitDisabled = !clearHtmlExcludeImg(
+        this.$refs.textarea.innerHTML.trim(),
+      );
+    },
+    _handleSend(e) {
+      const text = this.getFormatValue();
+      this.$emit("send", text);
+      this.clear();
+      this._checkSubmitDisabled();
+    },
+    _handleChangeFile(e) {
+      const { fileInput } = this.$refs;
+      Array.from(fileInput.files).forEach(file => {
+        this.$emit("upload", file);
+      });
+      fileInput.value = "";
+    },
+    clear() {
+      this.$refs.textarea.innerHTML = "";
+    },
+    initEmoji(data) {
+      emojiData = data;
+      this.$forceUpdate();
+    },
+    setValue(val) {
+      this.$refs.textarea.innerHTML = this.IMUI.emojiNameToImage(val);
+      this._checkSubmitDisabled();
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
+gap = 10px;
++b(lemon-editor)
+  height 200px
+  position relative
+  flex-column()
+  +e(tool)
+    display flex
+    height 40px
+    align-items center
+    justify-content space-between
+    padding 0 5px
+    box-shadow 0px -1px 0px rgba(171,169,169,0.3)
+  +e(tool-left){
+    display flex
+  }
+  +e(tool-right){
+    display flex
+  }
+  +e(tool-item)
+    cursor pointer
+    padding 4px gap
+    height 28px
+    line-height 24px;
+    color #999
+    transition all ease .3s
+    font-size 12px
+    [class^='lemon-icon-']
+      line-height 26px
+      font-size 22px
+    &:hover
+      color #333
+    +m(right){
+      margin-left:auto;
+    }
+  +e(inner)
+    flex 1
+    overflow-x hidden
+    overflow-y auto
+    scrollbar-light()
+  +e(clipboard-image)
+    position absolute
+    top 0
+    left 0
+    width 100%
+    height 100%
+    flex-column()
+    justify-content center
+    align-items center
+    background #f4f4f4
+    z-index 1
+    img
+      max-height 66%
+      max-width 80%
+      background #e9e9e9
+      //box-shadow 0 0 20px rgba(0,0,0,0.15)
+      user-select none
+      cursor pointer
+      border-radius 4px
+      margin-bottom 10px
+      border 3px dashed #ddd !important
+      box-sizing border-box
+    .clipboard-popover-title
+      font-size 14px
+      color #333
+  +e(input)
+    height 100%
+    box-sizing border-box
+    border none
+    outline none
+    padding 0 gap
+    scrollbar-light()
+    p,div
+      margin 0
+    img
+      height 20px
+      padding 0 2px
+      pointer-events none
+      position relative
+      top -1px
+      vertical-align middle
+  +e(footer)
+    display flex
+    height 52px
+    justify-content flex-end
+    padding 0 gap
+    align-items center
+  +e(tip)
+    margin-right 10px
+    font-size 12px
+    color #999
+    user-select none
+  +e(emoji)
+    user-select none
+    .lemon-popover
+      background #f6f6f6
+    .lemon-popover__content
+      padding 0
+    .lemon-popover__arrow
+      background #f6f6f6
+    .lemon-tabs-content
+      box-sizing border-box
+      padding 8px
+      height 200px
+      overflow-x hidden
+      overflow-y auto
+      scrollbar-light()
+      margin-bottom 8px
+  +e(emoji-item)
+    cursor pointer
+    width 22px
+    padding 4px
+    border-radius 4px
+    &:hover
+      background #e9e9e9
+</style>

+ 1384 - 0
src/components/LemonUI/components/index.vue

@@ -0,0 +1,1384 @@
+<script>
+import {
+  useScopedSlot,
+  funCall,
+  generateUUID,
+  clearHtmlExcludeImg,
+} from "../utils";
+import { isFunction, isString, isEmpty } from "../utils/validate";
+import contextmenu from "../directives/contextmenu";
+import {
+  DEFAULT_MENUS,
+  DEFAULT_MENU_LASTMESSAGES,
+  DEFAULT_MENU_CONTACTS,
+} from "../utils/constant";
+import lastContentRender from "../lastContentRender";
+
+import MemoryCache from "../utils/cache/memory";
+
+let allMessages = {};
+const emojiMap = {};
+const toPx = val => {
+  return isString(val) ? val : `${val}px`;
+};
+const toPoint = str => {
+  return str.replace("%", "") / 100;
+};
+
+let renderDrawerContent = () => {};
+
+export default {
+  name: "LemonImui",
+  provide() {
+    return {
+      IMUI: this,
+    };
+  },
+  props: {
+    width: {
+      type: [String, Number],
+      default: 850,
+    },
+    height: {
+      type: [String, Number],
+      default: 580,
+    },
+    theme: {
+      type: String,
+      default: "default",
+    },
+    simple: {
+      type: Boolean,
+      default: false,
+    },
+    loadingText: [String, Function],
+    loadendText: [String, Function],
+    /**
+     * 消息时间格式化规则
+     */
+    messageTimeFormat: Function,
+    /**
+     * 联系人最新消息时间格式化规则
+     */
+    contactTimeFormat: Function,
+    /**
+     * 初始化时是否隐藏抽屉
+     */
+    hideDrawer: {
+      type: Boolean,
+      default: true,
+    },
+    /**
+     * 是否隐藏导航按钮上的头像
+     */
+    hideMenuAvatar: Boolean,
+    hideMenu: Boolean,
+    /**
+     * 是否隐藏消息列表内的联系人名字
+     */
+    hideMessageName: Boolean,
+    /**
+     * 是否隐藏消息列表内的发送时间
+     */
+    hideMessageTime: Boolean,
+    sendKey: Function,
+    wrapKey: Function,
+    sendText: String,
+    contextmenu: Array,
+    contactContextmenu: Array,
+    avatarCricle: Boolean,
+    user: {
+      type: Object,
+      default: () => {
+        return {};
+      },
+    },
+  },
+  data() {
+    this.CacheContactContainer = new MemoryCache();
+    this.CacheMenuContainer = new MemoryCache();
+    this.CacheMessageLoaded = new MemoryCache();
+    this.CacheDraft = new MemoryCache();
+    return {
+      drawerVisible: !this.hideDrawer,
+      currentContactId: null,
+      currentNewContactId: null,
+      currentMessages: [],
+      activeSidebar: DEFAULT_MENU_LASTMESSAGES,
+      contacts: [],
+      conversations:[],
+      menus: [],
+      editorTools: [
+          { name: "msgRecords" }
+        // { name: "emoji" },
+        // { name: "uploadFile" },
+        // { name: "uploadImage" },
+      ],
+    };
+  },
+
+  render() {
+    return this._renderWrapper([
+      this._renderMenu(),
+      this._renderSidebarMessage(),//加载会话数据
+      this._renderSidebarContact(),//加载联系人
+      this._renderContainer(),
+      this._renderDrawer(),
+    ]);
+  },
+  created() {
+    this.initMenus();
+  },
+  async mounted() {
+    await this.$nextTick();
+  },
+  computed: {
+    currentContact() {
+       return this.conversations.find(item =>  item.conversationId == this.currentContactId) || {};
+    },
+    //获取当前联系人
+    currentNewContact() {
+       return this.contacts.find(item =>  item.id == this.currentNewContactId) || {};
+    },
+    currentMenu() {
+      return this.menus.find(item => item.name == this.activeSidebar) || {};
+    },
+    currentIsDefSidebar() {
+      return DEFAULT_MENUS.includes(this.activeSidebar);
+    },
+    lastMessages() {
+      // var data = this.contacts.filter(item => !isEmpty(item.lastContent));
+      // data.sort((a1, a2) => {
+      //   return a2.lastSendTime - a1.lastSendTime;
+      // });
+      var data=null;
+      if(!!this.conversations){
+          data=this.conversations;
+      }
+      return data;
+    },
+  },
+  watch: {
+    activeSidebar() {},
+  },
+  methods: {
+    _menuIsContacts() {
+      return this.activeSidebar == DEFAULT_MENU_CONTACTS;
+    },
+    _menuIsMessages() {
+      return this.activeSidebar == DEFAULT_MENU_LASTMESSAGES;
+    },
+    _createMessage(message) {
+      return {
+        ...{
+          id: generateUUID(),
+          type: "text",
+          status: "going",
+          sendTime: new Date().getTime(),
+          toContactId: this.currentContactId,
+          fromUser: {
+            ...this.user,
+          },
+        },
+        ...message,
+      };
+    },
+    /**
+     * 新增一条消息
+     */
+    appendMessage(message, scrollToBottom = false) {
+        let messageList = allMessages[message.toContactId];
+        // 如果是自己的消息需要push,发送的消息不再增加未读条数
+        let conversation=this.findConversation(message.toContactId);
+        let unread=conversation.unread!=undefined && conversation.unread>0?(conversation.unread+1):1;
+        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;
+          conversation.unread = unread;
+          conversation.lastSendTime=message.sendTime;
+          conversation.lastContent=this.lastContentRender(message);
+          this.updateContact(conversation);
+      } else {
+          // 如果消息存在则不再添加
+          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) {
+            conversation.unread=0;
+            if (scrollToBottom == true) {
+              this.messageViewToBottom();
+            }
+            this.CacheDraft.remove(message.toContactId);
+          } else {
+            conversation.unread = unread;
+          }
+          this.updateContact(conversation);
+      }
+    },
+    _emitSend(message, next, file) {
+      this.$emit(
+        "send",
+        message,
+        (replaceMessage = { status: "succeed" }) => {
+          next();
+          this.updateMessage(Object.assign(message, replaceMessage));
+        },
+        file,
+      );
+    },
+    _handleSend(text) {
+      const message = this._createMessage({ content: text });
+      this.appendMessage(message, true);
+      this._emitSend(message, () => {
+        this.updateContact({
+          id: message.toContactId,
+          lastContent: this.lastContentRender(message),
+          lastSendTime: message.sendTime,
+        });
+        this.CacheDraft.remove(message.toContactId);
+      });
+    },
+    _handleUpload(file) {
+      const imageTypes = ["image/gif", "image/jpeg", "image/png"];
+      let joinMessage;
+      if (imageTypes.includes(file.type)) {
+        joinMessage = {
+          type: "image",
+          content: URL.createObjectURL(file),
+        };
+      } else {
+        joinMessage = {
+          type: "file",
+          fileSize: file.size,
+          fileName: file.name,
+          content: "",
+        };
+      }
+      const message = this._createMessage(joinMessage);
+      this.appendMessage(message, true);
+      this._emitSend(
+        message,
+        () => {
+          this.updateContact({
+            id: message.toContactId,
+            lastContent: this.lastContentRender(message),
+            lastSendTime: message.sendTime,
+          });
+        },
+        file,
+      );
+    },
+    _handleRemoteImage(url) {
+        let joinMessage = {
+            type: "image",
+            content: url,
+          };
+        const message = this._createMessage(joinMessage);
+        this.appendMessage(message, true);
+        this._emitSend(
+          message,
+          () => {
+            this.updateContact({
+              id: message.toContactId,
+              lastContent: this.lastContentRender(message),
+              lastSendTime: message.sendTime,
+            });
+          },
+          null,
+        );
+    },
+    _pickImg(){
+        this.$emit("pick-image");
+    },
+    _pickMsgRecords(){
+        this.$emit("pick-msg-records");
+    },
+    _emitPullMessages(next) {
+      //console.log("qxj currentContact:"+JSON.stringify(this.currentContact));
+      this._changeContactLock = true;
+      this.$emit("pull-messages",
+      this.currentContact,
+        (messages = [], isEnd = false) => {
+          this._addMessage(messages, this.currentContactId, 0);
+          this.CacheMessageLoaded.set(this.currentContactId, isEnd);
+          if (isEnd == true) this.$refs.messages.loaded();
+          this.updateCurrentMessages();
+          this._changeContactLock = false;
+          next(isEnd);
+        },
+        this,
+      );
+    },
+    clearCacheContainer(name) {
+      this.CacheContactContainer.remove(name);
+      this.CacheMenuContainer.remove(name);
+    },
+    _renderWrapper(children) {
+      return (
+        <div
+          style={{
+            width: toPx(this.width),
+            height: toPx(this.height),
+          }}
+          ref="wrapper"
+          class={[
+            "lemon-wrapper",
+            `lemon-wrapper--theme-${this.theme}`,
+            { "lemon-wrapper--simple": this.simple },
+            this.drawerVisible && "lemon-wrapper--drawer-show",
+          ]}
+        >
+          {children}
+        </div>
+      );
+    },
+    _renderMenu() {
+      const menuItem = this._renderMenuItem();
+      return (
+        <div class="lemon-menu" v-show={!this.hideMenu}>
+          {
+            <lemon-avatar
+              v-show={!this.hideMenuAvatar}
+              on-click={e => {
+                this.$emit("menu-avatar-click", e);
+              }}
+              class="lemon-menu__avatar"
+              src={this.user.avatar}
+            />
+          }
+          {menuItem.top}
+          {this.$slots.menu}
+          <div class="lemon-menu__bottom">
+            {this.$slots["menu-bottom"]}
+            {menuItem.bottom}
+          </div>
+        </div>
+      );
+    },
+    _renderMenuAvatar() {
+      return;
+    },
+    _renderMenuItem() {
+      const top = [];
+      const bottom = [];
+      this.menus.forEach(item => {
+        const { name, title, unread, render, click } = item;
+        const node = (
+          <div
+            class={[
+              "lemon-menu__item",
+              { "lemon-menu__item--active": this.activeSidebar == name },
+            ]}
+            on-click={() => {
+              funCall(click, () => {
+                if (name) this.changeMenu(name);
+              });
+            }}
+            title={title}
+          >
+            <lemon-badge count={unread}>{render(item)}</lemon-badge>
+          </div>
+        );
+        item.isBottom === true ? bottom.push(node) : top.push(node);
+      });
+      return {
+        top,
+        bottom,
+      };
+    },
+    _renderSidebarMessage() {
+      return this._renderSidebar(
+        [
+          useScopedSlot(this.$scopedSlots["sidebar-message-top"], null, this),
+          this.lastMessages.map(contact => {
+            return this._renderContact(
+              {
+                contact,
+                timeFormat: this.contactTimeFormat,
+              },
+              () => this.changeContact(contact.conversationId),
+              this.$scopedSlots["sidebar-message"],
+            );
+          }),
+        ],
+        DEFAULT_MENU_LASTMESSAGES,
+        useScopedSlot(
+          this.$scopedSlots["sidebar-message-fixedtop"],
+          null,
+          this,
+        ),
+      );
+    },
+    _renderContact(props, onClick, slot) {
+      const {
+        click: customClick,
+        renderContainer,
+        id: contactId,
+      } = props.contact;
+      const click = () => {
+        funCall(customClick, () => {
+          onClick();
+          this._customContainerReady(
+            renderContainer,
+            this.CacheContactContainer,
+            contactId,
+          );
+        });
+      };
+
+      return (
+        <lemon-contact
+          class={{
+            "lemon-contact--active":this._menuIsMessages()?this.currentContactId == props.contact.conversationId:(this._menuIsContacts()?this.currentNewContactId == props.contact.id:false)
+          }}
+          v-lemon-contextmenu_contact={this.contactContextmenu}
+          props={props}
+          on-click={click}
+          scopedSlots={{ default: slot }}
+        />
+      );
+    },
+    _renderSidebarContact() {
+      let prevIndex;
+      return this._renderSidebar(
+        [
+          useScopedSlot(this.$scopedSlots["sidebar-contact-top"], null, this),
+          this.contacts.map(contact => {
+            if (!contact.index) return;
+            contact.index = contact.index.replace(/\[[0-9]*\]/, "");
+            const node = [
+              contact.index !== prevIndex && (
+                <p class="lemon-sidebar__label">{contact.index}</p>
+              ),
+              this._renderContact(
+                {
+                  contact: contact,
+                  simple: true,
+                },
+                () => {
+                  console.log("qxj contact click");
+                  this.currentNewContactId=contact.id;
+                  //this.changeContact(contact.id);
+                },
+                this.$scopedSlots["sidebar-contact"],
+              ),
+            ];
+            prevIndex = contact.index;
+            return node;
+          }),
+        ],
+        DEFAULT_MENU_CONTACTS,
+        useScopedSlot(
+          this.$scopedSlots["sidebar-contact-fixedtop"],
+          null,
+          this,
+        ),
+      );
+    },
+    _renderSidebar(children, name, fixedtop) {
+      return (
+        <div
+          class="lemon-sidebar"
+          v-show={this.activeSidebar == name}
+          on-scroll={this._handleSidebarScroll}
+        >
+          <div class="lemon-sidebar__fixed-top">{fixedtop}</div>
+          <div class="lemon-sidebar__scroll">{children}</div>
+        </div>
+      );
+    },
+    _renderDrawer() {
+      return this._menuIsMessages() && this.currentContactId ? (
+        <div class="lemon-drawer" ref="drawer">
+          {renderDrawerContent(this.currentContact)}
+          {useScopedSlot(this.$scopedSlots.drawer, "", this.currentContact)}
+        </div>
+      ) : (
+        ""
+      );
+    },
+    _isContactContainerCache(name) {
+      return name.startsWith("contact#");
+    },
+    _renderContainer() {
+      const nodes = [];
+      const cls = "lemon-container";
+      const curact = this.currentContact;
+      const curactNew = this.currentNewContact;
+      let defIsShow = true;
+      for (const name in this.CacheContactContainer.get()) {
+        const show = curactNew.id == name && this.currentIsDefSidebar;
+        if(show)defIsShow = !show;
+        nodes.push(
+          <div class={cls} v-show={show}>
+            {this.CacheContactContainer.get(name)}
+          </div>,
+        );
+      }
+      for (const name in this.CacheMenuContainer.get()) {
+          nodes.push(
+            <div
+              class={cls}
+              v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
+            >
+              {this.CacheMenuContainer.get(name)}
+            </div>,
+          );
+      }
+      
+      //聊天消息View
+      nodes.push(
+        <div
+          class={cls}
+          v-show={this._menuIsMessages() && defIsShow && curact.id}
+        >
+          <div class="lemon-container__title">
+            {useScopedSlot(
+              this.$scopedSlots["message-title"],
+              <div class="lemon-container__displayname">
+                {curact.displayName}
+              </div>,
+              curact,
+            )}
+          </div>
+          <div class="lemon-vessel">
+            <div class="lemon-vessel__left">
+              <lemon-messages
+                ref="messages"
+                loading-text={this.loadingText}
+                loadend-text={this.loadendText}
+                hide-time={this.hideMessageTime}
+                hide-name={this.hideMessageName}
+                time-format={this.messageTimeFormat}
+                reverse-user-id={this.user.id}
+                on-reach-top={this._emitPullMessages}
+                messages={this.currentMessages}
+              />
+              <lemon-editor
+                ref="editor"
+                tools={this.editorTools}
+                sendText={this.sendText}
+                sendKey={this.sendKey}
+                wrapKey={this.wrapKey}
+                onSend={this._handleSend}
+                onUpload={this._handleUpload}
+                onPickImg={this._pickImg}
+                onPickMsgRecords={this._pickMsgRecords}
+              />
+            </div>
+            <div class="lemon-vessel__right">
+              {useScopedSlot(this.$scopedSlots["message-side"], null, curact)}
+            </div>
+          </div>
+        </div>,
+      );
+      nodes.push(
+        <div class={cls} v-show={!curact.id && this.currentIsDefSidebar}>
+          {this.$slots.cover}
+        </div>,
+      );
+
+      //联系人View
+      nodes.push(
+        <div
+          class={cls}
+          v-show={this._menuIsContacts() && defIsShow && curactNew.id}>
+          {useScopedSlot(
+            this.$scopedSlots["contact-info"],
+            <div class="lemon-contact-info">
+              <lemon-avatar src={curactNew.avatar} size={90} />
+              <h4>{curactNew.displayName}</h4>
+              <lemon-button
+                on-click={() => {
+                  if (isEmpty(curactNew.lastContent)) {
+                    this.updateContact({
+                      id: curactNew.id,
+                      lastContent: " ",
+                    });
+                  }
+                  this.changeContact(curactNew.conversationId, DEFAULT_MENU_LASTMESSAGES);
+                }}
+              >
+                发送消息
+              </lemon-button>
+            </div>,
+            curactNew,
+          )}
+        </div>,
+      );
+      return nodes;
+    },
+    _handleSidebarScroll() {
+      contextmenu.hide();
+    },
+    _addContact(data, t) {
+      const type = {
+        0: "unshift",
+        1: "push",
+      }[t];
+      this.contacts[type](data);
+    },
+    _addMessage(data, contactId, t) {
+      const type = {
+        0: "unshift",
+        1: "push",
+      }[t];
+      if (!Array.isArray(data)) data = [data];
+      allMessages[contactId] = allMessages[contactId] || [];
+      allMessages[contactId][type](...data);
+      console.log("qxj _addMessage type:"+type);
+    },
+    /**
+     * 设置最新消息DOM
+     * @param {String} messageType 消息类型
+     * @param {Function} render 返回消息 vnode
+     */
+    setLastContentRender(messageType, render) {
+      lastContentRender[messageType] = render;
+    },
+    lastContentRender(message) {
+      if (!isFunction(lastContentRender[message.type])) {
+        console.error(
+          `not found '${
+            message.type
+          }' of the latest message renderer,try to use ‘setLastContentRender()’`,
+        );
+        return "";
+      }
+      return lastContentRender[message.type].call(this, message);
+    },
+    /**
+     * 将字符串内的 EmojiItem.name 替换为 img
+     * @param {String} str 被替换的字符串
+     * @return {String} 替换后的字符串
+     */
+    emojiNameToImage(str) {
+      return str.replace(/\[!(\w+)\]/gi, (str, match) => {
+        const file = match;
+        return emojiMap[file]
+          ? `<img emoji-name="${match}" src="${emojiMap[file]}" />`
+          : `[!${match}]`;
+      });
+    },
+    emojiImageToName(str) {
+      return str.replace(/<img emoji-name=\"([^\"]*?)\" [^>]*>/gi, "[!$1]");
+    },
+    updateCurrentMessages() {
+      if (!allMessages[this.currentContactId]){
+          allMessages[this.currentContactId] = [];
+      }
+      //console.log("qxj allMessages:"+JSON.stringify(allMessages));
+      this.currentMessages = allMessages[this.currentContactId];
+    },
+    /**
+     * 将当前聊天窗口滚动到底部
+     */
+    messageViewToBottom() {
+      this.$refs.messages.scrollToBottom();
+    },
+    /**
+     * 设置联系人的草稿信息
+     */
+    setDraft(cid, editorValue) {
+      if (isEmpty(cid) || isEmpty(editorValue)) return false;
+      const conversation = this.findConversation(cid);
+      let lastContent = conversation.lastContent;
+      if (isEmpty(conversation)) return false;
+      if (this.CacheDraft.has(cid)) {
+        lastContent = this.CacheDraft.get(cid).lastContent;
+      }
+      this.CacheDraft.set(cid, {
+        editorValue,
+        lastContent,
+      });
+      
+      var tempDraft=this.CacheDraft.get(cid);
+      //console.log("qxj editorValue:"+editorValue+",tempDraft:"+JSON.stringify(tempDraft)+"lastContent:"+lastContent);
+
+      this.updateContact({
+        id: cid,
+        lastContent: `<span style="color:red;">[草稿]</span><span>${this.lastContentRender(
+          { type: "text", content: editorValue },
+        )}</span>`,
+      });
+    },
+    /**
+     * 清空联系人草稿信息
+     */
+    clearDraft(contactId) {
+      const draft = this.CacheDraft.get(contactId);
+      if (draft) {
+        const currentContent = this.findConversation(contactId).lastContent;
+        console.log("qxj currentContent:"+currentContent+",lastContent:"+JSON.stringify(draft));
+        //if (currentContent.indexOf('<span style="color:red;">[草稿]</span>') === 0) {
+          this.updateContact({
+            id: contactId,
+            lastContent: draft.lastContent
+          });
+        //}
+        this.CacheDraft.remove(contactId);
+      }
+    },
+    /**
+     * 改变聊天对象
+     * @param contactId 联系人 id
+     */
+    async changeContact(contactId, menuName) {
+      if (menuName) {
+           this.changeMenu(menuName);
+      } else {
+        if (this._changeContactLock || this.currentContactId == contactId){
+           return false;
+        }
+      }
+      //保存上个聊天目标的草稿
+      if (this.currentContactId) {
+        const editorValue = clearHtmlExcludeImg(this.getEditorValue()).trim();
+        if (editorValue) {
+          this.setDraft(this.currentContactId, editorValue);
+          this.setEditorValue();
+        } else {
+          this.clearDraft(this.currentContactId);
+        }
+      }
+
+      this.currentContactId = contactId;
+      if (!this.currentContactId) return false;
+      this.$emit("change-conversation", this.currentContact, this);
+      if (isFunction(this.currentContact.renderContainer) || this.activeSidebar == DEFAULT_MENU_CONTACTS) {
+          return;
+      }
+      //填充草稿内容
+      const draft = this.CacheDraft.get(contactId);
+      if (draft) this.setEditorValue(draft.editorValue);
+
+      if (this.CacheMessageLoaded.has(contactId)) {
+        this.$refs.messages.loaded();
+      } else {
+        this.$refs.messages.resetLoadState();
+      }
+
+      if (!allMessages[contactId]) {
+          this.updateCurrentMessages();
+          this._emitPullMessages(isEnd => {
+              this.messageViewToBottom();
+          });
+      } else {
+          setTimeout(() => {
+              this.updateCurrentMessages();
+              this.messageViewToBottom();
+          }, 0);
+      }
+    },
+    /**
+     * 删除一条聊天消息
+     * @param messageId 消息 id
+     * @param contactId 联系人 id
+     */
+    removeMessage(messageId) {
+      const message = this.findMessage(messageId);
+      if (!message) return false;
+      const index = allMessages[message.toContactId].findIndex(
+        ({ id }) => id == messageId,
+      );
+      allMessages[message.toContactId].splice(index, 1);
+      return true;
+    },
+    /**
+     * 修改聊天一条聊天消息
+     * @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
+     * @param contactId 联系人 id
+     */
+    updateMessage(message) {
+      if (!message.id) return false;
+      let historyMessage = this.findMessage(message.id);
+      if (!historyMessage) return false;
+      historyMessage = Object.assign(historyMessage, message, {
+        toContactId: historyMessage.toContactId,
+      });
+      return true;
+    },
+    /**
+     * 手动更新对话消息
+     * @param {String} messageId 消息ID,如果为空则更新当前聊天窗口的所有消息
+     */
+    forceUpdateMessage(messageId) {
+      if (!messageId) {
+        this.$refs.messages.$forceUpdate();
+      } else {
+        const components = this.$refs.messages.$refs.message;
+        if (components) {
+          const messageComponent = components.find(
+            com => com.$attrs.message.id == messageId,
+          );
+          if (messageComponent) messageComponent.$forceUpdate();
+        }
+      }
+    },
+    _customContainerReady(render, cacheDrive, key) {
+      if (isFunction(render) && !cacheDrive.has(key)) {
+        cacheDrive.set(key, render.call(this));
+      }
+    },
+    /**
+     * 切换左侧按钮
+     * @param {String} name 按钮 name
+     */
+    changeMenu(name) {
+      this.$emit("change-menu", name);
+      this.activeSidebar = name;
+    },
+    /**
+     * 初始化编辑框的 Emoji 表情列表,是 Lemon-editor.initEmoji 的代理方法
+     * @param {Array<Emoji,EmojiItem>} data emoji 数据
+     * Emoji = {label: 表情,children: [{name: wx,title: 微笑,src: url}]} 分组
+     * EmojiItem = {name: wx,title: 微笑,src: url} 无分组
+     */
+    initEmoji(data) {
+      let flatData = [];
+      this.$refs.editor.initEmoji(data);
+      if (data[0].label) {
+        data.forEach(item => {
+          flatData.push(...item.children);
+        });
+      } else {
+        flatData = data;
+      }
+      flatData.forEach(({ name, src }) => (emojiMap[name] = src));
+    },
+    initEditorTools(data) {
+      //this.editorTools = data;
+      this.editorTools = data;
+      //this.$refs.editor.initTools(data);
+    },
+    /**
+     * 初始化左侧按钮
+     * @param {Array<Menu>} data 按钮数据
+     */
+    initMenus(data) {
+      const defaultMenus = [
+        {
+          name: DEFAULT_MENU_LASTMESSAGES,
+          title: "聊天",
+          unread: 0,
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-message" />;
+          },
+          isBottom: false,
+        },
+        {
+          name: DEFAULT_MENU_CONTACTS,
+          title: "通讯录",
+          unread: 0,
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-addressbook" />;
+          },
+          isBottom: false,
+        },
+      ];
+      let menus = [];
+      if (Array.isArray(data)) {
+        const indexMap = {
+          messages: 0,
+          contacts: 1,
+        };
+        const indexKeys = Object.keys(indexMap);
+        menus = data.map(item => {
+          if (indexKeys.includes(item.name)) {
+            return {
+              ...defaultMenus[indexMap[item.name]],
+              ...item,
+              ...{ renderContainer: null },
+            };
+          }
+          if (item.renderContainer) {
+            this._customContainerReady(
+              item.renderContainer,
+              this.CacheMenuContainer,
+              item.name,
+            );
+          }
+          return item;
+        });
+      } else {
+        menus = defaultMenus;
+      }
+      this.menus = menus;
+    },
+    /**
+     * 初始化联系人数据
+     * @param {Array<Contact>} data 联系人列表
+     */
+    initContacts(data) {
+      this.contacts = data;
+      this.sortContacts();
+      if(!!this.contacts>0){
+          this.currentNewContactId=(this.contacts[0]).id;
+      }
+    },
+      /**
+     * 初始化会话数据
+     * @param {Array<Contact>} data 会话列表
+     */
+    initConversations(data) {
+      this.conversations = data;
+      this.sortConversations();
+      
+    },
+
+    /**
+     * 使用 联系人的 index 值进行排序
+     */
+    sortContacts() {
+      this.contacts.sort((a, b) => {
+        if (!a.index) return;
+        return a.index.localeCompare(b.index);
+      });
+    },
+    sortConversations() {
+      this.conversations.sort((a, b) => {
+        if (!a.index) return;
+        return a.index.localeCompare(b.index);
+      });
+    },
+    appendContact(contact) {
+      if (isEmpty(contact.id) || isEmpty(contact.displayName)) {
+        console.error("id | displayName cant be empty");
+        return false;
+      }
+      if (this.hasContact(contact.id)) return true;
+      this.contacts.push(
+        Object.assign(
+          {
+            id: "",
+            displayName: "",
+            avatar: "",
+            index: "",
+            unread: 0,
+            lastSendTime: "",
+            lastContent: "",
+          },
+          contact,
+        ),
+      );
+      return true;
+    },
+
+     appendConversation(conversation) {
+      if (isEmpty(conversation.conversationId) || isEmpty(conversation.displayName)) {
+        console.error("id | displayName cant be empty");
+        return false;
+      }
+      if (this.hasConversation(conversation.conversationId)) return true;
+      let insConversation=Object.assign({
+            conversationId: "",
+            displayName: "",
+            avatar: "",
+            index: "",
+            unread: 1,
+            lastSendTime: "",
+            lastContent: "",
+          },conversation
+        );
+       console.log("qxj insConversation:"+JSON.stringify(insConversation));
+       this.conversations.unshift(insConversation);
+      //  this.updateContact(insConversation);
+       //this.conversations.push(insConversation);
+      return true;
+    },
+    removeContact(id) {
+      const index = this.findContactIndexById(id);
+      if (index === -1) return false;
+      this.contacts.splice(index, 1);
+      this.CacheDraft.remove(id);
+      this.CacheMessageLoaded.remove(id);
+      return true;
+    },
+    removeConversations(conversationId) {
+      const index = this.findConversationIndexById(conversationId);
+      if (index === -1) return false;
+      this.conversations.splice(index, 1);
+      // this.CacheDraft.remove(id);
+      // this.CacheMessageLoaded.remove(id);
+      return true;
+    },
+    topPopConversations(conversation,isNotDel) {  //置顶
+      if(!isNotDel){
+          this.removeConversations(conversation.conversationId);
+      }
+      this.conversations.unshift(conversation);
+    },
+    /**
+     * 修改会话数据
+     * @param {Contact} data 修改的数据,根据 Contact.id 查找联系人并覆盖传入的值
+     */
+    updateContact(data) {
+      const conversationId = data.conversationId;
+      const index = this.findConversationIndexById(conversationId);  
+      if (index !== -1) {
+        // if(isString(data.unread)) {
+        //     if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
+        //       data.unread =parseInt(unread) + parseInt(this.conversations[index].unread);
+        //     }
+        // }
+        this.$set(this.conversations, index, {
+          ...this.conversations[index],
+          ...data,
+        });
+      }
+    },
+    updateContact1(data) {
+      const contactId = data.id;
+      delete data.id;
+      const index = this.findContactIndexById(contactId);  
+      if (index !== -1) {
+        const { unread } = data;
+        if (isString(unread)) {
+          if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
+            data.unread =parseInt(unread) + parseInt(this.contacts[index].unread);
+          }
+        }
+        this.$set(this.contacts, index, {
+          ...this.contacts[index],
+          ...data,
+        });
+      }
+    },
+    /**
+     * 根据 id 查找联系人的索引
+     * @param contactId 联系人 id
+     * @return {Number} 联系人索引,未找到返回 -1
+     */
+    findContactIndexById(contactId) {
+      return this.contacts.findIndex(item => item.id == contactId);
+    },
+    findConversationIndexById(conversationId) {
+      return this.conversations.findIndex(item => item.conversationId == conversationId);
+    },
+    /**
+     * 根据 id 查找判断是否存在联系人
+     * @param contactId 联系人 id
+     * @return {Boolean}
+     */
+    hasContact(contactId) {
+      return this.findContactIndexById(contactId) !== -1;
+    },
+    hasConversation(conversationId) {
+      return this.findConversationIndexById(conversationId) !== -1;
+    },
+    findMessage(messageId) {
+      for (const key in allMessages) {
+        const message = allMessages[key].find(({ id }) => id == messageId);
+        if (message) return message;
+      }
+    },
+    findContact(contactId) {
+      return this.getContacts().find(({ id }) => id == contactId);
+    },
+    findConversation(currentContactId) {
+       return this.conversations.find(item =>  item.conversationId == currentContactId) || {};
+    },
+    /**
+     * 返回所有联系人
+     * @return {Array<Contact>}
+     */
+    getContacts() {
+      return this.contacts;
+    },
+      /**
+     * 返回所有会话
+     * @return {Array<Contact>}
+     */
+    getConversations() {
+      return this.conversations;
+    },
+    //返回当前聊天窗口联系人信息
+    getCurrentContact() {
+      return this.currentContact;
+    },
+    getCurrentMessages() {
+      return this.currentMessages;
+    },
+    setEditorValue(val = "") {
+      if (!isString(val)) return false;
+      this.$refs.editor.setValue(this.emojiNameToImage(val));
+    },
+    getEditorValue() {
+      return this.$refs.editor.getFormatValue();
+    },
+    /**
+     * 清空某个联系人的消息,切换到该联系人时会重新触发pull-messages事件
+     */
+    clearMessages(contactId) {
+      if (contactId) {
+        delete allMessages[contactId];
+        this.CacheMessageLoaded.remove(contactId);
+        this.CacheDraft.remove(contactId);
+      } else {
+        allMessages = {};
+        this.CacheMessageLoaded.remove();
+        this.CacheDraft.remove();
+      }
+      return true;
+    },
+    /**
+     * 返回所有消息
+     * @return {Object<Contact.id,Message>}
+     */
+    getMessages(contactId) {
+      return (contactId ? allMessages[contactId] : allMessages) || [];
+    },
+    changeDrawer(params) {
+      this.drawerVisible = !this.drawerVisible;
+      if (this.drawerVisible == true) this.openDrawer(params);
+    },
+    // openDrawer(data) {
+    //   renderDrawerContent = data || new Function();
+    //   this.drawerVisible = true;
+    // },
+    openDrawer(params) {
+      renderDrawerContent = isFunction(params)
+        ? params
+        : params.render || new Function();
+      const wrapperWidth = this.$refs.wrapper.clientWidth;
+      const wrapperHeight = this.$refs.wrapper.clientHeight;
+      let width = params.width || 200;
+      let height = params.height || wrapperHeight;
+      let offsetX = params.offsetX || 0;
+      let offsetY = params.offsetY || 0;
+      const position = params.position || "right";
+      if (isString(width)) width = wrapperWidth * toPoint(width);
+      if (isString(height)) height = wrapperHeight * toPoint(height);
+      if (isString(offsetX)) offsetX = wrapperWidth * toPoint(offsetX);
+      if (isString(offsetY)) offsetY = wrapperHeight * toPoint(offsetY);
+
+      this.$refs.drawer.style.width = `${width}px`;
+      this.$refs.drawer.style.height = `${height}px`;
+
+      let left = 0;
+      let top = 0;
+      let shadow = "";
+      if (position == "right") {
+        left = wrapperWidth;
+      } else if (position == "rightInside") {
+        left = wrapperWidth - width;
+        shadow = `-15px 0 16px -14px rgba(0,0,0,0.08)`;
+      } else if (position == "center") {
+        left = wrapperWidth / 2 - width / 2;
+        top = wrapperHeight / 2 - height / 2;
+        shadow = `0 0 20px rgba(0,0,0,0.08)`;
+      }
+      left += offsetX;
+      top += offsetY + -1;
+      this.$refs.drawer.style.top = `${top}px`;
+      this.$refs.drawer.style.left = `${left}px`;
+      this.$refs.drawer.style.boxShadow = shadow;
+
+      this.drawerVisible = true;
+    },
+    closeDrawer() {
+      this.drawerVisible = false;
+    },
+  },
+};
+
+
+
+</script>
+<style lang="stylus">
+bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
+@import '../styles/utils/index'
+
++b(lemon-wrapper)
+  display flex
+  font-size 14px
+  font-family "Microsoft YaHei"
+  //mask-image radial-gradient(circle, white 100%, black 100%)
+  background #efefef
+  transition all .4s bezier
+  position relative
+  p
+    margin 0
+  img
+    vertical-align middle
+    border-style none
++b(lemon-menu)
+  flex-column()
+  align-items center
+  width 60px
+  background #1d232a
+  padding 15px 0
+  position relative
+  user-select none
+  +e(bottom)
+    flex-column()
+    position absolute
+    bottom 0
+  +e(avatar)
+    margin-bottom 20px
+    cursor pointer
+  +e(item)
+    color #999
+    cursor pointer
+    padding 14px 10px
+    max-width 100%
+    +m(active)
+      color #0fd547
+    &:hover:not(.lemon-menu__item--active)
+      color #eee
+    word-break()
+    > *
+      font-size 24px
+    .ant-badge-count
+      display inline-block
+      padding 0 4px
+      height 18px
+      line-height 16px
+      min-width 18px
+    .ant-badge-count
+    .ant-badge-dot
+      box-shadow 0 0 0 1px #1d232a
++b(lemon-sidebar)
+  width 250px
+  background #efefef
+  display flex
+  flex-direction column
+  +e(scroll)
+    overflow-y auto
+    scrollbar-light()
+  +e(label)
+    padding 6px 14px 6px 14px
+    color #666
+    font-size 12px
+    margin 0
+    text-align left
+  +b(lemon-contact--active)
+    background #d9d9d9
++b(lemon-container)
+  flex 1
+  flex-column()
+  background #f4f4f4
+  word-break()
+  position relative
+  z-index 10
+  +e(title)
+    padding 15px 15px
+  +e(displayname)
+    font-size 16px
++b(lemon-vessel)
+  display flex
+  flex 1
+  min-height 100px
+  +e(left)
+    display flex
+    flex-direction column
+    flex 1
+  +e(right)
+    flex none
++b(lemon-messages)
+  flex 1
+  height auto
++b(lemon-drawer)
+  position absolute
+  top 0
+  overflow hidden
+  background #f6f6f6
+  z-index 11
+  display none
++b(lemon-wrapper)
+  +m(drawer-show)
+    +b(lemon-drawer)
+      display block
++b(lemon-contact-info)
+  flex-column()
+  justify-content center
+  align-items center
+  height 100%
+  h4
+    font-size 16px
+    font-weight normal
+    margin 10px 0 20px 0
+    user-select none
+.lemon-wrapper--theme-blue
+  .lemon-message__content
+    background #f3f3f3
+    &::before
+      border-right-color #f3f3f3
+  .lemon-message--reverse .lemon-message__content
+    background #e6eeff
+    &::before
+      border-left-color #e6eeff
+  .lemon-container
+    background #fff
+  .lemon-sidebar
+    background #f9f9f9
+    .lemon-contact
+      background #f9f9f9
+      &:hover:not(.lemon-contact--active)
+        background #f1f1f1
+      &--active
+        background #e9e9e9
+  .lemon-menu
+    background #096bff
+  .lemon-menu__item
+    color rgba(255,255,255,0.4)
+    &:hover:not(.lemon-menu__item--active)
+      color rgba(255,255,255,0.6)
+    &--active
+      color #fff
+      text-shadow 0 0 10px rgba(2,48,118,0.4)
+.lemon-wrapper--simple
+  .lemon-menu
+  .lemon-sidebar
+    display none
+.lemon-wrapper--simple
+  .lemon-menu
+  .lemon-sidebar
+    display none
++b(lemon-contextmenu)
+  border-radius 4px
+  font-size 14px
+  font-variant tabular-nums
+  line-height 1.5
+  color rgba(0, 0, 0, 0.65)
+  z-index 9999
+  background-color #fff
+  border-radius 6px
+  box-shadow 0 2px 8px rgba(0, 0, 0, 0.06)
+  position absolute
+  transform-origin 50% 150%
+  box-sizing border-box
+  user-select none
+  overflow hidden
+  min-width 120px
+  +e(item)
+    font-size 14px
+    line-height 16px
+    padding 10px 15px
+    cursor pointer
+    display flex
+    align-items center
+    color #333
+    > span
+      display inline-block
+      flex none
+      //max-width 100px
+      ellipsis()
+    &:hover
+      background #f3f3f3
+      color #000
+    &:active
+      background #e9e9e9
+  +e(icon)
+    font-size 16px
+    margin-right 4px
+</style>

+ 30 - 0
src/components/LemonUI/components/lemon-message-voice.vue

@@ -0,0 +1,30 @@
+<script>
+export default {
+  name: "lemonMessageVoice",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-voice"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => {
+            return <span>{props.content}&nbsp;🔈</span>;
+          }
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+.lemon-message.lemon-message-voice
+  user-select none
+  .lemon-message__content
+    border 2px solid #000
+    font-size 12px
+    cursor pointer
+    &::before
+      display none
+</style>

+ 251 - 0
src/components/LemonUI/components/message/basic.vue

@@ -0,0 +1,251 @@
+<script>
+import { useScopedSlot } from "../../utils";
+export default {
+  name: "lemonMessageBasic",
+  inject: {
+    IMUI: {
+      from: "IMUI",
+      default() {
+        return this;
+      },
+    },
+  },
+  props: {
+    contextmenu: Array,
+    message: {
+      type: Object,
+      default: () => {
+        return {};
+      },
+    },
+    timeFormat: {
+      type: Function,
+      default: () => "",
+    },
+    reverse: Boolean,
+    hideName: Boolean,
+    hideTime: Boolean,
+    showContext:Boolean,
+  },
+  data() {
+    return {};
+  },
+  render() {
+    const { fromUser, status, sendTime } = this.message;
+    const hideTitle = this.hideName == true && this.hideTime == true;
+    return (
+      <div
+        class={[
+          "lemon-message",
+          `lemon-message--status-${status}`,
+          {
+            "lemon-message--reverse": this.reverse,
+            "lemon-message--hide-title": hideTitle,
+          },
+        ]}
+      >
+        <div class="lemon-message__avatar">
+          <lemon-avatar
+            size={36}
+            shape="square"
+            src={fromUser.avatar}
+            on-click={e => {
+              this._emitClick(e, "avatar");
+            }}
+          />
+        </div>
+        <div class="lemon-message__inner">
+          <div class="lemon-message__title">
+            {this.hideName == false && (
+              <span
+                on-click={e => {
+                  this._emitClick(e, "displayName");
+                }}
+              >
+                {fromUser.displayName}
+              </span>
+            )}
+            {this.hideTime == false && (
+              <span
+                class="lemon-message__time"
+                on-click={e => {
+                  this._emitClick(e, "sendTime");
+                }}
+              >
+                {this.timeFormat(sendTime)}
+              </span>
+            )}
+          </div>
+
+          <div class="lemon-message__content-flex">
+            <div
+              v-lemon-contextmenu_message={this.IMUI.contextmenu}
+              class="lemon-message__content"
+              on-click={e => {
+                this._emitClick(e, "content");
+              }}
+            >
+              {useScopedSlot(this.$scopedSlots["content"], null, this.message)}
+            </div>
+            <div class="lemon-message__content-after"
+             on-click={e => {
+              this._contextClick(e, "avatar");
+            }}
+            >
+                 {  this.showContext?'查看上下文':'' }
+            </div>
+
+            <div class="lemon-message__status"
+                on-click={e => {
+                  this._emitClick(e, "status");
+                }}
+            >
+              <i class="lemon-icon-loading lemonani-spin" />
+              <i
+                class="lemon-icon-prompt"
+                title="重发消息"
+                style={{
+                  color: "#ff2525",
+                  cursor: "pointer",
+                }}
+              />
+            </div>
+
+
+          </div>
+       
+       
+       </div>
+      </div>
+    );
+  },
+  created() {},
+  mounted() {},
+  computed: {},
+  watch: {},
+  methods: {
+    _emitClick(e, key) {
+      this.IMUI.$emit("message-click", e, key, this.message, this.IMUI);
+    },
+    _contextClick(e, key) {
+      this.IMUI.$emit("context-click", e, key, this.message, this.IMUI);
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
+arrow()
+  content ' '
+  position absolute
+  top 6px
+  width 0
+  height 0
+  border 4px solid transparent
++b(lemon-message)
+  display flex
+  padding 10px 0
+  +e(time)
+    color #b9b9b9
+    padding 0 5px
+  +e(inner)
+    position relative
+  +e(avatar)
+    padding-right 10px
+    user-select none
+    .lemon-avatar
+      cursor pointer
+  +e(title)
+    display flex
+    font-size 12px
+    line-height 16px
+    height 16px
+    padding-bottom 4px
+    user-select none
+    color #666
+  +e(content-flex)
+    display flex
+  +e(content)
+    font-size 14px
+    line-height 20px
+    padding 8px 10px
+    background #fff
+    border-radius 4px
+    position relative
+    margin 0
+    img
+    video
+      background #e9e9e9
+      height 100px
+    &:before
+      arrow()
+      left -4px
+      border-left none
+      border-right-color #fff
+  +e(content-after)
+    display block
+    width 80px !important
+    height 36px
+    line-height 40px
+    padding-left 6px
+    flex none
+    font-size 11px !important
+    color #666 !important
+    overflow hidden
+    visibility hidden
+    cursor pointer
+  +e(status)
+    position absolute
+    top 23px
+    right 20px
+    color #aaa
+    font-size 20px
+    .lemon-icon-loading
+    .lemon-icon-prompt
+      display none
+  +m(status-going)
+    .lemon-icon-loading
+      display inline-block
+  +m(status-failed)
+    .lemon-icon-prompt
+      display inline-block
+  +m(status-succeed)
+    +e(content-after)
+      visibility visible
+  +m(reverse)
+    flex-direction row-reverse
+    +e(content-flex)
+      flex-direction row-reverse
+    +e(content-after)
+      padding-right 6px
+      padding-left 0
+      text-align right
+    +e(title)
+      flex-direction row-reverse
+    +e(status)
+      left 26px
+      right auto
+    +e(content)
+      background #35d863
+      &:before
+        arrow()
+        left auto
+        right -4px
+        border-right none
+        border-left-color #35d863
+    +e(title)
+      text-align right
+    +e(avatar)
+      padding-right 0
+      padding-left 10px
+  +m(hide-title)
+    +e(avatar)
+      padding-top 10px
+    +e(status)
+      top 14px
+    +e(content)
+      position relative
+      top -10px
+      &:before
+        top 14px
+</style>

+ 158 - 0
src/components/LemonUI/components/message/elise-audio.vue

@@ -0,0 +1,158 @@
+<template>
+	<view v-if='url' class='flex audio' :class="{active:status}" :style="{width:getWith(durationS),  background:audioColor}"  @click='play(audioId)' >
+		<view class='mr-3' >
+			<view class="wifi-symbol " :class="status?'active':''">
+            <view class="wifi-circle first"></view>
+            <view class="wifi-circle second"></view>
+            <view class="wifi-circle third"></view>
+        </view>
+		</view>
+		<view class='ml-3'>{{durationS ? durationS + 's' : ''}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				context: null,
+				duration: 100,
+				status: false,
+			}
+		},
+		props: {
+			url: {
+        type:String,
+        default:''
+      },
+			audioColor: {
+				type: String,
+				default: '#5ba5ef'
+			},
+      durationS:[String,Number],
+			audioId: [String,Number]
+		},
+		created() {
+			this.context = uni.createInnerAudioContext();
+			this.context.src = this.url;
+			this.onEnded();
+			uni.$on('stop',(id)=> {
+				if(id && id != this.audioId) {
+					this.context.stop();
+					this.status = false;
+				} else if(!id){
+					this.context.stop();
+					this.status = false;
+				}
+			})
+		},
+		methods: {
+			play(id) { //点击播放
+				if(this.status) {
+					this.context.pause();
+					this.status = !this.status;
+				}else {
+					uni.$emit('stop',id)
+					this.context.play()
+					this.status = !this.status;
+				}
+			},
+			onEnded() { //播放结束
+				this.context.onEnded(()=> {
+					this.status = false;
+				})
+			},
+      getWith(s){
+        if(s){
+          const w = s *10 + 150
+           return  w > 520?'520rpx' :(s *10 + 150)+'rpx'
+        }
+      },
+		}
+	}
+</script>
+
+<style  lang="scss">
+	.audio {
+		background: #68d7bb;
+    height: 58rpx;
+    border-radius: 50rpx;
+    width: 200rpx;
+    align-items: center;
+		// padding: 20rpx;
+    &.active {
+		  opacity: 0.8;
+    }
+    
+	}
+	.flex {
+		display: flex;
+		flex-direction: row;
+    justify-content: space-between;
+	}
+	.flex-1 {
+		flex: 1;
+	}
+	.ml-3 {
+		margin-right: 30rpx;
+    color: #fff;
+	}
+	.mr-3 {
+		margin-left: 30rpx;
+	}
+  .wifi-symbol {
+      width: 50rpx;
+      height: 50rpx;
+      box-sizing: border-box;
+      overflow: hidden;
+      transform: rotate(135deg) translate3d(0, 0, 0);
+      -webkit-transform: rotate(135deg) translate3d(0, 0, 0);
+      backface-visibility: hidden;
+      -webkit-backface-visibility: hidden;
+  }
+  .wifi-circle {
+      border: 5rpx solid #fff;
+      border-radius: 50%;
+      position: absolute;
+  }
+  .first {
+      width: 5rpx;
+      height: 5rpx;
+      background: #fff;
+      top: 45rpx;
+      left: 45rpx;
+  }
+  .second {
+      width: 25rpx;
+      height: 25rpx;
+      top: 35rpx;
+      left: 35rpx;
+  }
+  .third {
+      width: 40rpx;
+      height: 40rpx;
+      top: 25rpx;
+      left: 25rpx;
+  }
+  .active {
+     .second {
+        animation: fadeInOut 1s infinite 0.2s;
+       -webkit-animation: fadeInOut 1s infinite 0.2s;
+    }
+
+    .third {
+        animation: fadeInOut 1s infinite 0.4s;
+        -webkit-animation: fadeInOut 1s infinite 0.4s;
+    }
+  }
+
+  @keyframes fadeInOut {
+      0% {
+          opacity: 0; /*初始状态 透明度为0*/
+      }
+      100% {
+          opacity: 1; /*结尾状态 透明度为1*/
+      }
+  }
+
+</style>

+ 38 - 0
src/components/LemonUI/components/message/event.vue

@@ -0,0 +1,38 @@
+<script>
+export default {
+  name: "lemonMessageEvent",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  render() {
+    const { content } = this.$attrs.message;
+    return (
+      <div class="lemon-message lemon-message-event">
+        <span
+          class="lemon-message-event__content"
+          on-click={e => this._emitClick(e, "content")}
+        >
+          {content}
+        </span>
+      </div>
+    );
+  },
+  methods: {
+    _emitClick(e, key) {
+      this.IMUI.$emit("message-click", e, key, this.$attrs.message, this.IMUI);
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-event)
+  +e(content)
+    user-select none
+    display inline-block
+    background #e9e9e9
+    color #aaa
+    font-size 12px
+    margin 0 auto
+    padding 5px 10px
+    border-radius 4px
+</style>

+ 59 - 0
src/components/LemonUI/components/message/file.vue

@@ -0,0 +1,59 @@
+<script>
+import { formatByte } from "../../utils";
+export default {
+  name: "lemonMessageFile",
+  inheritAttrs: false,
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-file"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => [
+            <div class="lemon-message-file__inner">
+              <p class="lemon-message-file__name">{props.fileName}</p>
+              <p class="lemon-message-file__byte">
+                {formatByte(props.fileSize)}
+              </p>
+            </div>,
+            <div class="lemon-message-file__sfx">
+              <i class="lemon-icon-attah" />
+            </div>
+          ]
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-file)
+  +b(lemon-message)
+    +e(content)
+      display flex
+      cursor pointer
+      width 200px
+      background #fff
+      padding 12px 18px
+      overflow hidden
+      p
+        margin 0
+  +e(tip)
+    display none
+  +e(inner)
+    flex 1
+  +e(name)
+    font-size 14px
+  +e(byte)
+    font-size 12px
+    color #aaa
+  +e(sfx)
+    display flex
+    align-items center
+    justify-content center
+    font-weight bold
+    user-select none
+    font-size 34px
+    color #ccc
+</style>

+ 32 - 0
src/components/LemonUI/components/message/image.vue

@@ -0,0 +1,32 @@
+<script>
+export default {
+  name: "lemonMessageImage",
+  inheritAttrs: false,
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-image"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => <img src={props.content} />
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-image)
+  +b(lemon-message)
+    +e(content)
+      padding 0
+      cursor pointer
+      overflow hidden
+      max-width 20%
+      min-width 100px
+      img
+        width: 100%
+        height: auto
+        display block
+</style>

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

@@ -0,0 +1,36 @@
+<script>
+export default {
+  name: "lemonMessageText",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-text"
+        props={{ ...this.$attrs }}
+        scopedSlots={{ 
+          content: props => {
+            const content = this.IMUI.emojiNameToImage(props.content);
+            return <span domProps={{ innerHTML: content }} />;
+          }
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-text)
+  +b(lemon-message)
+    +e(content)
+      img
+        width 18px
+        height 18px
+        display inline-block
+        background transparent
+        position relative
+        top -1px
+        padding 0 2px
+        vertical-align middle
+</style>

+ 61 - 0
src/components/LemonUI/components/message/video.vue

@@ -0,0 +1,61 @@
+<script>
+export default {
+  name: "lemonMessageVideo",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  data() {
+    return {
+         url:require('@/assets/image/video.png')
+    };
+  },
+  created() {
+    
+  },
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-video"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+           content: props => {
+              return  this.renderHtml(props);
+          }
+        }}
+      />
+    );
+  },
+   methods: {
+      renderHtml(data){       
+        return <div><img class="imgc" src={data.imageUrl} /><img class="btnPlay" src={this.url} /> </div>;  
+        // return <span>{props.content}&nbsp;🔈</span>;
+      }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-video)
+  +b(lemon-message)
+    +e(content)
+      padding 0
+      cursor pointer
+      overflow hidden
+      max-width 20%
+      min-width 100px
+      background: rgba(0,0,0,0)
+      .btnPlay
+        width :40px
+        height: 40px
+        align-items: center
+        justify-content: center
+        z-index 10
+        position: absolute
+        left: calc(50% - 20px)
+        top: calc(50% - 30px)
+        background: rgba(0,0,0,0)
+      .imgc
+        width: 100%
+        height: auto
+        display block
+        background: rgba(0,0,0,0)
+</style>

+ 47 - 0
src/components/LemonUI/components/message/voice.vue

@@ -0,0 +1,47 @@
+<script>
+export default {
+  name: "lemonMessageVoice",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  data() {
+    return {
+       
+    };
+  },
+  created() {
+    
+  },
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-voice"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => {
+            return  this.renderHtml(props);
+          }
+        }}
+      />
+    );
+  },
+   methods: {
+      renderHtml(data){       
+        return <audio block="true" showDuration='false' duration={data.duration} controls src={data.content}  ></audio>;
+        // return <span>{props.content}&nbsp;🔈</span>;
+      }
+  }
+};
+</script>
+
+<style lang="stylus">
+
+.lemon-message.lemon-message-voice
+  user-select none
+  .lemon-message__content
+    border 0px solid #000
+    font-size 12px
+    cursor pointer
+    &::before
+      display none
+
+</style>

+ 200 - 0
src/components/LemonUI/components/messages.vue

@@ -0,0 +1,200 @@
+<script>
+import { hoursTimeFormat } from "../utils";
+import { isString } from "../utils/validate";
+import contextmenu from "../directives/contextmenu";
+export default {
+  name: "LemonMessages",
+  components: {},
+  props: {
+    //是否隐藏消息发送人昵称
+    hideName: Boolean,
+    //是否隐藏显示消息时间
+    hideTime: Boolean,
+    showContext: Boolean,
+    reverseUserId: [String, Number],
+    timeRange: {
+      type: Number,
+      default: 1,
+    },
+    timeFormat: {
+      type: Function,
+      default(val) {
+        return hoursTimeFormat(val);
+      },
+    },
+    loadingText: {
+      type: [String, Function],
+    },
+    loadendText: {
+      type: [String, Function],
+      default: "暂无更多消息",
+    },
+    messages: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    this._lockScroll = false;
+    return {
+      _loading: false,
+      _loadend: false,
+    };
+  },
+  render() {
+    return (
+      <div class="lemon-messages" ref="wrap" on-scroll={this._handleScroll}>
+        <div
+          class={[
+            "lemon-messages__load",
+            `lemon-messages__load--${this._loadend ? "end" : "ing"}`,
+          ]}
+        >
+          <span class="lemon-messages__loadend">
+            {isString(this.loadendText) ? this.loadendText : this.loadendText()}
+          </span>
+          <span class="lemon-messages__loading">
+            {this.loadingText ? (
+              isString(this.loadingText) ? (
+                this.loadingText
+              ) : (
+                this.loadingText()
+              )
+            ) : (
+                <i class="lemon-icon-loading lemonani-spin" />
+            )}
+          </span>
+        </div>
+        {this.messages.map((message, index) => {
+          const node = [];
+          const tagName = `lemon-message-${message.type}`;
+          const prev = this.messages[index - 1];
+          if (
+            prev &&
+            this.msecRange &&
+            message.sendTime - prev.sendTime > this.msecRange
+          ) {
+            node.push(
+              <lemon-message-event
+                attrs={{
+                  message: {
+                    id: "__time__",
+                    type: "event",
+                    content: hoursTimeFormat(message.sendTime),
+                  },
+                }}
+              />,
+            );
+          }
+          let attrs;
+          if (message.type == "event") {
+            attrs = { message: message };
+          } else {
+            attrs = {
+              timeFormat: this.timeFormat,
+              message: message,
+              reverse: this.reverseUserId == message.fromUser.id,
+              hideTime: this.hideTime,
+              hideName: this.hideName,
+              showContext:this.showContext
+            };
+            
+          }
+          node.push(<tagName ref="message" refInFor={true} attrs={attrs} />);
+          return node;
+        })}
+      
+      
+      
+      </div>
+    );
+  },
+  computed: {
+    msecRange() {
+      return this.timeRange * 1000 * 60;
+    },
+  },
+  watch: {},
+  methods: {
+    loaded() {
+      this._loadend = true;
+      this.$forceUpdate();
+    },
+    resetLoadState() {
+      this._lockScroll = true;
+      this._loading = false;
+      this._loadend = false;
+      setTimeout(() => {
+        this._lockScroll = false;
+      }, 200);
+    },
+    async _handleScroll(e) {
+      console.log("qxj _handleScroll");
+      if (this._lockScroll) return;
+      const { target } = e;
+      contextmenu.hide();
+      if (target.scrollTop == 0 && this._loading == false && this._loadend == false) {
+          this._loading = true;
+          await this.$nextTick();
+          const hst = target.scrollHeight;
+          this.$emit("reach-top", async isEnd => {
+            await this.$nextTick();
+            target.scrollTop = target.scrollHeight - hst;
+            this._loading = false;
+            this._loadend = !!isEnd;
+          });
+      }
+      // if (target.scrollBottom == 0) {
+      //     this._loading = true;
+      //     await this.$nextTick();
+      //     const hst = target.scrollHeight;
+      //     this.$emit("reach-bottom", async isEnd => {
+      //       await this.$nextTick();
+      //       target.scrollBottom = target.scrollHeight - hst;
+      //       //this._loading = false;
+      //       //this._loadend = !!isEnd;
+      //     });
+      // }
+    },
+    async scrollToBottom() {
+      await this.$nextTick();
+      const { wrap } = this.$refs;
+      console.log("qxj scrollToBottom wrap:"+wrap);
+      if (wrap) {
+        wrap.scrollTop = wrap.scrollHeight;
+      }
+    },
+  },
+  created() {},
+  mounted() {},
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-messages)
+  height 400px
+  overflow-x hidden
+  overflow-y auto
+  scrollbar-light()
+  padding 10px 15px
+  +e(time)
+    text-align center
+    font-size 12px
+  +e(load)
+    user-select none
+    font-size 12px
+    text-align center
+    color #999
+    line-height 30px
+    .lemon-messages__loading
+    .lemon-messages__loadend
+      display none
+    +m(ing)
+      .lemon-icon-loading
+        font-size 22px
+      .lemon-messages__loading
+        display block
+    +m(end)
+      .lemon-messages__loadend
+        display block
+</style>

+ 146 - 0
src/components/LemonUI/components/popover.vue

@@ -0,0 +1,146 @@
+<script>
+const popoverCloseQueue = [];
+import contextmenu from "../directives/contextmenu";
+const triggerEvents = {
+  hover(el) {},
+  focus(el) {
+    el.addEventListener("focus", e => {
+      this.changeVisible();
+    });
+    el.addEventListener("blur", e => {
+      this.changeVisible();
+    });
+  },
+  click(el) {
+    el.addEventListener("click", e => {
+      e.stopPropagation();
+      contextmenu.hide();
+      this.changeVisible();
+    });
+  },
+  contextmenu(el) {
+    el.addEventListener("contextmenu", e => {
+      e.preventDefault();
+      this.changeVisible();
+    });
+  }
+};
+export default {
+  name: "LemonPopover",
+  props: {
+    trigger: {
+      type: String,
+      default: "click",
+      validator(val) {
+        return Object.keys(triggerEvents).includes(val);
+      }
+    }
+  },
+  data() {
+    return {
+      popoverStyle: {},
+      visible: false
+    };
+  },
+  created() {
+    document.addEventListener("click", this._documentClickEvent);
+    popoverCloseQueue.push(this.close);
+  },
+  mounted() {
+    triggerEvents[this.trigger].call(this, this.$slots.default[0].elm);
+  },
+  render() {
+    return (
+      <span style="position:relative">
+        <transition name="lemon-slide-top">
+          {this.visible && (
+            <div
+              class="lemon-popover"
+              ref="popover"
+              style={this.popoverStyle}
+              on-click={e => e.stopPropagation()}
+            >
+              <div class="lemon-popover__content">{this.$slots.content}</div>
+              <div class="lemon-popover__arrow" />
+            </div>
+          )}
+        </transition>
+        {this.$slots.default}
+      </span>
+    );
+  },
+  destroyed() {
+    document.removeEventListener("click", this._documentClickEvent);
+  },
+  computed: {},
+  watch: {
+    async visible(val) {
+      if (val) {
+        await this.$nextTick();
+        const defaultEl = this.$slots.default[0].elm;
+        const contentEl = this.$refs.popover;
+
+        this.popoverStyle = {
+          top: `-${contentEl.offsetHeight + 10}px`,
+          left: `${defaultEl.offsetWidth / 2 - contentEl.offsetWidth / 2}px`
+        };
+      }
+    }
+  },
+  methods: {
+    _documentClickEvent(e) {
+      e.stopPropagation();
+      if (this.visible) this.close();
+    },
+    changeVisible() {
+      this.visible ? this.close() : this.open();
+    },
+    open() {
+      this.closeAll();
+      this.visible = true;
+    },
+    closeAll() {
+      popoverCloseQueue.forEach(callback => callback());
+    },
+    close() {
+      this.visible = false;
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-popover)
+  border 1px solid #eee
+  border-radius 4px
+  font-size 14px
+  font-variant tabular-nums
+  line-height 1.5
+  color rgba(0, 0, 0, 0.65)
+  z-index 10
+  background-color #fff
+  border-radius 4px
+  box-shadow 0 2px 8px rgba(0, 0, 0, 0.08)
+  position absolute
+  transform-origin 50% 150%
+  +e(content)
+    padding 15px
+    box-sizing border-box
+    position relative
+    z-index 1
+  +e(arrow)
+    left 50%
+    transform translateX(-50%) rotate(45deg)
+    position absolute
+    z-index 0
+    bottom -4px
+    box-shadow 3px 3px 7px rgba(0, 0, 0, 0.07)
+    width 8px
+    height 8px
+    background #fff
+.lemon-slide-top-leave-active ,.lemon-slide-top-enter-active
+  transition all .2s cubic-bezier(0.645, 0.045, 0.355, 1)
+.lemon-slide-top-enter, .lemon-slide-top-leave-to
+  transform translateY(-10px) scale(.8)
+  opacity 0
+</style>

+ 1384 - 0
src/components/LemonUI/components/records.vue

@@ -0,0 +1,1384 @@
+<script>
+import {
+  useScopedSlot,
+  funCall,
+  generateUUID,
+  clearHtmlExcludeImg,
+} from "../utils";
+import { isFunction, isString, isEmpty } from "../utils/validate";
+import contextmenu from "../directives/contextmenu";
+import {
+  DEFAULT_MENUS,
+  DEFAULT_MENU_LASTMESSAGES,
+  DEFAULT_MENU_CONTACTS,
+} from "../utils/constant";
+import lastContentRender from "../lastContentRender";
+
+import MemoryCache from "../utils/cache/memory";
+
+let allMessages = {};
+const emojiMap = {};
+const toPx = val => {
+  return isString(val) ? val : `${val}px`;
+};
+const toPoint = str => {
+  return str.replace("%", "") / 100;
+};
+
+let renderDrawerContent = () => {};
+
+export default {
+  name: "LemonRecords",
+  provide() {
+    return {
+      IMUI: this,
+    };
+  },
+  props: {
+    width: {
+      type: [String, Number],
+      default: 850,
+    },
+    height: {
+      type: [String, Number],
+      default: 580,
+    },
+    theme: {
+      type: String,
+      default: "default",
+    },
+    simple: {
+      type: Boolean,
+      default: false,
+    },
+    loadingText: [String, Function],
+    loadendText: [String, Function],
+    /**
+     * 消息时间格式化规则
+     */
+    messageTimeFormat: Function,
+    /**
+     * 联系人最新消息时间格式化规则
+     */
+    contactTimeFormat: Function,
+    /**
+     * 初始化时是否隐藏抽屉
+     */
+    hideDrawer: {
+      type: Boolean,
+      default: true,
+    },
+    /**
+     * 是否隐藏导航按钮上的头像
+     */
+    hideMenuAvatar: Boolean,
+    hideMenu: Boolean,
+    /**
+     * 是否隐藏消息列表内的联系人名字
+     */
+    hideMessageName: Boolean,
+    /**
+     * 是否隐藏消息列表内的发送时间
+     */
+    hideMessageTime: Boolean,
+    showContext:Boolean,
+    sendKey: Function,
+    wrapKey: Function,
+    sendText: String,
+    contextmenu: Array,
+    contactContextmenu: Array,
+    avatarCricle: Boolean,
+    user: {
+      type: Object,
+      default: () => {
+        return {};
+      },
+    },
+  },
+  data() {
+    this.CacheContactContainer = new MemoryCache();
+    this.CacheMenuContainer = new MemoryCache();
+    this.CacheMessageLoaded = new MemoryCache();
+    this.CacheDraft = new MemoryCache();
+    return {
+      drawerVisible: !this.hideDrawer,
+      currentContactId: null,
+      currentNewContactId: null,
+      currentMessages: [],
+      activeSidebar: DEFAULT_MENU_LASTMESSAGES,
+      contacts: [],
+      conversations:[],
+      menus: [],
+      editorTools: [
+        { name: "emoji" },
+        { name: "uploadFile" },
+        { name: "uploadImage" },
+      ],
+    };
+  },
+
+  render() {
+    return this._renderWrapper([
+      //this._renderMenu(),
+      //this._renderSidebarMessage(),//加载会话数据
+      //this._renderSidebarContact(),//加载联系人
+      this._renderContainer(),
+      //this._renderDrawer(),
+    ]);
+  },
+  created() {
+    this.initMenus();
+  },
+  async mounted() {
+    await this.$nextTick();
+  },
+  computed: {
+    currentContact() {
+       return this.conversations.find(item =>  item.conversationId == this.currentContactId) || {};
+    },
+    //获取当前联系人
+    currentNewContact() {
+       return this.contacts.find(item =>  item.id == this.currentNewContactId) || {};
+    },
+    currentMenu() {
+      return this.menus.find(item => item.name == this.activeSidebar) || {};
+    },
+    currentIsDefSidebar() {
+      return DEFAULT_MENUS.includes(this.activeSidebar);
+    },
+    lastMessages() {
+      // var data = this.contacts.filter(item => !isEmpty(item.lastContent));
+      // data.sort((a1, a2) => {
+      //   return a2.lastSendTime - a1.lastSendTime;
+      // });
+      var data=null;
+      if(!!this.conversations){
+          data=this.conversations;
+      }
+      return data;
+    },
+  },
+  watch: {
+    activeSidebar() {},
+  },
+  methods: {
+    _menuIsContacts() {
+      return this.activeSidebar == DEFAULT_MENU_CONTACTS;
+    },
+    _menuIsMessages() {
+      return this.activeSidebar == DEFAULT_MENU_LASTMESSAGES;
+    },
+    _createMessage(message) {
+      return {
+        ...{
+          id: generateUUID(),
+          type: "text",
+          status: "going",
+          sendTime: new Date().getTime(),
+          toContactId: this.currentContactId,
+          fromUser: {
+            ...this.user,
+          },
+        },
+        ...message,
+      };
+    },
+    /**
+     * 新增一条消息
+     */
+    appendMessage(message, scrollToBottom = false) {
+        let messageList = allMessages[message.toContactId];
+        // 如果是自己的消息需要push,发送的消息不再增加未读条数
+        let conversation=this.findConversation(message.toContactId);
+        let unread=conversation.unread!=undefined && conversation.unread>0?(conversation.unread+1):1;
+        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;
+          conversation.unread = unread;
+          conversation.lastSendTime=message.sendTime;
+          conversation.lastContent=this.lastContentRender(message);
+          this.updateContact(conversation);
+      } else {
+          // 如果消息存在则不再添加
+          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) {
+            conversation.unread=0;
+            if (scrollToBottom == true) {
+              this.messageViewToBottom();
+            }
+            this.CacheDraft.remove(message.toContactId);
+          } else {
+            conversation.unread = unread;
+          }
+          this.updateContact(conversation);
+      }
+    },
+    _emitSend(message, next, file) {
+      this.$emit(
+        "send",
+        message,
+        (replaceMessage = { status: "succeed" }) => {
+          next();
+          this.updateMessage(Object.assign(message, replaceMessage));
+        },
+        file,
+      );
+    },
+    _handleSend(text) {
+      const message = this._createMessage({ content: text });
+      this.appendMessage(message, true);
+      this._emitSend(message, () => {
+        this.updateContact({
+          id: message.toContactId,
+          lastContent: this.lastContentRender(message),
+          lastSendTime: message.sendTime,
+        });
+        this.CacheDraft.remove(message.toContactId);
+      });
+    },
+    _handleUpload(file) {
+      const imageTypes = ["image/gif", "image/jpeg", "image/png"];
+      let joinMessage;
+      if (imageTypes.includes(file.type)) {
+        joinMessage = {
+          type: "image",
+          content: URL.createObjectURL(file),
+        };
+      } else {
+        joinMessage = {
+          type: "file",
+          fileSize: file.size,
+          fileName: file.name,
+          content: "",
+        };
+      }
+      const message = this._createMessage(joinMessage);
+      this.appendMessage(message, true);
+      this._emitSend(
+        message,
+        () => {
+          this.updateContact({
+            id: message.toContactId,
+            lastContent: this.lastContentRender(message),
+            lastSendTime: message.sendTime,
+          });
+        },
+        file,
+      );
+    },
+    _handleRemoteImage(url) {
+        let joinMessage = {
+            type: "image",
+            content: url,
+          };
+        const message = this._createMessage(joinMessage);
+        this.appendMessage(message, true);
+        this._emitSend(
+          message,
+          () => {
+            this.updateContact({
+              id: message.toContactId,
+              lastContent: this.lastContentRender(message),
+              lastSendTime: message.sendTime,
+            });
+          },
+          null,
+        );
+    },
+    _pickImg(){
+        this.$emit("pick-image");
+    },
+    _emitPullMessages(next) {
+      //console.log("qxj currentContact:"+JSON.stringify(this.currentContact));
+      this._changeContactLock = true;
+      this.$emit("pull-messages",
+      this.currentContact,
+        (messages = [], isEnd = false) => {
+          this._addMessage(messages, this.currentContactId, 0);
+          this.CacheMessageLoaded.set(this.currentContactId, isEnd);
+          if (isEnd == true) this.$refs.messages.loaded();
+          this.updateCurrentMessages();
+          this._changeContactLock = false;
+          next(isEnd);
+        },
+        this,
+      );
+    },
+     _emitPullUpMessages(next) {
+        this._changeContactLock = true;
+        this.$emit("pull-up-messages",
+        this.currentContact,
+          (messages = [], isEnd = false) => {
+            this._addMessage(messages, this.currentContactId, 1);
+            this.CacheMessageLoaded.set(this.currentContactId, isEnd);
+            if (isEnd == true) this.$refs.messages.loaded();
+            this.updateCurrentMessages();
+            this._changeContactLock = false;
+            next(isEnd);
+          },
+          this,
+        );
+    },
+    clearCacheContainer(name) {
+      this.CacheContactContainer.remove(name);
+      this.CacheMenuContainer.remove(name);
+    },
+    _renderWrapper(children) {
+      return (
+        <div
+          style={{
+            width: toPx(this.width),
+            height: toPx(this.height),
+          }}
+          ref="wrapper"
+          class={[
+            "lemon-wrapper",
+            `lemon-wrapper--theme-${this.theme}`,
+            { "lemon-wrapper--simple": this.simple },
+            this.drawerVisible && "lemon-wrapper--drawer-show",
+          ]}
+        >
+          {children}
+        </div>
+      );
+    },
+    _renderMenu() {
+      const menuItem = this._renderMenuItem();
+      return (
+        <div class="lemon-menu" v-show={!this.hideMenu}>
+          {
+            <lemon-avatar
+              v-show={!this.hideMenuAvatar}
+              on-click={e => {
+                this.$emit("menu-avatar-click", e);
+              }}
+              class="lemon-menu__avatar"
+              src={this.user.avatar}
+            />
+          }
+          {menuItem.top}
+          {this.$slots.menu}
+          <div class="lemon-menu__bottom">
+            {this.$slots["menu-bottom"]}
+            {menuItem.bottom}
+          </div>
+        </div>
+      );
+    },
+    _renderMenuAvatar() {
+      return;
+    },
+    _renderMenuItem() {
+      const top = [];
+      const bottom = [];
+      this.menus.forEach(item => {
+        const { name, title, unread, render, click } = item;
+        const node = (
+          <div
+            class={[
+              "lemon-menu__item",
+              { "lemon-menu__item--active": this.activeSidebar == name },
+            ]}
+            on-click={() => {
+              funCall(click, () => {
+                if (name) this.changeMenu(name);
+              });
+            }}
+            title={title}
+          >
+            <lemon-badge count={unread}>{render(item)}</lemon-badge>
+          </div>
+        );
+        item.isBottom === true ? bottom.push(node) : top.push(node);
+      });
+      return {
+        top,
+        bottom,
+      };
+    },
+    _renderSidebarMessage() {
+      return this._renderSidebar(
+        [
+          useScopedSlot(this.$scopedSlots["sidebar-message-top"], null, this),
+          this.lastMessages.map(contact => {
+            return this._renderContact(
+              {
+                contact,
+                timeFormat: this.contactTimeFormat,
+              },
+              () => this.changeContact(contact.conversationId),
+              this.$scopedSlots["sidebar-message"],
+            );
+          }),
+        ],
+        DEFAULT_MENU_LASTMESSAGES,
+        useScopedSlot(
+          this.$scopedSlots["sidebar-message-fixedtop"],
+          null,
+          this,
+        ),
+      );
+    },
+    _renderContact(props, onClick, slot) {
+      const {
+        click: customClick,
+        renderContainer,
+        id: contactId,
+      } = props.contact;
+      const click = () => {
+        funCall(customClick, () => {
+          onClick();
+          this._customContainerReady(
+            renderContainer,
+            this.CacheContactContainer,
+            contactId,
+          );
+        });
+      };
+      return (
+        <lemon-contact
+          class={{
+            "lemon-contact--active":this._menuIsMessages()?this.currentContactId == props.contact.conversationId:(this._menuIsContacts()?this.currentNewContactId == props.contact.id:false)
+          }}
+          v-lemon-contextmenu_contact={this.contactContextmenu}
+          props={props}
+          on-click={click}
+          scopedSlots={{ default: slot }}
+        />
+      );
+    },
+    _renderSidebarContact() {
+      let prevIndex;
+      return this._renderSidebar(
+        [
+          useScopedSlot(this.$scopedSlots["sidebar-contact-top"], null, this),
+          this.contacts.map(contact => {
+            if (!contact.index) return;
+            contact.index = contact.index.replace(/\[[0-9]*\]/, "");
+            const node = [
+              contact.index !== prevIndex && (
+                <p class="lemon-sidebar__label">{contact.index}</p>
+              ),
+              this._renderContact(
+                {
+                  contact: contact,
+                  simple: true,
+                },
+                () => {
+                  console.log("qxj contact click");
+                  this.currentNewContactId=contact.id;
+                  //this.changeContact(contact.id);
+                },
+                this.$scopedSlots["sidebar-contact"],
+              ),
+            ];
+            prevIndex = contact.index;
+            return node;
+          }),
+        ],
+        DEFAULT_MENU_CONTACTS,
+        useScopedSlot(
+          this.$scopedSlots["sidebar-contact-fixedtop"],
+          null,
+          this,
+        ),
+      );
+    },
+    _renderSidebar(children, name, fixedtop) {
+      return (
+        <div
+          class="lemon-sidebar"
+          v-show={this.activeSidebar == name}
+          on-scroll={this._handleSidebarScroll}
+        >
+          <div class="lemon-sidebar__fixed-top">{fixedtop}</div>
+          <div class="lemon-sidebar__scroll">{children}</div>
+        </div>
+      );
+    },
+    _renderDrawer() {
+      return this._menuIsMessages() && this.currentContactId ? (
+        <div class="lemon-drawer" ref="drawer">
+          {renderDrawerContent(this.currentContact)}
+          {useScopedSlot(this.$scopedSlots.drawer, "", this.currentContact)}
+        </div>
+      ) : (
+        ""
+      );
+    },
+    _isContactContainerCache(name) {
+      return name.startsWith("contact#");
+    },
+    _renderContainer() {
+      const nodes = [];
+      const cls = "lemon-container";
+      const curact = this.currentContact;
+      const curactNew = this.currentNewContact;
+      let defIsShow = true;
+      for (const name in this.CacheContactContainer.get()) {
+        const show = curactNew.id == name && this.currentIsDefSidebar;
+        if(show)defIsShow = !show;
+        nodes.push(
+          <div class={cls} v-show={show}>
+            {this.CacheContactContainer.get(name)}
+          </div>,
+        );
+      }
+      for (const name in this.CacheMenuContainer.get()) {
+          nodes.push(
+            <div
+              class={cls}
+              v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
+            >
+              {this.CacheMenuContainer.get(name)}
+            </div>,
+          );
+      }
+      
+      //聊天消息View
+      nodes.push(
+        <div
+          class={cls}
+          v-show={this._menuIsMessages() && defIsShow && curact.id}
+        >
+          <div class="lemon-container__title">
+            {useScopedSlot(
+              this.$scopedSlots["message-title"],
+              <div class="lemon-container__displayname">
+                {curact.displayName}
+              </div>,
+              curact,
+            )}
+          </div>
+          <div class="lemon-vessel">
+            <div class="lemon-vessel__left">
+              <lemon-messages
+                ref="messages"
+                loading-text={this.loadingText}
+                loadend-text={this.loadendText}
+                hide-time={this.hideMessageTime}
+                hide-name={this.hideMessageName}
+                show-context={this.showContext}
+                time-format={this.messageTimeFormat}
+                reverse-user-id={this.user.id}
+                on-reach-top={this._emitPullMessages}
+                on-reach-bottom={this._emitPullUpMessages}
+                messages={this.currentMessages}
+              />
+          
+            </div>
+           
+          </div>
+        </div>,
+      );
+      nodes.push(
+        <div class={cls} v-show={!curact.id && this.currentIsDefSidebar}>
+          {this.$slots.cover}
+        </div>,
+      );
+
+      //联系人View
+      nodes.push(
+        <div
+          class={cls}
+          v-show={this._menuIsContacts() && defIsShow && curactNew.id}>
+          {useScopedSlot(
+            this.$scopedSlots["contact-info"],
+            <div class="lemon-contact-info">
+              <lemon-avatar src={curactNew.avatar} size={90} />
+              <h4>{curactNew.displayName}</h4>
+              <lemon-button
+                on-click={() => {
+                  if (isEmpty(curactNew.lastContent)) {
+                    this.updateContact({
+                      id: curactNew.id,
+                      lastContent: " ",
+                    });
+                  }
+                  this.changeContact(curactNew.conversationId, DEFAULT_MENU_LASTMESSAGES);
+                }}
+              >
+                发送消息
+              </lemon-button>
+            </div>,
+            curactNew,
+          )}
+        </div>,
+      );
+      return nodes;
+    },
+    _handleSidebarScroll() {
+      contextmenu.hide();
+    },
+    _addContact(data, t) {
+      const type = {
+        0: "unshift",
+        1: "push",
+      }[t];
+      this.contacts[type](data);
+    },
+    _addMessage(data, contactId, t) {
+      const type = {
+        0: "unshift",
+        1: "push",
+      }[t];
+      if (!Array.isArray(data)) data = [data];
+      allMessages[contactId] = allMessages[contactId] || [];
+      allMessages[contactId][type](...data);
+    },
+    /**
+     * 设置最新消息DOM
+     * @param {String} messageType 消息类型
+     * @param {Function} render 返回消息 vnode
+     */
+    setLastContentRender(messageType, render) {
+      lastContentRender[messageType] = render;
+    },
+    lastContentRender(message) {
+      if (!isFunction(lastContentRender[message.type])) {
+        console.error(
+          `not found '${
+            message.type
+          }' of the latest message renderer,try to use ‘setLastContentRender()’`,
+        );
+        return "";
+      }
+      return lastContentRender[message.type].call(this, message);
+    },
+    /**
+     * 将字符串内的 EmojiItem.name 替换为 img
+     * @param {String} str 被替换的字符串
+     * @return {String} 替换后的字符串
+     */
+    emojiNameToImage(str) {
+      return str.replace(/\[!(\w+)\]/gi, (str, match) => {
+        const file = match;
+        return emojiMap[file]
+          ? `<img emoji-name="${match}" src="${emojiMap[file]}" />`
+          : `[!${match}]`;
+      });
+    },
+    emojiImageToName(str) {
+      return str.replace(/<img emoji-name=\"([^\"]*?)\" [^>]*>/gi, "[!$1]");
+    },
+    updateCurrentMessages() {
+      if (!allMessages[this.currentContactId]){
+          allMessages[this.currentContactId] = [];
+      }
+      //console.log("qxj allMessages:"+JSON.stringify(allMessages));
+      this.currentMessages = allMessages[this.currentContactId];
+    },
+    /**
+     * 将当前聊天窗口滚动到底部
+     */
+    messageViewToBottom() {
+      this.$refs.messages.scrollToBottom();
+    },
+    /**
+     * 设置联系人的草稿信息
+     */
+    setDraft(cid, editorValue) {
+      if (isEmpty(cid) || isEmpty(editorValue)) return false;
+      const conversation = this.findConversation(cid);
+      let lastContent = conversation.lastContent;
+      if (isEmpty(conversation)) return false;
+      if (this.CacheDraft.has(cid)) {
+        lastContent = this.CacheDraft.get(cid).lastContent;
+      }
+      this.CacheDraft.set(cid, {
+        editorValue,
+        lastContent,
+      });
+      
+      var tempDraft=this.CacheDraft.get(cid);
+      //console.log("qxj editorValue:"+editorValue+",tempDraft:"+JSON.stringify(tempDraft)+"lastContent:"+lastContent);
+
+      this.updateContact({
+        id: cid,
+        lastContent: `<span style="color:red;">[草稿]</span><span>${this.lastContentRender(
+          { type: "text", content: editorValue },
+        )}</span>`,
+      });
+    },
+    /**
+     * 清空联系人草稿信息
+     */
+    clearDraft(contactId) {
+      const draft = this.CacheDraft.get(contactId);
+      if (draft) {
+        const currentContent = this.findConversation(contactId).lastContent;
+        console.log("qxj currentContent:"+currentContent+",lastContent:"+JSON.stringify(draft));
+        //if (currentContent.indexOf('<span style="color:red;">[草稿]</span>') === 0) {
+          this.updateContact({
+            id: contactId,
+            lastContent: draft.lastContent
+          });
+        //}
+        this.CacheDraft.remove(contactId);
+      }
+    },
+    /**
+     * 改变聊天对象
+     * @param contactId 联系人 id
+     */
+    async changeContact(contactId, menuName) {
+      if (menuName) {
+           this.changeMenu(menuName);
+      } else {
+        if (this._changeContactLock || this.currentContactId == contactId){
+           console.log("qxj _changeContactLock currentContactId:"+this.currentContactId+" contactId:"+contactId);
+           return false;
+        }
+      }
+      //保存上个聊天目标的草稿
+      if (this.currentContactId) {
+        const editorValue = clearHtmlExcludeImg(this.getEditorValue()).trim();
+        if (editorValue) {
+          this.setDraft(this.currentContactId, editorValue);
+          this.setEditorValue();
+        } else {
+          this.clearDraft(this.currentContactId);
+        }
+      }
+
+      this.currentContactId = contactId;
+      if (!this.currentContactId) return false;
+      this.$emit("change-conversation", this.currentContact, this);
+      if (isFunction(this.currentContact.renderContainer) || this.activeSidebar == DEFAULT_MENU_CONTACTS) {
+          return;
+      }
+      //填充草稿内容
+      const draft = this.CacheDraft.get(contactId);
+      if (draft) this.setEditorValue(draft.editorValue);
+
+      if (this.CacheMessageLoaded.has(contactId)) {
+        this.$refs.messages.loaded();
+      } else {
+        this.$refs.messages.resetLoadState();
+      }
+      if (!allMessages[contactId]) {
+          this.updateCurrentMessages();
+          this._emitPullMessages(isEnd => {
+              this.messageViewToBottom();
+          });
+      } else {
+          setTimeout(() => {
+              this.updateCurrentMessages();
+              this.messageViewToBottom();
+          }, 0);
+      }
+    },
+    /**
+     * 删除一条聊天消息
+     * @param messageId 消息 id
+     * @param contactId 联系人 id
+     */
+    removeMessage(messageId) {
+      const message = this.findMessage(messageId);
+      if (!message) return false;
+      const index = allMessages[message.toContactId].findIndex(
+        ({ id }) => id == messageId,
+      );
+      allMessages[message.toContactId].splice(index, 1);
+      return true;
+    },
+    /**
+     * 修改聊天一条聊天消息
+     * @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
+     * @param contactId 联系人 id
+     */
+    updateMessage(message) {
+      if (!message.id) return false;
+      let historyMessage = this.findMessage(message.id);
+      if (!historyMessage) return false;
+      historyMessage = Object.assign(historyMessage, message, {
+        toContactId: historyMessage.toContactId,
+      });
+      return true;
+    },
+    /**
+     * 手动更新对话消息
+     * @param {String} messageId 消息ID,如果为空则更新当前聊天窗口的所有消息
+     */
+    forceUpdateMessage(messageId) {
+      if (!messageId) {
+        this.$refs.messages.$forceUpdate();
+      } else {
+        const components = this.$refs.messages.$refs.message;
+        if (components) {
+          const messageComponent = components.find(
+            com => com.$attrs.message.id == messageId,
+          );
+          if (messageComponent) messageComponent.$forceUpdate();
+        }
+      }
+    },
+    _customContainerReady(render, cacheDrive, key) {
+      if (isFunction(render) && !cacheDrive.has(key)) {
+        cacheDrive.set(key, render.call(this));
+      }
+    },
+    /**
+     * 切换左侧按钮
+     * @param {String} name 按钮 name
+     */
+    changeMenu(name) {
+      this.$emit("change-menu", name);
+      this.activeSidebar = name;
+    },
+    /**
+     * 初始化编辑框的 Emoji 表情列表,是 Lemon-editor.initEmoji 的代理方法
+     * @param {Array<Emoji,EmojiItem>} data emoji 数据
+     * Emoji = {label: 表情,children: [{name: wx,title: 微笑,src: url}]} 分组
+     * EmojiItem = {name: wx,title: 微笑,src: url} 无分组
+     */
+    initEmoji(data) {
+      let flatData = [];
+      this.$refs.editor.initEmoji(data);
+      if (data[0].label) {
+        data.forEach(item => {
+          flatData.push(...item.children);
+        });
+      } else {
+        flatData = data;
+      }
+      flatData.forEach(({ name, src }) => (emojiMap[name] = src));
+    },
+    initEditorTools(data) {
+      //this.editorTools = data;
+      this.editorTools = data;
+      //this.$refs.editor.initTools(data);
+    },
+    /**
+     * 初始化左侧按钮
+     * @param {Array<Menu>} data 按钮数据
+     */
+    initMenus(data) {
+      const defaultMenus = [
+        {
+          name: DEFAULT_MENU_LASTMESSAGES,
+          title: "聊天",
+          unread: 0,
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-message" />;
+          },
+          isBottom: false,
+        },
+        {
+          name: DEFAULT_MENU_CONTACTS,
+          title: "通讯录",
+          unread: 0,
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-addressbook" />;
+          },
+          isBottom: false,
+        },
+      ];
+      let menus = [];
+      if (Array.isArray(data)) {
+        const indexMap = {
+          messages: 0,
+          contacts: 1,
+        };
+        const indexKeys = Object.keys(indexMap);
+        menus = data.map(item => {
+          if (indexKeys.includes(item.name)) {
+            return {
+              ...defaultMenus[indexMap[item.name]],
+              ...item,
+              ...{ renderContainer: null },
+            };
+          }
+          if (item.renderContainer) {
+            this._customContainerReady(
+              item.renderContainer,
+              this.CacheMenuContainer,
+              item.name,
+            );
+          }
+          return item;
+        });
+      } else {
+        menus = defaultMenus;
+      }
+      this.menus = menus;
+    },
+    /**
+     * 初始化联系人数据
+     * @param {Array<Contact>} data 联系人列表
+     */
+    initContacts(data) {
+      this.contacts = data;
+      this.sortContacts();
+      if(!!this.contacts>0){
+          this.currentNewContactId=(this.contacts[0]).id;
+      }
+    },
+      /**
+     * 初始化会话数据
+     * @param {Array<Contact>} data 会话列表
+     */
+    initConversations(data) {
+      this.conversations = data;
+      this.sortConversations();
+      
+    },
+
+    /**
+     * 使用 联系人的 index 值进行排序
+     */
+    sortContacts() {
+      this.contacts.sort((a, b) => {
+        if (!a.index) return;
+        return a.index.localeCompare(b.index);
+      });
+    },
+    sortConversations() {
+      this.conversations.sort((a, b) => {
+        if (!a.index) return;
+        return a.index.localeCompare(b.index);
+      });
+    },
+    appendContact(contact) {
+      if (isEmpty(contact.id) || isEmpty(contact.displayName)) {
+        console.error("id | displayName cant be empty");
+        return false;
+      }
+      if (this.hasContact(contact.id)) return true;
+      this.contacts.push(
+        Object.assign(
+          {
+            id: "",
+            displayName: "",
+            avatar: "",
+            index: "",
+            unread: 0,
+            lastSendTime: "",
+            lastContent: "",
+          },
+          contact,
+        ),
+      );
+      return true;
+    },
+
+     appendConversation(conversation) {
+      if (isEmpty(conversation.conversationId) || isEmpty(conversation.displayName)) {
+        console.error("id | displayName cant be empty");
+        return false;
+      }
+      if (this.hasConversation(conversation.conversationId)) return true;
+      let insConversation=Object.assign({
+            conversationId: "",
+            displayName: "",
+            avatar: "",
+            index: "",
+            unread: 1,
+            lastSendTime: "",
+            lastContent: "",
+          },conversation
+        );
+       console.log("qxj insConversation:"+JSON.stringify(insConversation));
+       this.conversations.unshift(insConversation);
+      //  this.updateContact(insConversation);
+       //this.conversations.push(insConversation);
+      return true;
+    },
+    removeContact(id) {
+      const index = this.findContactIndexById(id);
+      if (index === -1) return false;
+      this.contacts.splice(index, 1);
+      this.CacheDraft.remove(id);
+      this.CacheMessageLoaded.remove(id);
+      return true;
+    },
+    removeConversations(conversationId) {
+      const index = this.findConversationIndexById(conversationId);
+      if (index === -1) return false;
+      this.conversations.splice(index, 1);
+      // this.CacheDraft.remove(id);
+      // this.CacheMessageLoaded.remove(id);
+      return true;
+    },
+    topPopConversations(conversation,isNotDel) {  //置顶
+      if(!isNotDel){
+          this.removeConversations(conversation.conversationId);
+      }
+      this.conversations.unshift(conversation);
+    },
+    /**
+     * 修改会话数据
+     * @param {Contact} data 修改的数据,根据 Contact.id 查找联系人并覆盖传入的值
+     */
+    updateContact(data) {
+      const conversationId = data.conversationId;
+      const index = this.findConversationIndexById(conversationId);  
+      if (index !== -1) {
+        // if(isString(data.unread)) {
+        //     if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
+        //       data.unread =parseInt(unread) + parseInt(this.conversations[index].unread);
+        //     }
+        // }
+        this.$set(this.conversations, index, {
+          ...this.conversations[index],
+          ...data,
+        });
+      }
+    },
+    updateContact1(data) {
+      const contactId = data.id;
+      delete data.id;
+      const index = this.findContactIndexById(contactId);  
+      if (index !== -1) {
+        const { unread } = data;
+        if (isString(unread)) {
+          if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
+            data.unread =parseInt(unread) + parseInt(this.contacts[index].unread);
+          }
+        }
+        this.$set(this.contacts, index, {
+          ...this.contacts[index],
+          ...data,
+        });
+      }
+    },
+    /**
+     * 根据 id 查找联系人的索引
+     * @param contactId 联系人 id
+     * @return {Number} 联系人索引,未找到返回 -1
+     */
+    findContactIndexById(contactId) {
+      return this.contacts.findIndex(item => item.id == contactId);
+    },
+    findConversationIndexById(conversationId) {
+      return this.conversations.findIndex(item => item.conversationId == conversationId);
+    },
+    /**
+     * 根据 id 查找判断是否存在联系人
+     * @param contactId 联系人 id
+     * @return {Boolean}
+     */
+    hasContact(contactId) {
+      return this.findContactIndexById(contactId) !== -1;
+    },
+    hasConversation(conversationId) {
+      return this.findConversationIndexById(conversationId) !== -1;
+    },
+    findMessage(messageId) {
+      for (const key in allMessages) {
+        const message = allMessages[key].find(({ id }) => id == messageId);
+        if (message) return message;
+      }
+    },
+    findContact(contactId) {
+      return this.getContacts().find(({ id }) => id == contactId);
+    },
+    findConversation(currentContactId) {
+       return this.conversations.find(item =>  item.conversationId == currentContactId) || {};
+    },
+    /**
+     * 返回所有联系人
+     * @return {Array<Contact>}
+     */
+    getContacts() {
+      return this.contacts;
+    },
+      /**
+     * 返回所有会话
+     * @return {Array<Contact>}
+     */
+    getConversations() {
+      return this.conversations;
+    },
+    //返回当前聊天窗口联系人信息
+    getCurrentContact() {
+      return this.currentContact;
+    },
+    getCurrentMessages() {
+      return this.currentMessages;
+    },
+    setEditorValue(val = "") {
+      if (!isString(val)) return false;
+      this.$refs.editor.setValue(this.emojiNameToImage(val));
+    },
+    getEditorValue() {
+      return this.$refs.editor.getFormatValue();
+    },
+    /**
+     * 清空某个联系人的消息,切换到该联系人时会重新触发pull-messages事件
+     */
+    clearMessages(contactId) {
+      if (contactId) {
+        delete allMessages[contactId];
+        this.CacheMessageLoaded.remove(contactId);
+        this.CacheDraft.remove(contactId);
+      } else {
+        allMessages = {};
+        this.CacheMessageLoaded.remove();
+        this.CacheDraft.remove();
+      }
+      return true;
+    },
+    /**
+     * 返回所有消息
+     * @return {Object<Contact.id,Message>}
+     */
+    getMessages(contactId) {
+      return (contactId ? allMessages[contactId] : allMessages) || [];
+    },
+    changeDrawer(params) {
+      this.drawerVisible = !this.drawerVisible;
+      if (this.drawerVisible == true) this.openDrawer(params);
+    },
+    // openDrawer(data) {
+    //   renderDrawerContent = data || new Function();
+    //   this.drawerVisible = true;
+    // },
+    openDrawer(params) {
+      renderDrawerContent = isFunction(params)
+        ? params
+        : params.render || new Function();
+      const wrapperWidth = this.$refs.wrapper.clientWidth;
+      const wrapperHeight = this.$refs.wrapper.clientHeight;
+      let width = params.width || 200;
+      let height = params.height || wrapperHeight;
+      let offsetX = params.offsetX || 0;
+      let offsetY = params.offsetY || 0;
+      const position = params.position || "right";
+      if (isString(width)) width = wrapperWidth * toPoint(width);
+      if (isString(height)) height = wrapperHeight * toPoint(height);
+      if (isString(offsetX)) offsetX = wrapperWidth * toPoint(offsetX);
+      if (isString(offsetY)) offsetY = wrapperHeight * toPoint(offsetY);
+
+      this.$refs.drawer.style.width = `${width}px`;
+      this.$refs.drawer.style.height = `${height}px`;
+
+      let left = 0;
+      let top = 0;
+      let shadow = "";
+      if (position == "right") {
+        left = wrapperWidth;
+      } else if (position == "rightInside") {
+        left = wrapperWidth - width;
+        shadow = `-15px 0 16px -14px rgba(0,0,0,0.08)`;
+      } else if (position == "center") {
+        left = wrapperWidth / 2 - width / 2;
+        top = wrapperHeight / 2 - height / 2;
+        shadow = `0 0 20px rgba(0,0,0,0.08)`;
+      }
+      left += offsetX;
+      top += offsetY + -1;
+      this.$refs.drawer.style.top = `${top}px`;
+      this.$refs.drawer.style.left = `${left}px`;
+      this.$refs.drawer.style.boxShadow = shadow;
+
+      this.drawerVisible = true;
+    },
+    closeDrawer() {
+      this.drawerVisible = false;
+    },
+  },
+};
+
+
+
+</script>
+<style lang="stylus">
+bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
+@import '../styles/utils/index'
+
++b(lemon-wrapper)
+  display flex
+  font-size 14px
+  font-family "Microsoft YaHei"
+  //mask-image radial-gradient(circle, white 100%, black 100%)
+  background #efefef
+  transition all .4s bezier
+  position relative
+  p
+    margin 0
+  img
+    vertical-align middle
+    border-style none
++b(lemon-menu)
+  flex-column()
+  align-items center
+  width 60px
+  background #1d232a
+  padding 15px 0
+  position relative
+  user-select none
+  +e(bottom)
+    flex-column()
+    position absolute
+    bottom 0
+  +e(avatar)
+    margin-bottom 20px
+    cursor pointer
+  +e(item)
+    color #999
+    cursor pointer
+    padding 14px 10px
+    max-width 100%
+    +m(active)
+      color #0fd547
+    &:hover:not(.lemon-menu__item--active)
+      color #eee
+    word-break()
+    > *
+      font-size 24px
+    .ant-badge-count
+      display inline-block
+      padding 0 4px
+      height 18px
+      line-height 16px
+      min-width 18px
+    .ant-badge-count
+    .ant-badge-dot
+      box-shadow 0 0 0 1px #1d232a
++b(lemon-sidebar)
+  width 250px
+  background #efefef
+  display flex
+  flex-direction column
+  +e(scroll)
+    overflow-y auto
+    scrollbar-light()
+  +e(label)
+    padding 6px 14px 6px 14px
+    color #666
+    font-size 12px
+    margin 0
+    text-align left
+  +b(lemon-contact--active)
+    background #d9d9d9
++b(lemon-container)
+  flex 1
+  flex-column()
+  background #f4f4f4
+  word-break()
+  position relative
+  z-index 10
+  +e(title)
+    padding 15px 15px
+  +e(displayname)
+    font-size 16px
++b(lemon-vessel)
+  display flex
+  flex 1
+  min-height 100px
+  +e(left)
+    display flex
+    flex-direction column
+    flex 1
+  +e(right)
+    flex none
++b(lemon-messages)
+  flex 1
+  height auto
++b(lemon-drawer)
+  position absolute
+  top 0
+  overflow hidden
+  background #f6f6f6
+  z-index 11
+  display none
++b(lemon-wrapper)
+  +m(drawer-show)
+    +b(lemon-drawer)
+      display block
++b(lemon-contact-info)
+  flex-column()
+  justify-content center
+  align-items center
+  height 100%
+  h4
+    font-size 16px
+    font-weight normal
+    margin 10px 0 20px 0
+    user-select none
+.lemon-wrapper--theme-blue
+  .lemon-message__content
+    background #f3f3f3
+    &::before
+      border-right-color #f3f3f3
+  .lemon-message--reverse .lemon-message__content
+    background #e6eeff
+    &::before
+      border-left-color #e6eeff
+  .lemon-container
+    background #fff
+  .lemon-sidebar
+    background #f9f9f9
+    .lemon-contact
+      background #f9f9f9
+      &:hover:not(.lemon-contact--active)
+        background #f1f1f1
+      &--active
+        background #e9e9e9
+  .lemon-menu
+    background #096bff
+  .lemon-menu__item
+    color rgba(255,255,255,0.4)
+    &:hover:not(.lemon-menu__item--active)
+      color rgba(255,255,255,0.6)
+    &--active
+      color #fff
+      text-shadow 0 0 10px rgba(2,48,118,0.4)
+.lemon-wrapper--simple
+  .lemon-menu
+  .lemon-sidebar
+    display none
+.lemon-wrapper--simple
+  .lemon-menu
+  .lemon-sidebar
+    display none
++b(lemon-contextmenu)
+  border-radius 4px
+  font-size 14px
+  font-variant tabular-nums
+  line-height 1.5
+  color rgba(0, 0, 0, 0.65)
+  z-index 9999
+  background-color #fff
+  border-radius 6px
+  box-shadow 0 2px 8px rgba(0, 0, 0, 0.06)
+  position absolute
+  transform-origin 50% 150%
+  box-sizing border-box
+  user-select none
+  overflow hidden
+  min-width 120px
+  +e(item)
+    font-size 14px
+    line-height 16px
+    padding 10px 15px
+    cursor pointer
+    display flex
+    align-items center
+    color #333
+    > span
+      display inline-block
+      flex none
+      //max-width 100px
+      ellipsis()
+    &:hover
+      background #f3f3f3
+      color #000
+    &:active
+      background #e9e9e9
+  +e(icon)
+    font-size 16px
+    margin-right 4px
+</style>

+ 77 - 0
src/components/LemonUI/components/tabs.vue

@@ -0,0 +1,77 @@
+<script>
+export default {
+  name: "LemonTabs",
+  props: {
+    activeIndex: String
+  },
+  data() {
+    return {
+      active: this.activeIndex
+    };
+  },
+  mounted() {
+    if (!this.active) {
+      this.active = this.$slots["tab-pane"][0].data.attrs.index;
+    }
+  },
+  render() {
+    const pane = [];
+    const nav = [];
+    this.$slots["tab-pane"].map(vnode => {
+      const { tab, index } = vnode.data.attrs;
+      pane.push(
+        <div class="lemon-tabs-content__pane" v-show={this.active == index}>
+          {vnode}
+        </div>
+      );
+      nav.push(
+        <div
+          class={[
+            "lemon-tabs-nav__item",
+            this.active == index && "lemon-tabs-nav__item--active"
+          ]}
+          on-click={() => this._handleNavClick(index)}
+        >
+          {tab}
+        </div>
+      );
+    });
+    return (
+      <div class="lemon-tabs">
+        <div class="lemon-tabs-content">{pane}</div>
+        <div class="lemon-tabs-nav">{nav}</div>
+      </div>
+    );
+  },
+  methods: {
+    _handleNavClick(index) {
+      this.active = index;
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
+pane-color = #f6f6f6
++b(lemon-tabs)
+  background pane-color
++b(lemon-tabs-content)
+  width 100%
+  height 100%
+  padding 15px
+  +e(pane)
+    //scrollbar-light()
+    //overflow-y auto
+    height 100%
+    width 100%
++b(lemon-tabs-nav)
+  display flex
+  background #eee
+  +e(item)
+    line-height 38px
+    padding 0 15px
+    cursor pointer
+    transition all .3s cubic-bezier(0.645, 0.045, 0.355, 1)
+    +m(active)
+      background pane-color
+</style>

+ 148 - 0
src/components/LemonUI/database/contacts.js

@@ -0,0 +1,148 @@
+export default [
+  {
+    id: 1,
+    displayName: "像梦一样自由",
+    avatar: "https://p.qqan.com/up/2020-2/2020022821001845128.jpg",
+    index: "X",
+    unread: 0,
+    lastSendTime: 1698150575000,
+    lastContent: "你开心吗",
+  },
+  {
+    id: 2,
+    displayName: "梦醒时分、",
+    avatar: "https://p.qqan.com/up/2021-1/20211301122243621.jpg",
+    index: "M",
+    unread: 0,
+    lastSendTime: 1698146315000,
+    lastContent: "不错哟",
+  },
+  {
+    id: 3,
+    displayName: "凌云",
+    avatar: "https://p.qqan.com/up/2021-1/2021129102387841.jpg",
+    index: "L",
+    unread: 0,
+    lastSendTime: 1698146242000,
+    lastContent: "",
+  },
+  {
+    id: 4,
+    displayName: "小郭",
+    avatar: "https://p.qqan.com/up/2021-1/2021122135507881.jpg",
+    index: "X",
+    unread: 0,
+    lastSendTime: 1698146170000,
+    lastContent: "",
+  },
+  {
+    id: 5,
+    displayName: "杨玉泉",
+    avatar: "https://p.qqan.com/up/2021-1/20211211131598147.jpg",
+    index: "Y",
+    unread: 0,
+    lastSendTime: 1698145150000,
+    lastContent: "",
+  },
+  {
+    id: 6,
+    displayName: "森系Style",
+    avatar: "https://p.qqan.com/up/2021-1/2021113104111220.jpg",
+    index: "S",
+    unread: 0,
+    lastSendTime: 1698141142000,
+    lastContent: "",
+  },
+  {
+    id: 7,
+    displayName: "霸王花",
+    avatar: "https://p.qqan.com/up/2021-1/20211411391666.jpg",
+    index: "B",
+    unread: 0,
+    lastSendTime: 1697968342000,
+    lastContent: "你怎么还不睡呀?",
+  },
+  {
+    id: 8,
+    displayName: "曾平",
+    avatar: "https://p.qqan.com/up/2020-12/202012291044425822.jpg",
+    index: "Z",
+    unread: 0,
+    lastSendTime: 1697967982000,
+    lastContent: "",
+  },
+  {
+    id: 9,
+    displayName: "淡然",
+    avatar: "https://p.qqan.com/up/2020-12/202012141813343503.jpg",
+    index: "D",
+    unread: 0,
+    lastSendTime: 1697967202000,
+    lastContent: "",
+  },
+  {
+    id: 10,
+    displayName: "叶子。",
+    avatar: "https://p.qqan.com/up/2021-1/20211301122243621.jpg",
+    index: "Y",
+    unread: 0,
+    lastSendTime: 1697966602000,
+    lastContent: "",
+  },
+  {
+    id: 11,
+    displayName: "土豆",
+    avatar: "https://p.qqan.com/up/2020-12/202012111157268739.jpg",
+    index: "T",
+    unread: 0,
+    lastSendTime: 1697966302000,
+    lastContent: "",
+  },
+  {
+    id: 12,
+    displayName: "清沫",
+    avatar: "https://p.qqan.com/up/2020-12/202012415467996.jpg",
+    index: "Q",
+    unread: 0,
+    lastSendTime: 1697966139000,
+    lastContent: "",
+  },
+  {
+    id: 13,
+    displayName: "Lemon-imui交流群",
+    avatar: "https://p.qqan.com/up/2020-11/20201127157109035.jpg",
+    index: "L",
+    isGroup: true,
+    unread: 0,
+    lastSendTime: 1697966079000,
+    lastContent: "你很开心吗",
+  },
+  {
+    id: 14,
+    displayName: "系统通知",
+    avatar: "https://p.qqan.com/up/2020-6/2020061117234279854.jpg",
+    index: "[1]系統通知",
+    unread: 0,
+    lastSendTime: 1697879439000,
+    lastContent: "宁静致远通过了你的好友请求",
+    renderContainer() {
+      return (
+        ""
+        // <div style="padding:15px;">
+        //   <div>宁静致远通过了你的好友请求</div>
+        //   <div>宁静致远通过了你的好友请求</div>
+        //   <div>宁静致远通过了你的好友请求</div>
+        // </div>
+      );
+    },
+  },
+  {
+    id: 1688856679458971,
+    displayName: "宁静致远。",
+    avatar: "https://p.qqan.com/up/2020-6/2020060308522797777.jpg",
+    index: "N",
+    unread: 0,
+    lastSendTime: 1697811039000,
+    lastContent: "",
+  },
+];

+ 167 - 0
src/components/LemonUI/database/conversations.js

@@ -0,0 +1,167 @@
+
+export default [
+  {
+    conversationId: 1,
+    displayName: "像梦一样自由",
+    avatar: "https://p.qqan.com/up/2020-2/2020022821001845128.jpg",
+    index: "X",
+    unread: 5,
+    lastSendTime: 1566047865417,
+    lastContent: "你开心吗",
+  },
+  {
+    conversationId: 2,
+    displayName: "zyp",
+    avatar: "https://p.qqan.com/up/2021-1/20211301122243621.jpg",
+    index: "Z",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "不错哟",
+  },
+  {
+    conversationId: 3,
+    displayName: "凌云",
+    avatar: "https://p.qqan.com/up/2021-1/2021129102387841.jpg",
+    index: "L",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "",
+  },
+  {
+    conversationId: 4,
+    displayName: "小郭",
+    avatar: "https://p.qqan.com/up/2021-1/2021122135507881.jpg",
+    index: "X",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "",
+  },
+  {
+    conversationId: 5,
+    displayName: "杨玉泉",
+    avatar: "https://p.qqan.com/up/2021-1/20211211131598147.jpg",
+    index: "Y",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "",
+  },
+  {
+    conversationId: 6,
+    displayName: "森系Style",
+    avatar: "https://p.qqan.com/up/2021-1/2021113104111220.jpg",
+    index: "S",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "",
+  },
+  {
+    conversationId: 7,
+    displayName: "霸王花",
+    avatar: "https://p.qqan.com/up/2021-1/20211411391666.jpg",
+    index: "B",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "你怎么还不睡呀?",
+  },
+  {
+    conversationId: 8,
+    displayName: "曾平",
+    avatar: "https://p.qqan.com/up/2020-12/202012291044425822.jpg",
+    index: "Z",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "",
+  },
+  {
+    conversationId: 9,
+    displayName: "淡然",
+    avatar: "https://p.qqan.com/up/2020-12/202012141813343503.jpg",
+    index: "D",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "",
+  },
+  {
+    "conversationId": 7881300287915556,
+    "displayName": "RつE",
+    "avatar": "http://wx.qlogo.cn/mmhead/Q3auHgzwzM4qCB1MNA6PTO8MDXiavEEklbYx0BlX1uRP7KbleAg1rmw/0",
+    "index": "R",
+    "unread": 0,
+    "lastSendTime": 1697678814,
+    "lastContent": "[视频]"
+  },
+  {
+    "conversationId": 7881301063988639,
+    "displayName": "初衷",
+    "avatar": "http://wx.qlogo.cn/mmhead/AVK3Oz5sfA6fLpRL1OrA42xflZXL6zY56I7joCVoX2pXP0ABs28QySOdria474v2grXSHibHShRP8/0",
+    "index": "C",
+    "unread": 0,
+    "lastSendTime": 0,
+    "lastContent": ""
+},
+{
+    "conversationId": 7881303019934662,
+    "displayName": "AllMight",
+    "avatar": "http://wx.qlogo.cn/mmhead/oYwP0cFmRU1Zd0HX9qibZiaIy78ibQDz3H7hlyicAFhUQGfldYYXfdoTeA/0",
+    "index": "A",
+    "unread": 0,
+    "lastSendTime": 1697678592,
+    "lastContent": "[视频]"
+},
+{
+    "conversationId": 7881303630026525,
+    "displayName": "咕咕咕",
+    "avatar": "http://wx.qlogo.cn/mmhead/Q3auHgzwzM4RPPP6vuW2YyHnmJZOkEZMCNYdcMhkFu4q9MGz6iaul4g/0",
+    "index": "G",
+    "unread": 0,
+    "lastSendTime": 0,
+    "lastContent": ""
+},
+{
+    "conversationId": 7881301068952363,
+    "displayName": "Ximi",
+    "avatar": "http://wx.qlogo.cn/mmhead/icYBDgv87Hlam1ib5FZ9LurxDoCtBCevQHGo1Gugg0Cz4/0",
+    "index": "X",
+    "unread": 0,
+    "lastSendTime": 1697621742,
+    "lastContent": "[视频]"
+},
+  {
+    conversationId: 13,
+    displayName: "Lemon-imui交流群",
+    avatar: "https://p.qqan.com/up/2020-11/20201127157109035.jpg",
+    index: "L",
+    isGroup: true,
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "这个咋处理啊?",
+  },
+  {
+    conversationId: 14,
+    displayName: "系统通知",
+    avatar: "https://p.qqan.com/up/2020-6/2020061117234279854.jpg",
+    index: "[1]系統通知",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "宁静致远通过了你的好友请求",
+    renderContainer() {
+      return (
+        ""
+        // <div style="padding:15px;">
+        //   <div>宁静致远通过了你的好友请求</div>
+        //   <div>宁静致远通过了你的好友请求</div>
+        //   <div>宁静致远通过了你的好友请求</div>
+        // </div>
+      );
+    },
+  },
+  {
+    conversationId: 1688856679458971,
+    displayName: "宁静致远。",
+    avatar: "https://p.qqan.com/up/2020-6/2020060308522797777.jpg",
+    index: "N",
+    unread: 0,
+    lastSendTime: 1566047865417,
+    lastContent: "",
+  },
+];

+ 292 - 0
src/components/LemonUI/database/emoji.js

@@ -0,0 +1,292 @@
+export default [
+  {
+    label: "表情",
+    children: [
+      {
+        name: "1f600",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f600.png",
+      },
+      {
+        name: "1f62c",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f62c.png",
+      },
+      {
+        name: "1f601",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f601.png",
+      },
+      {
+        name: "1f602",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f602.png",
+      },
+      {
+        name: "1f923",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f923.png",
+      },
+      {
+        name: "1f973",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f973.png",
+      },
+      {
+        name: "1f603",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f603.png",
+      },
+      {
+        name: "1f604",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f604.png",
+      },
+      {
+        name: "1f605",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f605.png",
+      },
+      {
+        name: "1f606",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f606.png",
+      },
+      {
+        name: "1f607",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f607.png",
+      },
+      {
+        name: "1f609",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f609.png",
+      },
+      {
+        name: "1f60a",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60a.png",
+      },
+      {
+        name: "1f642",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f642.png",
+      },
+      {
+        name: "1f643",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f643.png",
+      },
+      {
+        name: "1263a",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/263a.png",
+      },
+      {
+        name: "1f60b",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60b.png",
+      },
+      {
+        name: "1f60c",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60c.png",
+      },
+      {
+        name: "1f60d",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60d.png",
+      },
+      {
+        name: "1f970",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f970.png",
+      },
+      {
+        name: "1f618",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f618.png",
+      },
+      {
+        name: "1f617",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f617.png",
+      },
+      {
+        name: "1f619",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f619.png",
+      },
+      {
+        name: "1f61a",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61a.png",
+      },
+      {
+        name: "1f61c",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61c.png",
+      },
+      {
+        name: "1f92a",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92a.png",
+      },
+      {
+        name: "1f928",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f928.png",
+      },
+      {
+        name: "1f9d0",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f9d0.png",
+      },
+      {
+        name: "1f61d",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61d.png",
+      },
+      {
+        name: "1f61b",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61b.png",
+      },
+      {
+        name: "1f911",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f911.png",
+      },
+      {
+        name: "1f913",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f913.png",
+      },
+      {
+        name: "1f60e",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60e.png",
+      },
+      {
+        name: "1f929",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f929.png",
+      },
+      {
+        name: "1f921",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f921.png",
+      },
+      {
+        name: "1f920",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f920.png",
+      },
+      {
+        name: "1f917",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f917.png",
+      },
+      {
+        name: "1f60f",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f60f.png",
+      },
+      {
+        name: "1f636",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f636.png",
+      },
+      {
+        name: "1f610",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f610.png",
+      },
+      {
+        name: "1f611",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f611.png",
+      },
+      {
+        name: "1f612",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f612.png",
+      },
+      {
+        name: "1f644",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f644.png",
+      },
+      {
+        name: "1f914",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f914.png",
+      },
+      {
+        name: "1f925",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f925.png",
+      },
+      {
+        name: "1f92d",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92d.png",
+      },
+      {
+        name: "1f92b",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92b.png",
+      },
+      {
+        name: "1f92c",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92c.png",
+      },
+      {
+        name: "1f92f",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f92f.png",
+      },
+      {
+        name: "1f633",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f633.png",
+      },
+      {
+        name: "1f61e",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61e.png",
+      },
+      {
+        name: "1f61f",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f61f.png",
+      },
+      {
+        name: "1f620",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f620.png",
+      },
+      {
+        name: "1f621",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f621.png",
+      },
+    ],
+  },
+  {
+    label: "收藏",
+    children: [
+      {
+        name: "1f62c",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f62c.png",
+      },
+      {
+        name: "1f621",
+        title: "微笑",
+        src: "https://cdn.jsdelivr.net/gh/twitter/twemoji@v14.0.2/assets/72x72/1f621.png",
+      },
+    ],
+  },
+];

+ 360 - 0
src/components/LemonUI/database/messages.js

@@ -0,0 +1,360 @@
+
+import ContactsData from "./contacts";
+import UserData from "./user";
+const generateRandId = () => {
+  return Math.random()
+    .toString(36)
+    .substr(-8);
+};
+const getContact = id => {
+  const data = ContactsData.find(contact => contact.id == id);
+  return { id: data.id, avatar: data.avatar, displayName: data.displayName };
+};
+export default {
+  1: [
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "问你件事",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "啥子。",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "为什么",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "你穿了高跟鞋还这么矮",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "因为我矮啊。[!1f600][!1f600][!1f600]",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "你开心吗",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+  ],
+  2: [],
+  3: [],
+  4: [],
+  5: [],
+  6: [],
+  7: [],
+  8: [],
+  9: [],
+  10: [],
+  11: [],
+  12: [],
+  13: [
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670481209000,
+      content: "我是测试时候看到的",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670541395000,
+      content: "上新版本了,玩玩",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670544995000,
+      content: "项目内没有搞这个",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670545175000,
+      content: "@awesome 最新的,不然哪有这功能",
+      toContactId: 1,
+      fromUser: getContact(5),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670631575000,
+      content: "其实是跟你的遮罩层有冲突",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670804375000,
+      content: "自己修改index哈",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670977175000,
+      content: "你们升级到最近版了吗?",
+      toContactId: 1,
+      fromUser: getContact(6),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670980775000,
+      content: "wo 现在用的142",
+      toContactId: 1,
+      fromUser: getContact(7),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1671247175000,
+      content: "问你件事",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "failed",
+      type: "text",
+      sendTime: 1671340182000,
+      content: "啥子。",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696907382000,
+      content: "为什么",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696920042000,
+      content: "你穿了高跟鞋还这么矮[!1f62c][!1f601][!1f602]",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696995642000,
+      content: "因为我矮啊。[!1f600][!1f600][!1f600]",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696995642000,
+      content: "你有意见吗。[!1f643][!1f643][!1f643]",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696995942000,
+      content: "你开心吗",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+  ],
+  14: [],
+  15: [],
+  1688856679458971: [
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670481209000,
+      content: "我是测试时候看到的",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670541395000,
+      content: "上新版本了,玩玩",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670544995000,
+      content: "项目内没有搞这个",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670545175000,
+      content: "@awesome 最新的,不然哪有这功能",
+      toContactId: 1,
+      fromUser: getContact(5),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670631575000,
+      content: "其实是跟你的遮罩层有冲突",
+      toContactId: 1,
+      fromUser: getContact(4),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670804375000,
+      content: "自己修改index哈",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670977175000,
+      content: "你们升级到最近版了吗?",
+      toContactId: 1,
+      fromUser: getContact(6),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1670980775000,
+      content: "wo 现在用的142",
+      toContactId: 1,
+      fromUser: getContact(7),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1671247175000,
+      content: "问你件事",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "failed",
+      type: "text",
+      sendTime: 1671340182000,
+      content: "啥子。",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696907382000,
+      content: "为什么",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696920042000,
+      content: "你穿了高跟鞋还这么矮[!1f62c][!1f601][!1f602]",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696995642000,
+      content: "因为我矮啊。[!1f600][!1f600][!1f600]",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696995642000,
+      content: "你有意见吗。[!1f643][!1f643][!1f643]",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1696995942000,
+      content: "你开心吗",
+      toContactId: 1,
+      fromUser: getContact(1),
+    },
+  ],
+
+
+
+};

+ 220 - 0
src/components/LemonUI/database/messages1.js

@@ -0,0 +1,220 @@
+
+import ContactsData from "./contacts";
+import UserData from "./user";
+const generateRandId = () => {
+  return Math.random()
+    .toString(36)
+    .substr(-8);
+};
+const getContact = id => {
+  const data = ContactsData.find(contact => contact.id == id);
+  return { id: data.id, avatar: data.avatar, displayName: data.displayName };
+};
+export default {
+  1: [
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "问你件事",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "啥子。",
+      toContactId: 1,
+      fromUser: getContact(7881300287915556),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "为什么",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "你穿了高跟鞋还这么矮",
+      toContactId: 1,
+      fromUser: UserData,
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "因为我矮啊。[!1f600][!1f600][!1f600]",
+      toContactId: 1,
+      fromUser: getContact(7881300287915556),
+    },
+    {
+      id: generateRandId(),
+      status: "succeed",
+      type: "text",
+      sendTime: 1566047865417,
+      content: "你开心吗",
+      toContactId: 1,
+      fromUser: getContact(7881300287915556),
+    },
+  ],
+  // 2: [],
+  // 3: [],
+  // 4: [],
+  // 5: [],
+  // 6: [],
+  // 7: [],
+  // 8: [],
+  // 9: [],
+  // 10: [],
+  // 11: [],
+  // 12: [],
+  // 13: [
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670481209000,
+  //     content: "我是测试时候看到的",
+  //     toContactId: 1,
+  //     fromUser: getContact(4),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670541395000,
+  //     content: "上新版本了,玩玩",
+  //     toContactId: 1,
+  //     fromUser: getContact(4),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670544995000,
+  //     content: "项目内没有搞这个",
+  //     toContactId: 1,
+  //     fromUser: getContact(4),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670545175000,
+  //     content: "@awesome 最新的,不然哪有这功能",
+  //     toContactId: 1,
+  //     fromUser: getContact(5),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670631575000,
+  //     content: "其实是跟你的遮罩层有冲突",
+  //     toContactId: 1,
+  //     fromUser: getContact(4),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670804375000,
+  //     content: "自己修改index哈",
+  //     toContactId: 1,
+  //     fromUser: UserData,
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670977175000,
+  //     content: "你们升级到最近版了吗?",
+  //     toContactId: 1,
+  //     fromUser: getContact(6),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1670980775000,
+  //     content: "wo 现在用的142",
+  //     toContactId: 1,
+  //     fromUser: getContact(7),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1671247175000,
+  //     content: "问你件事",
+  //     toContactId: 1,
+  //     fromUser: UserData,
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "failed",
+  //     type: "text",
+  //     sendTime: 1671340182000,
+  //     content: "啥子。",
+  //     toContactId: 1,
+  //     fromUser: getContact(1),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1696907382000,
+  //     content: "为什么",
+  //     toContactId: 1,
+  //     fromUser: UserData,
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1696920042000,
+  //     content: "你穿了高跟鞋还这么矮[!1f62c][!1f601][!1f602]",
+  //     toContactId: 1,
+  //     fromUser: UserData,
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1696995642000,
+  //     content: "因为我矮啊。[!1f600][!1f600][!1f600]",
+  //     toContactId: 1,
+  //     fromUser: getContact(1),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1696995642000,
+  //     content: "你有意见吗。[!1f643][!1f643][!1f643]",
+  //     toContactId: 1,
+  //     fromUser: getContact(1),
+  //   },
+  //   {
+  //     id: generateRandId(),
+  //     status: "succeed",
+  //     type: "text",
+  //     sendTime: 1696995942000,
+  //     content: "你开心吗",
+  //     toContactId: 1,
+  //     fromUser: getContact(1),
+  //   },
+  // ],
+  // 14: [],
+  // 15: [],
+};

+ 5 - 0
src/components/LemonUI/database/user.js

@@ -0,0 +1,5 @@
+export default {
+  id: 1000,
+  avatar: "https://p.qqan.com/up/2018-4/15244505348390471.jpg",
+  displayName: "野火。",
+};

+ 83 - 0
src/components/LemonUI/directives/contextmenu.js

@@ -0,0 +1,83 @@
+// import Vue from "vue";
+import { isFunction, isEmpty } from "../utils/validate";
+import LemonPopover from "../components/popover.vue";
+let popover;
+
+const hidePopover = () => {
+  if (popover) popover.style.display = "none";
+};
+const showPopover = () => {
+  if (popover) popover.style.display = "block";
+};
+document.addEventListener("click", e => {
+  hidePopover();
+});
+export default {
+  hide: hidePopover,
+  bind(el, binding, vnode) {
+    el.addEventListener(
+      binding.modifiers.click ? "click" : "contextmenu",
+      e => {
+        if (isEmpty(binding.value) || !Array.isArray(binding.value)) return;
+        if (binding.modifiers.click) e.stopPropagation();
+        e.preventDefault();
+        LemonPopover.methods.closeAll();
+        let component;
+        let visibleItems = [];
+        if (binding.modifiers.message) component = vnode.context;
+        else if (binding.modifiers.contact) component = vnode.child;
+        if (!popover) {
+          popover = document.createElement("div");
+          popover.className = "lemon-contextmenu";
+          document.body.appendChild(popover);
+        }
+        popover.innerHTML = binding.value
+          .map(item => {
+            let visible;
+            if (isFunction(item.visible)) {
+              visible = item.visible(component);
+            } else {
+              visible = item.visible === undefined ? true : item.visible;
+            }
+
+            if (visible) {
+              visibleItems.push(item);
+              const icon = item.icon
+                ? `<i class="lemon-contextmenu__icon ${item.icon}"></i>`
+                : "";
+              return `<div style="color:${item.color}" title="${
+                item.text
+              }" class="lemon-contextmenu__item">${icon}<span>${
+                item.text
+              }</span></div>`;
+            }
+            return "";
+          })
+          .join("");
+        popover.style.top = `${e.pageY}px`;
+        popover.style.left = `${e.pageX}px`;
+
+        popover.childNodes.forEach((node, index) => {
+          const { click, render } = visibleItems[index];
+          node.addEventListener("click", e => {
+            e.stopPropagation();
+            if (isFunction(click)) click(e, component, hidePopover);
+          });
+
+          // if (isFunction(render)) {
+          //   const ins = Vue.extend({
+          //     render: h => {
+          //       return render(h, component, hidePopover);
+          //     },
+          //   });
+          //   const renderComponent = new ins().$mount();
+          //   node.querySelector("span").innerHTML =
+          //     renderComponent.$el.outerHTML;
+          // }
+        });
+
+        showPopover();
+      },
+    );
+  },
+};

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/components/LemonUI/index.css


+ 56 - 0
src/components/LemonUI/index.js

@@ -0,0 +1,56 @@
+import Contextmenu from "./directives/contextmenu";
+import LemonTabs from "./components/tabs";
+import LemonPopover from "./components/popover";
+import LemonButton from "./components/button";
+import LemonBadge from "./components/badge";
+import LemonAvatar from "./components/avatar";
+import LemonContact from "./components/contact";
+import LemonEditor from "./components/editor";
+import LemonMessages from "./components/messages";
+import LemonMessageBasic from "./components/message/basic";
+import LemonMessageText from "./components/message/text";
+import lemonMessageImage from "./components/message/image";
+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 LemonIMUI from "./components/index";
+import LemonRecords from "./components/records";
+import "./styles/common/index.styl";
+const version = "1.4.2";
+const components = [
+  LemonIMUI,
+  LemonContact,
+  LemonMessages,
+  LemonEditor,
+  LemonAvatar,
+  LemonBadge,
+  LemonButton,
+  LemonPopover,
+  LemonTabs,
+  LemonMessageBasic,
+  LemonMessageText,
+  lemonMessageImage,
+  lemonMessageFile,
+  lemonMessageEvent,
+  LemonMessageVoice,
+  LemonMessageVideo,
+  LemonRecords
+];
+const install = (Vue) => {
+  Vue.directive("LemonContextmenu", Contextmenu);
+  components.forEach(component => {
+    Vue.component(component.name, component);
+  });
+};
+
+if (typeof window !== "undefined" && window.Vue) {
+  install(window.Vue);
+}
+
+export default {
+  version,
+  install
+};

+ 21 - 0
src/components/LemonUI/lastContentRender.js

@@ -0,0 +1,21 @@
+import {clearHtml } from './utils';
+export default {
+  file(message) {
+    return "[文件]";
+  },
+  image(message) {
+    return "[图片]";
+  },
+  voice(message) {
+    return "[语音]";
+  },
+  video(message) {
+    return "[视频]";
+  },
+  text(message) {
+    return this.emojiNameToImage(clearHtml(message.content));
+  },
+  event(message){
+    return '[通知]';
+  },
+};

+ 13 - 0
src/components/LemonUI/styles/common/animate.styl

@@ -0,0 +1,13 @@
+
+.lemonani-spin
+  display inline-block
+  animation lemonani-spin 1s infinite
+@keyframes lemonani-spin{
+  0%{
+    transform rotate(0deg)
+  }
+  100%{
+    transform rotate(360deg)
+  }
+}
+

+ 45 - 0
src/components/LemonUI/styles/common/icons.styl

@@ -0,0 +1,45 @@
+// @font-face {
+//   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'),
+//   url('//at.alicdn.com/t/font_1312162_neqltsj20an.woff') format('woff'),
+//   url('//at.alicdn.com/t/font_1312162_neqltsj20an.ttf') format('truetype'),
+//   url('//at.alicdn.com/t/font_1312162_neqltsj20an.svg#iconfont') format('svg');
+// }
+@font-face {
+  font-family: 'lemon-icons'; 
+  src:url('../fonts/icon.woff') format('woff');
+}
+[class^='lemon-icon-'],
+[class*=' lemon-icon-']
+  font-family lemon-icons !important
+  speak none
+  font-style normal
+  font-weight 400
+  font-variant normal
+  text-transform none
+  line-height 1
+  vertical-align baseline
+  display inline-block
+
+.lemon-icon-loading:before
+  content '\e633'
+.lemon-icon-prompt:before
+  content '\e71b'
+.lemon-icon-message:before
+  content '\e84a'
+.lemon-icon-emoji:before
+  content '\e6f6'
+.lemon-icon-attah:before
+  content '\e7e1'
+.lemon-icon-image:before
+  content '\e7de'
+.lemon-icon-folder:before
+  content '\e7d1'
+.lemon-icon-people:before
+  content '\e715'
+.lemon-icon-group:before
+  content '\e6ff'
+.lemon-icon-addressbook:before
+  content '\e6e2'

+ 3 - 0
src/components/LemonUI/styles/common/index.styl

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

+ 23 - 0
src/components/LemonUI/styles/common/normalize.styl

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

BIN=BIN
src/components/LemonUI/styles/fonts/icon.woff


+ 67 - 0
src/components/LemonUI/styles/utils/bem.styl

@@ -0,0 +1,67 @@
+// -----------------------------------------------------------------------------
+// bem-sugar.styl --- Bem mixins for stylus language
+//
+// Copyright (c) 2017 Ilya Obuhov
+//
+// Author: Ilya Obuhov <iobuhov.mail@gmail.com>
+// URL: https://github.com/iobuhov/stylus-bem-sugar
+
+
+e-prefix    ?= '__'
+m-prefix    ?= '--'
+m-delimiter ?= '_'
+group-store = ()
+
+str()
+  join('', arguments)
+
+b(name)
+  .{name}
+    {block}
+
+group()
+  caller = called-from[0]
+  level = length(called-from) + 1
+  elements = group-store[level]
+  selector = ()
+  parent = null
+  {join(',', elements)}
+    {block}
+  group-store[level] = null
+
+m(mod, val=null)
+  val    = val && m-delimiter + val
+  mod    = m-prefix + mod
+  mod    = val ? mod + val : mod
+  caller = called-from[0]
+  if caller in ('group')
+    level = length(called-from)
+    mod = str('&', mod)
+    if group-store[level] == null
+      group-store[level] = mod
+    else
+      push(group-store[level], mod)
+  &{mod}
+    {block}
+
+e(element)
+  element = e-prefix + element
+  caller  = called-from[0]
+  gcaller = called-from[1]
+  if caller in ('group')
+    level = length(called-from)
+    if gcaller in ('e' 'm')
+      element = str('& ^[0]', element)
+    else
+      element = str('^[0]', element)
+    if group-store[level] == null
+      group-store[level] = element
+    else
+      push(group-store[level], element)
+  else
+    if caller in ('e' 'm')
+      & ^[0]{element}
+        {block}
+    else
+      &{element}
+        {block}

+ 47 - 0
src/components/LemonUI/styles/utils/functional.styl

@@ -0,0 +1,47 @@
+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 
+    width 5px
+    background-color $background
+
+scrollbar-dark() 
+  scrollbar-theme()
+
+scrollbar-light() 
+  scrollbar-theme(#aaa, transparent)
+
+
+vertical-center()
+  &::after
+    display inline-block
+    content ''
+    height 100%
+    vertical-align middle
+
+position-center($type fixed) 
+  position $type
+  top 50%
+  left 50%
+  transform translate(-50%, -50%)
+ellipsis()
+  text-overflow ellipsis
+  overflow hidden
+  white-space nowrap
+word-break()
+  word-break break-all
+  word-wrap break-word
+  white-space pre-wrap

+ 5 - 0
src/components/LemonUI/styles/utils/index.styl

@@ -0,0 +1,5 @@
+@import './functional';
+@import './bem';
+@import './var';
+
+

+ 26 - 0
src/components/LemonUI/styles/utils/var.styl

@@ -0,0 +1,26 @@
+//color-primary #2977fa
+color-primary = #1bc213
+color-light = #fff
+/* 头像 */
+avatar-size = 45px
+avatar-radius = 50%
+
+/** 标题 */
+title-background = color-primary
+title-color = color-light
+title-height = 44px
+/* 气泡 */
+bubble-background = color-primary
+bubble-color = color-light
+bubble-radius = 12px
+
+bubble-self-background = #e7ebef
+bubble-self-color = #606d84
+
+/* 输入框 */
+editor-textarea-height = 40px
+editor-textarea-radius = 5px
+
+editor-submit-disable-color = #bcbcbc
+editor-submit-disable-background = #ebebeb
+editor-submit-radius = editor-textarea-radius

+ 24 - 0
src/components/LemonUI/utils/cache/memory.js

@@ -0,0 +1,24 @@
+export default class MemoryCache {
+  constructor() {
+    this.table = {};
+  }
+  get(key) {
+    return key ? this.table[key] : this.table;
+  }
+  set(key, val) {
+    this.table[key] = val;
+  }
+  // setOnly(key, val) {
+  //   if (!this.has(key)) this.set(key, val);
+  // }
+  remove(key) {
+    if (key) {
+      delete this.table[key];
+    } else {
+      this.table = {};
+    }
+  }
+  has(key) {
+    return !!this.table[key];
+  }
+}

+ 16 - 0
src/components/LemonUI/utils/constant.js

@@ -0,0 +1,16 @@
+export const EMIT_AVATAR_CLICK = "avatar-click";
+
+export const DEFAULT_MENU_LASTMESSAGES = "messages";
+export const DEFAULT_MENU_CONTACTS = "contacts";
+export const DEFAULT_MENUS = [DEFAULT_MENU_LASTMESSAGES, DEFAULT_MENU_CONTACTS];
+/**
+ * 聊天消息类型
+ */
+export const MESSAGE_TYPE = ["voice", "file", "video", "image", "text"];
+
+/**
+ * 聊天消息状态
+ */
+export const MESSAGE_STATUS = ["going", "succeed", "failed"];
+
+export const CONTACT_TYPE = ["many", "single"];

+ 134 - 0
src/components/LemonUI/utils/index.js

@@ -0,0 +1,134 @@
+import { isPlainObject, isFunction } from "../utils/validate";
+export function messageToHtml() {}
+export function messageToText() {}
+/**
+ * 使用某个组件上的作用域插槽
+ * @param {VueComponent} inject
+ * @param {String} slotName
+ * @param {Node} defaultElement
+ * @param {Object} props
+ */
+export function useScopedSlot(slot, def, props) {
+  return slot ? slot(props) : def;
+}
+export function padZero(val) {
+  return val < 10 ? `0${val}` : val;
+}
+export function hoursTimeFormat(t) {
+  const date = new Date(t);
+  const nowDate = new Date();
+  const Y = t => {
+    return t.getFullYear();
+  };
+  const MD = t => {
+    return `${t.getMonth() + 1}-${t.getDate()}`;
+  };
+  const dateY = Y(date);
+  const nowDateY = Y(nowDate);
+
+  let format;
+  if (dateY !== nowDateY) {
+    format = "y年m月d日 h:i";
+  } else if (`${dateY}-${MD(date)}` === `${nowDateY}-${MD(nowDate)}`) {
+    format = "h:i";
+  } else {
+    format = "m月d日 h:i";
+  }
+  return timeFormat(t, format);
+}
+export function timeFormat(t, format) {
+  if (!format) format = "y-m-d h:i:s";
+  if (t) t = new Date(t);
+  else t = new Date();
+  const formatArr = [
+    t.getFullYear().toString(),
+    padZero((t.getMonth() + 1).toString()),
+    padZero(t.getDate().toString()),
+    padZero(t.getHours().toString()),
+    padZero(t.getMinutes().toString()),
+    padZero(t.getSeconds().toString()),
+  ];
+  const reg = "ymdhis";
+  for (let i = 0; i < formatArr.length; i++) {
+    format = format.replace(reg.charAt(i), formatArr[i]);
+  }
+  return format;
+}
+
+export function funCall(event, callback) {
+  if (isFunction(event)) {
+    event(() => {
+      callback();
+    });
+  } else {
+    callback();
+  }
+}
+/**
+ * 获取数组相交的值组成新数组
+ * @param {Array} a
+ * @param {Array} b
+ */
+export function arrayIntersect(a, b) {
+  return a.filter(x => b.includes(x));
+}
+//清除字符串内的所有HTML标签
+export function clearHtml(str) {
+  return str.replace(/<.*?>/gi, "");
+}
+//清除字符串内的所有HTML标签,除了IMG
+export function clearHtmlExcludeImg(str) {
+  return str.replace(/<(?!img).*?>/gi, "");
+}
+export function error(text) {
+  throw new Error(text);
+}
+export function cloneDeep(obj) {
+  const newobj = { ...obj };
+  for (const key in newobj) {
+    const val = newobj[key];
+    if (isPlainObject(val)) {
+      newobj[key] = cloneDeep(val);
+    }
+  }
+  return newobj;
+}
+
+export function mergeDeep(o1, o2) {
+  for (const key in o2) {
+    if (isPlainObject(o1[key])) {
+      o1[key] = mergeDeep(o1[key], o2[key]);
+    } else {
+      o1[key] = o2[key];
+    }
+  }
+  return o1;
+}
+
+export function formatByte(value) {
+  if (null == value || value == "") {
+    return "0 Bytes";
+  }
+  var unitArr = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
+  var index = 0;
+  var srcsize = parseFloat(value);
+  index = Math.floor(Math.log(srcsize) / Math.log(1024));
+  var size = srcsize / Math.pow(1024, index);
+  size = parseFloat(size.toFixed(2));
+  return size + unitArr[index];
+}
+
+export function generateUUID() {
+  var d = new Date().getTime();
+  if (window.performance && typeof window.performance.now === "function") {
+    d += performance.now(); //use high-precision timer if available
+  }
+  var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(
+    c,
+  ) {
+    var r = (d + Math.random() * 16) % 16 | 0;
+    d = Math.floor(d / 16);
+    return (c == "x" ? r : (r & 0x3) | 0x8).toString(16);
+  });
+  return uuid;
+}

+ 37 - 0
src/components/LemonUI/utils/validate.js

@@ -0,0 +1,37 @@
+export function isPlainObject(obj) {
+  return Object.prototype.toString.call(obj) === "[object Object]";
+}
+export function isString(str) {
+  return typeof str == "string";
+}
+export function isToday(time) {
+  return new Date().getTime() - time < 86400000;
+}
+export function isEmpty(obj) {
+  if (!obj) return true;
+  if (Array.isArray(obj) && obj.length == 0) return true;
+  if (isPlainObject(obj) && Object.values(obj).length == 0) return true;
+  return false;
+}
+export function isUrl(str) {
+  const reg =
+    "^((https|http|ftp|rtsp|mms)?://)" +
+    "?(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?" + //ftp的user@
+    "(([0-9]{1,3}.){3}[0-9]{1,3}" + // IP形式的URL- 199.194.52.184
+    "|" + // 允许IP和DOMAIN(域名)
+    "([0-9a-z_!~*'()-]+.)*" + // 域名- www.
+    "([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]." + // 二级域名
+    "[a-z]{2,6})" + // first level domain- .com or .museum
+    "(:[0-9]{1,4})?" + // 端口- :80
+    "((/?)|" + // 如果没有文件名,则不需要斜杠
+    "(/[0-9a-z_!~*'().;?:@&=+$,%#-]+)+/?)$";
+  return new RegExp(reg).test(str) ? true : false;
+}
+
+export function isFunction(val) {
+  return val && typeof val === "function";
+}
+
+export function isEng(val) {
+  return /^[A-Za-z]+$/.test(val);
+}

+ 393 - 0
src/components/Material/chat.vue

@@ -0,0 +1,393 @@
+<template>
+  <div v-if="type == 'image'">
+
+    <!-- <ul v-for="(item,index) in value" :key="index" class="el-upload-list el-upload-list--picture-card">
+      <li tabindex="0" class="el-upload-list__item is-ready" :style="'width: '+width+'px;height: '+height+'px'">
+        <div>
+          <img :src="item" alt="" class="el-upload-list__item-thumbnail">
+          <span class="el-upload-list__item-actions">
+            <span v-if="index != 0" class="el-upload-list__item-preview" @click="moveMaterial(index,'up')">
+              <i class="el-icon-back" />
+            </span>
+            <span class="el-upload-list__item-preview" @click="zoomMaterial(index)">
+              <i class="el-icon-view" />
+            </span>
+            <span class="el-upload-list__item-delete" @click="deleteMaterial(index)">
+              <i class="el-icon-delete" />
+            </span>
+            <span v-if="index != value.length-1" class="el-upload-list__item-preview" @click="moveMaterial(index,'down')">
+              <i class="el-icon-right" />
+            </span>
+          </span>
+        </div>
+      </li>
+    </ul>
+    <div v-if="num > value.length" tabindex="0" class="el-upload el-upload--picture-card" :style="'width: '+width+'px;height: '+height+'px;'+'line-height:'+height+'px;'" @click="toSeleteMaterial">
+      <i class="el-icon-plus" />
+    </div> -->
+
+
+    <!-- 查看 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="dialogVisible"
+      width="35%">
+       <img :src="url" alt="" style="width: 100%">
+    </el-dialog>
+    <!-- 素材列表 -->
+    <el-dialog
+      title="图片素材库"
+      append-to-body
+      :visible.sync="listDialogVisible"
+      width="70%">
+      <el-container>
+        <el-aside width="unset">
+          <div style="margin-bottom: 10px">
+            <el-button
+              class="el-icon-plus"
+              size="small"
+              @click="materialgroupAdd()">
+              添加分组
+            </el-button>
+          </div>
+          <div class="group-list">
+            <div class="group-item" v-for="(group) in materialGroupList">
+                <el-button  @click="selectGroup(group)" type="primary" plain >{{group.name}}</el-button>
+            </div>
+          </div>
+        </el-aside>
+        <el-main>
+          <el-card>
+            <div slot="header">
+              <el-row>
+                <el-col :span="12">
+                  <span>{{ materialGroup.name }}</span>
+                  <span v-if="materialGroup.groupId >0">
+                    <el-button size="small" type="text" class="el-icon-edit" style="margin-left: 10px;" @click="materialgroupEdit(materialGroup)">重命名</el-button>
+                    <el-button size="small" type="text" class="el-icon-delete" style="margin-left: 10px;color: red" @click="materialgroupDelete(materialGroup)">删除</el-button>
+                  </span>
+                </el-col>
+                <el-col :span="12" style="text-align: right;">
+                  <el-upload
+                    :action="uploadUrl"
+                    :file-list="[]"
+                    :on-progress="handleProgress"
+                    :before-upload="beforeUpload"
+                    :on-success="handleSuccess"
+                    :data="multipartFile"
+                    multiple>
+                    <el-button size="small" type="primary">批量上传</el-button>
+                  </el-upload>
+                </el-col>
+              </el-row>
+            </div>
+            <div v-loading="tableLoading">
+              <el-alert
+                v-if="tableData.length <= 0"
+                title="暂无数据"
+                type="info"
+                :closable="false"
+                center
+                show-icon/>
+              <el-row :gutter="5">
+                <el-checkbox-group v-model="urls" :max="num - value.length">
+                  <el-col v-for="(item,index) in tableData" :key="index" :span="4" >
+                    <el-card :body-style="{ padding: '5px' }">
+                      <el-image
+                        style="width: 100%;height: 100px"
+                        :src="item.url"
+                        fit="contain"
+                        :preview-src-list="[item.url]"
+                        :z-index="9999" />
+                      <div>
+                        <el-checkbox class="material-name" :label="item.url">
+                          选择
+                        </el-checkbox>
+                        <el-row>
+                          <el-col :span="24" class="col-do">
+                            <el-button type="text" size="medium" @click="materialDel(item)">删除</el-button>
+                          </el-col>
+                        </el-row>
+
+                      </div>
+                    </el-card>
+                  </el-col>
+                </el-checkbox-group>
+              </el-row>
+               <pagination
+                  v-show="total>0"
+                  :total="total"
+                  :page.sync="queryParams.pageNum"
+                  :limit.sync="queryParams.pageSize"
+                  @pagination="getMaterialList"/>
+               
+            </div>
+          </el-card>
+        </el-main>
+      </el-container>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="listDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submit">确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listMaterial, getMaterial, delMaterial, addMaterial, updateMaterial, exportMaterial } from "@/api/store/material";
+import { getAllMaterialGroup,listMaterialGroup, getMaterialGroup, delMaterialGroup, addMaterialGroup, updateMaterialGroup, exportMaterialGroup } from "@/api/store/materialGroup";
+export default {
+  name: 'ImageSelect',
+  props: {
+    // 素材数据
+    value: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    // 素材类型
+    type: {
+      type: String
+    },
+    // 素材限制数量,默认5个
+    num: {
+      type: Number,
+      default() {
+        return 5
+      }
+    },
+    // 宽度
+    width: {
+      type: Number,
+      default() {
+        return 150
+      }
+    },
+    // 宽度
+    height: {
+      type: Number,
+      default() {
+        return 150
+      }
+    }
+  },
+  data() {
+    return {
+      multipartFile:{},
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/qwUploadOSS",
+      dialogVisible: false,
+      url: '',
+      listDialogVisible: false,
+      materialGroupList: [],
+      materialGroupLoading: false,
+      materialGroup:{},
+      tableData: [],
+      resultNumber: 0,
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        type: null,
+        groupId: null,
+        name: null,
+        url: null,
+        isDel: null,
+        createUserId: null,
+      },
+      tableLoading: false,
+      urls: []
+    }
+  },
+  mounted(){
+    this.getAllMaterialGroup();
+  },
+  methods: {
+    openMaterial(deviceid){
+       var mpartFile={deviceId:deviceid};
+       this.multipartFile=mpartFile;
+       this.toSeleteMaterial();
+    },
+    selectGroup(item){
+      this.materialGroup=item;
+      this.queryParams.groupId=item.groupId;
+      this.getMaterialList();
+    },
+    materialgroupAdd() {
+      const that = this
+      this.$prompt('请输入分组名', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消'
+      }).then(({ value }) => {
+        addMaterialGroup({
+          name: value
+        }).then(function() {
+          that.materialGroup={};
+          that.getAllMaterialGroup()
+        })
+      }).catch(() => {
+
+      })
+    },
+    materialgroupDelete(materialgroupObj) {
+      const that = this
+      this.$confirm('是否确认删除该分组?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(function() {
+        delMaterialGroup(materialgroupObj.groupId)
+          .then(function() {
+            that.materialGroup={};
+            that.getAllMaterialGroup()
+          })
+      })
+    },
+    materialgroupEdit(materialgroupObj) {
+      const that = this
+      this.$prompt('请输入分组名', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        inputValue: materialgroupObj.name
+      }).then(({ value }) => {
+        updateMaterialGroup({
+          groupId: materialgroupObj.groupId,
+          name: value
+        }).then(function() {
+          that.materialGroup={};
+          that.getAllMaterialGroup()
+        })
+      }).catch(() => {
+
+      })
+    },
+    getAllMaterialGroup() {
+      this.materialGroupLoading = true;
+      getAllMaterialGroup({}).then(response => {
+        this.materialGroupList = response.data
+        console.log(this.materialGroupList)
+        this.materialGroupLoading = false;
+      });
+    },
+    getMaterialList() {
+      this.tableLoading = true;
+      listMaterial(this.queryParams).then(response => {
+        this.tableData = response.rows;
+        this.total = response.total;
+        this.tableLoading = false;
+      });
+    },
+    moveMaterial(index, type) {
+      if (type == 'up') {
+        const tempOption = this.value[index - 1]
+        this.$set(this.value, index - 1, this.value[index])
+        this.$set(this.value, index, tempOption)
+      }
+      if (type == 'down') {
+        const tempOption = this.value[index + 1]
+        this.$set(this.value, index + 1, this.value[index])
+        this.$set(this.value, index, tempOption)
+      }
+    },
+    zoomMaterial(index) {
+      this.dialogVisible = true
+      this.url = this.value[index]
+    },
+    deleteMaterial(index) {
+      const that = this
+      this.$confirm('是否确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(function() {
+        that.value.splice(index, 1)
+        that.urls = []
+      })
+    },
+    toSeleteMaterial() {
+      this.listDialogVisible = true
+      this.getAllMaterialGroup()
+      this.getMaterialList();
+    },
+    materialDel(item) {
+      const that = this
+      this.$confirm('是否确认删除该素材?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(function() {
+        delMaterial(item.id)
+          .then(function() {
+            that.queryParams.pageNum=0;
+            that.getMaterialList();
+          })
+      })
+    },
+    handleProgress(event, file, fileList) {
+    },
+    handleSuccess(response, file, fileList) {
+      const that = this
+      addMaterial({
+        groupId: this.queryParams.groupId,
+        fileName: file.name,
+        url: response.url,
+        data:response.data
+      }).then(() => {
+        this.resultNumber++
+        if (fileList.length === this.resultNumber) {
+          that.getMaterialList()
+          this.resultNumber = 0
+        }
+      })
+    },
+    beforeUpload(file) {
+      const isPic =
+        file.type === 'image/jpeg' ||
+        file.type === 'image/png' ||
+        file.type === 'image/gif' ||
+        file.type === 'image/jpg'
+      const isLt2M = file.size / 1024 / 1024 < 2
+      if (!isPic) {
+        this.$message.error('上传图片只能是 JPG、JPEG、PNG、GIF 格式!')
+        return false
+      }
+      if (!isLt2M) {
+        this.$message.error('上传头像图片大小不能超过 2MB!')
+      }
+      return isPic && isLt2M
+    },
+    submit() {
+      //console.log("qxj urls :"+JSON.stringify(this.urls));
+      //console.log("qxj tableData:"+JSON.stringify(this.tableData));    
+      var data=this.tableData.find(item =>  item.url == this.urls) || {};
+      this.$emit('pickImageData', data);
+      this.listDialogVisible = false;
+      this.urls=[];
+    }
+  }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+  ::v-deep .el-icon-circle-close{
+    color: red;
+  }
+  .material-name{
+    padding: 8px 0px;
+  }
+  .col-do{
+    text-align: center;
+  }
+  .button-do{
+    padding: unset!important;
+    font-size: 12px;
+  }
+  .group-list{
+    display: flex;
+    flex-direction:column;
+    align-items: flex-start;
+  }
+  .group-item{
+    margin: 5px;
+  }
+</style>

+ 4 - 4
src/components/Material/index.vue

@@ -28,8 +28,7 @@
     <el-dialog
       append-to-body
       :visible.sync="dialogVisible"
-      width="35%"
-    >
+      width="35%">
       <img :src="url" alt="" style="width: 100%">
     </el-dialog>
     <!-- 素材列表 -->
@@ -37,8 +36,7 @@
       title="图片素材库"
       append-to-body
       :visible.sync="listDialogVisible"
-      width="70%"
-    >
+      width="70%">
       <el-container>
         <el-aside width="unset">
           <div style="margin-bottom: 10px">
@@ -129,10 +127,12 @@
           </el-card>
         </el-main>
       </el-container>
+
       <span slot="footer" class="dialog-footer">
         <el-button @click="listDialogVisible = false">取 消</el-button>
         <el-button type="primary" @click="submit">确 定</el-button>
       </span>
+      
     </el-dialog>
   </div>
 </template>

+ 758 - 0
src/components/VideoPlayer/VueAliplayer.vue

@@ -0,0 +1,758 @@
+<template>
+  <div class="prism-player" :id="playerId" :style="playStyle">
+      <img  v-show="isShowPlay" @click="play()" :src="playIcon" class="btnPlay" />
+  </div>
+</template>
+
+<script>
+import image from '../LemonUI/components/message/image.vue';
+  export default {
+  components: { image },
+    name: "Aliplayer",
+    props: {
+      source: {
+        type: String,
+        default: ""
+      },
+      //媒体转码服务的媒体Id。
+      vid: {
+        type: String,
+        default: ""
+      },
+      //播放权证
+      playauth: {
+        type: String,
+        default: ""
+      },
+      //容器的大小
+      height: {
+        type: String,
+        default: "320px"
+      },
+      //容器的大小
+      width: {
+        type: String,
+        default: "100%"
+      },
+      //视频的高度大小
+      videoWidth: {
+        type: String,
+        default: "100%"
+      },
+      //视频的宽度大小
+      videoHeight: {
+        type: String,
+        default: "320px"
+      },
+      //播放器自动加载,目前仅h5可用
+      preload: {
+        type: Boolean,
+        default: false
+      },
+      //播放器默认封面图片,请填写正确的图片url地址。需要autoplay为’false’时,才生效
+      cover: {
+        type: String,
+        default: ""
+      },
+      //播放内容是否为直播,直播时会禁止用户拖动进度条。
+      isLive: {
+        type: Boolean,
+        default: false
+      },
+      //播放器是否自动播放,在移动端autoplay属性会失效。
+      autoplay: {
+        type: Boolean,
+        default: false
+      },
+      //播放器自动循环播放。
+      rePlay: {
+        type: Boolean,
+        default: false
+      },
+      //指定使用H5播放器。
+      useH5Prism: {
+        type: Boolean,
+        default: false
+      },
+      //指定使用Flash播放器。
+      useFlashPrism: {
+        type: Boolean,
+        default: false
+      },
+      //H5是否内置播放,有的Android浏览器不起作用。
+      playsinline: {
+        type: Boolean,
+        default: false
+      },
+      //显示播放时缓冲图标,默认true。
+      showBuffer: {
+        type: Boolean,
+        default: true
+      },
+      //URL 皮肤图片,不建议随意修改该字段,如要修改,请参照皮肤定制。
+      skinRes: {
+        type: String,
+        default: ""
+      },
+
+      skinLayout: {
+        type: Array,
+        default: function () {
+          return []
+        }
+      },
+      //默认为‘hover’。可选的值为:‘click’、‘hover’、‘always’。
+      controlBarVisibility: {
+        type: String,
+        default: "hover"
+      },
+      //控制栏自动隐藏时间(ms)
+      showBarTime: {
+        type: String,
+        default: ""
+      },
+      /***
+       * JSON串用于定制性接口参数,目前支持:
+       1.“fullTitle”:“测试页面”
+       全屏时显示视频标题(仅flash支持)。
+       2. “m3u8BufferLength”:“30”
+       播放m3u8时加载缓存ts文件长度单位(秒)(仅flash支持)。
+       3. “liveStartTime”:“2016/08/17 12:00:00”
+       直播开始时间,用于提示直播未开始(仅flash支持)。
+       4. “liveOverTime”:“2016/08/17 14:00:00”
+       直播结束时间,用于提示直播结束(仅flash支持)。
+       */
+      extraInfo: {
+        type: String,
+        default: ""
+      },
+
+      /**
+       *是否允许系统右键菜单显示,默认为false。
+       */
+      enableSystemMenu: {
+        type: Boolean,
+        default: false
+      },
+
+      /***
+       *
+       *指定播放地址格式,只有使用vid的播放方式时支持
+       可选值为’mp4’、’m3u8’、’flv’、’mp3’,默认为空,仅H5支持。
+       * */
+      format: {
+        type: String,
+        default: "mp4"
+      },
+      /***
+       *
+       * 指定返回音频还是视频,只有使用vid的播放方式时支持。
+       可选值为’video’和’audio’,默认为’video’
+       ‘audio’主要是针对只包含音频的视频格式,比如音频的mp4,仅H5支持。
+       * */
+      mediaType: {
+        type: String,
+        default: "video"
+      },
+      /***
+       * 指定排序方式,只有使用vid + plauth播放方式时支持。
+       ‘desc’表示按倒序排序(即:从大到小排序)
+       ‘asc’表示按正序排序(即:从小到大排序)
+       默认值:‘asc’,仅H5支持。
+       * */
+      qualitySort: {
+        type: String,
+        default: "asc"
+      },
+      /***
+       * 显示视频清晰度,多个用逗号分隔,比如:’FD,LD’,此值是vid对应流清晰度的一个子集,
+       取值范围:FD(流畅)LD(标清)SD(高清)HD(超清)OD(原画)2K(2K)4K(4K),仅H5支持。
+       * */
+      definition: {
+        type: String,
+        default: ""
+      },
+      /**
+       * 默认视频清晰度,此值是vid对应流的一个清晰度,
+       取值范围:FD(流畅)LD(标清)SD(高清)HD(超清)OD(原画)2K(2K)4K(4K),仅H5支持。
+       * */
+      defaultDefinition: {
+        type: String,
+        default: ""
+      },
+      /**
+       * 声明启用同层H5播放器,启用时设置的值为‘h5’
+       * */
+      x5_type: {
+        type: String,
+        default: "h5"
+      },
+      /**
+       * 声明视频播放时是否进入到TBS的全屏模式,默认为false。
+       当需要把视频做为背景时,设置为true
+       * */
+      x5_fullscreen: {
+        type: Boolean,
+        default: false
+      },
+      /**
+       * 声明视频播在界面上的位置,默认为“center”。
+       可选值为:“top”,“center”
+       * */
+      x5_video_position: {
+        type: String,
+        default: "center"
+      },
+      /**
+       * 声明 TBS 播放器支持的方向,可选值:
+       landscape:横屏)
+       portraint:竖屏
+       landscape
+       * */
+      x5_orientation: {
+        type: String,
+        default: "portraint"
+      },
+      /**
+       * 声明TBS全屏播放是否横屏,默认值为true。
+       * */
+      x5LandscapeAsFullScreen: {
+        type: String,
+        default: "true"
+      },
+      /**
+       * 延迟播放时间,单位为秒。
+       * */
+      autoPlayDelay: {
+        type: Number,
+        default: 0
+      },
+      /**
+       * 延迟播放提示文本
+       * */
+      autoPlayDelayDisplayText: {
+        type: String,
+        default: "正在转码,请稍后......"
+      },
+      /**
+       * 国际化,默认为‘zh-cn’。
+       如果未设置,则采用浏览器语言。
+       可选值为‘zh-cn’、‘en-us’或其它值。
+       * */
+      language: {
+        type: String,
+        default: "zh-cn"
+      },
+      /**
+       * 自定义国际化文本json结构,key的值需要和language属性值对应起来。
+       例子:{jp:{Play:”Play”}}
+       * */
+      languageTexts: {
+        type: Object,
+        default: function () {
+          return {}
+        }
+      },
+      /**
+       * flash启用截图功能
+       * */
+      snapshot: {
+        type: Boolean,
+        default: false
+      },
+      /**
+       * H5设置截图水印。
+       * */
+      snapshotWatermark: {
+        type: Object,
+        default: function () {
+          return {}
+        }
+      },
+      /**
+       * Safari浏览器可以启用Hls插件播放,Safari 11除外。
+       * */
+      useHlsPluginForSafari: {
+        type: Boolean,
+        default: false
+      },
+      /**
+       * H5播放flv时,设置是否启用播放缓存,只在直播下起作用。
+       * */
+      enableStashBufferForFlv: {
+        type: Boolean,
+        default: false
+      },
+      /**
+       * H5播放flv时,初始缓存大小,只在直播下起作用。
+       * */
+      stashInitialSizeForFlv: {
+        type: Number,
+        default: 10
+      },
+      /**
+       * 缓冲多长时间后,提示用户切换低清晰度,默认:20秒。
+       * */
+      loadDataTimeout: {
+        type: Number,
+        default: 20
+      },
+      /**
+       *最大缓冲超时时间,超过这个时间会有错误提示,默认:60秒。
+       * */
+      waitingTimeout: {
+        type: Number,
+        default: 60
+      },
+      /**
+       * 直播开始时间,直播时移功能使用,格式为:“2018/01/04 12:00:00”。
+       * */
+      liveStartTime: {
+        type: String,
+        default: ""
+      },
+      /**
+       * 直播结束时间,直播时移功能使用,格式为:“2018/01/04 12:00:00”。
+       * */
+      liveOverTime: {
+        type: String,
+        default: ""
+      },
+      /**
+       * 直播可用时移查询地址,详情参见直播时移。
+       * https://help.aliyun.com/document_detail/65129.html?spm=a2c4g.11186623.2.28.3603bf80LR74sS
+       * */
+      liveTimeShiftUrl: {
+        type: String,
+        default: ""
+      },
+      /**
+       * flv直播和hls时移切换是,重新创建播放器方法,详情参见直播时移。
+       * https://help.aliyun.com/document_detail/65129.html?spm=a2c4g.11186623.2.28.3603bf80LR74sS
+       * */
+      recreatePlayer: {
+        type: Function,
+        default: function () {
+        }
+      },
+      /**
+       *是否显示检测按钮,默认为true。
+       * */
+      diagnosisButtonVisible: {
+        type: Boolean,
+        default: true
+      },
+      /**
+       * 禁用进度条的Seek,默认为false,仅Flash支持。
+       * */
+      disableSeek: {
+        type: Boolean,
+        default: false
+      },
+      /**
+       * 加密类型,播放点播私有加密视频时,设置值为1,默认值为0。
+       * */
+      encryptType: {
+        type: Number,
+        default: 0
+      },
+      /**
+       * 进度条打点内容数组,详情参见进度条打点。
+       * [
+       {offset:0,text:'阿里视频云端到云到端服务的重要一环'},
+       {offset:10,text:'除了支持点播和直播的基础播放功能外'},
+       {offset:20,text:'深度融合视频云业务'},
+       {offset:30,text:'为用户提供简单、快速、安全、稳定的视频播放服务'},
+       {offset:40,text:'安装播放器Demo进行体验'},
+       {offset:50,text:'开发人员请点击SDK下载'}
+       ]
+       * https://yq.aliyun.com/articles/686043?spm=a2c4g.11186623.2.31.3603bf80LR74sS
+       * */
+      progressMarkers: {
+        type: Array,
+        default: function () {
+          return []
+        }
+      },
+      /**
+       * 点播失败重试次数,默认3次
+       * */
+      vodRetry: {
+        type: Number,
+        default: 3
+      },
+      /**
+       * 直播播放失败重试次数,默认5次
+       * */
+      liveRetry: {
+        type: Number,
+        default: 5
+      },
+
+      playStyle: {
+        type: String,
+        default: ""
+      },
+      aliplayerSdkPath: {
+        // Aliplayer 代码的路径
+        type: String,
+        default: "//g.alicdn.com/de/prismplayer/2.8.2/aliplayer-min.js"
+      },
+
+
+    },
+    data() {
+      return {
+        playerId: "aliplayer_" + Math.random().toString(36).substr(2),
+        scriptTagStatus: 0,
+        isReload: false,
+        instance: null,
+        isShowPlay:false,
+        playIcon:require('@/assets/image/video.png')
+      };
+    },
+    created() {
+      if (window.Aliplayer !== undefined) {
+        // 如果全局对象存在,说明编辑器代码已经初始化完成,直接加载编辑器
+        this.scriptTagStatus = 2;
+        this.initAliplayer();
+      } else {
+        // 如果全局对象不存在,说明编辑器代码还没有加载完成,需要加载编辑器代码
+        this.insertScriptTag();
+      }
+    },
+    mounted() {
+      if (window.Aliplayer !== undefined) {
+        // 如果全局对象存在,说明编辑器代码已经初始化完成,直接加载编辑器
+        this.scriptTagStatus = 2;
+        this.initAliplayer();
+      } else {
+        // 如果全局对象不存在,说明编辑器代码还没有加载完成,需要加载编辑器代码
+        this.insertScriptTag();
+      }
+    },
+    methods: {
+      insertScriptTag() {
+        const _this = this;
+        let playerScriptTag = document.getElementById("playerScriptTag");
+        // 如果这个tag不存在,则生成相关代码tag以加载代码
+        if (playerScriptTag === null) {
+          playerScriptTag = document.createElement("script");
+          playerScriptTag.type = "text/javascript";
+          playerScriptTag.src = this.aliplayerSdkPath;
+          playerScriptTag.id = "playerScriptTag";
+          let s = document.getElementsByTagName("head")[0];
+          s.appendChild(playerScriptTag);
+        }
+        if (playerScriptTag.loaded) {
+          _this.scriptTagStatus++;
+        } else {
+          playerScriptTag.addEventListener("load", () => {
+            _this.scriptTagStatus++;
+            playerScriptTag.loaded = true;
+            _this.initAliplayer();
+          });
+        }
+        _this.initAliplayer();
+      },
+      initAliplayer() {
+        const _this = this;
+        // scriptTagStatus 为 2 的时候,说明两个必需引入的 js 文件都已经被引入,且加载完成
+        if (
+          _this.scriptTagStatus === 2 &&
+          (_this.instance === null || _this.reloadPlayer)
+        ) {
+          _this.instance && _this.instance.dispose();
+
+          //document.querySelector("#" + _this.playerId).innerHTML = "";
+
+          // Vue 异步执行 DOM 更新,这样一来代码执行到这里的时候可能 template 里面的 script 标签还没真正创建
+          // 所以,我们只能在 nextTick 里面初始化 Aliplayer
+          _this.$nextTick(() => {
+            _this.instance = window.Aliplayer({
+              id: _this.playerId,
+              source: _this.source,
+              vid: _this.vid,
+              playauth: _this.playauth,
+              width: _this.width,
+              height: _this.height,
+              videoWidth: _this.videoWidth,
+              videoHeight: _this.videoHeight,
+              preload: _this.preload,
+              cover: _this.cover,
+              isLive: _this.isLive,
+              autoplay: _this.autoplay,
+              rePlay: _this.rePlay,
+              useH5Prism: _this.useH5Prism,
+              useFlashPrism: _this.useFlashPrism,
+              playsinline: _this.playsinline,
+              showBuffer: _this.showBuffer,
+              skinRes: _this.skinRes,
+              skinLayout: _this.skinLayout,
+              controlBarVisibility: _this.controlBarVisibility,
+              showBarTime: _this.showBarTime,
+              extraInfo: _this.extraInfo,
+              enableSystemMenu: _this.enableSystemMenu,
+              format: _this.format,
+              mediaType: _this.mediaType,
+              qualitySort: _this.qualitySort,
+              definition: _this.definition,
+              defaultDefinition: _this.defaultDefinition,
+              x5_type: _this.x5_type,
+              x5_fullscreen: _this.x5_fullscreen,
+              x5_video_position: _this.x5_video_position,
+              x5_orientation: _this.x5_orientation,
+              x5LandscapeAsFullScreen: _this.x5LandscapeAsFullScreen,
+              autoPlayDelay: _this.autoPlayDelay,
+              autoPlayDelayDisplayText: _this.autoPlayDelayDisplayText,
+              language: _this.language,
+              languageTexts: _this.languageTexts,
+              snapshot: _this.snapshot,
+              snapshotWatermark: _this.snapshotWatermark,
+              useHlsPluginForSafari: _this.useHlsPluginForSafari,
+              enableStashBufferForFlv: _this.enableStashBufferForFlv,
+              stashInitialSizeForFlv: _this.stashInitialSizeForFlv,
+              loadDataTimeout: _this.loadDataTimeout,
+              waitingTimeout: _this.waitingTimeout,
+              liveStartTime: _this.liveStartTime,
+              liveTimeShiftUrl: _this.liveTimeShiftUrl,
+              liveShiftSource: _this.liveShiftSource,
+              recreatePlayer: _this.recreatePlayer,
+              diagnosisButtonVisible: _this.diagnosisButtonVisible,
+              disableSeek: _this.disableSeek,
+              encryptType: _this.encryptType,
+              progressMarkers: _this.progressMarkers,
+              vodRetry: _this.vodRetry,
+              liveRetry: _this.liveRetry,
+            });
+
+            // 绑定事件,当 AliPlayer 初始化完成后,将编辑器实例通过自定义的 ready 事件交出去
+            _this.instance.on("ready", () => {
+              this.$emit("ready", _this.instance);
+            });
+
+            _this.instance.on("play", () => {
+              this.isShowPlay=false;
+              this.$emit("play", _this.instance);
+            });
+
+            _this.instance.on("pause", () => {
+              this.$emit("pause", _this.instance);
+            });
+
+            _this.instance.on("ended", () => {
+              this.isShowPlay=true;
+              this.$emit("ended", _this.instance);
+            });
+
+            _this.instance.on("liveStreamStop", () => {
+              this.$emit("liveStreamStop", _this.instance);
+            });
+
+            _this.instance.on("m3u8Retry", () => {
+              this.$emit("m3u8Retry", _this.instance);
+            });
+
+            _this.instance.on("hideBar", () => {
+              this.$emit("hideBar", _this.instance);
+            });
+
+            _this.instance.on("waiting", () => {
+              this.$emit("waiting", _this.instance);
+            });
+
+            _this.instance.on("snapshoted", () => {
+              this.$emit("snapshoted", _this.instance);
+            });
+
+            _this.instance.on("timeupdate", () => {
+              this.$emit("timeupdate", _this.instance);
+            });
+
+            _this.instance.on("requestFullScreen", () => {
+              this.$emit("requestFullScreen", _this.instance);
+            });
+
+            _this.instance.on("cancelFullScreen", () => {
+              this.$emit("cancelFullScreen", _this.instance);
+            });
+
+            _this.instance.on("error", () => {
+              this.$emit("error", _this.instance);
+            });
+
+            _this.instance.on("startSeek", () => {
+              this.$emit("startSeek", _this.instance);
+            });
+
+            _this.instance.on("completeSeek", () => {
+              this.$emit("completeSeek", _this.instance);
+            });
+          });
+        }
+      },
+      /**
+       * 播放视频
+       */
+      play: function () {
+        this.instance.play();
+      },
+      /**
+       * 暂停视频
+       */
+      pause: function () {
+        this.instance.pause();
+      },
+      /**
+       * 重播视频
+       */
+      replay: function () {
+        this.instance.replay();
+      },
+      /**
+       * 跳转到某个时刻进行播放
+       * @argument time 的单位为秒
+       */
+      seek: function (time) {
+        this.instance.seek(time);
+      },
+      /**
+       * 获取当前时间 单位秒
+       */
+      getCurrentTime: function () {
+        return this.instance.getCurrentTime();
+      },
+      /**
+       *获取视频总时长,返回的单位为秒
+       * @returns 返回的单位为秒
+       */
+      getDuration: function () {
+        return this.instance.getDuration();
+      },
+      /**
+       获取当前的音量,返回值为0-1的实数ios和部分android会失效
+       */
+      getVolume: function () {
+        return this.instance.getVolume();
+      },
+      /**
+       设置音量,vol为0-1的实数,ios和部分android会失效
+       */
+      setVolume: function (vol) {
+        this.instance.setVolume(vol);
+      },
+      /**
+       *直接播放视频url,time为可选值(单位秒)目前只支持同种格式(mp4/flv/m3u8)之间切换暂不支持直播rtmp流切换
+       *@argument url 视频地址
+       *@argument time 跳转到多少秒
+       */
+      loadByUrl: function (url, time) {
+        this.instance.loadByUrl(url, time);
+      },
+      /**
+       * 设置播放速度
+       *@argument speed 速度
+       */
+      setSpeed: function (speed) {
+        this.instance.setSpeed(speed);
+      },
+      /**
+       * 设置播放器大小w,h可分别为400px像素或60%百分比chrome浏览器下flash播放器分别不能小于397x297
+       *@argument w 播放器宽度
+       *@argument h 播放器高度
+       */
+      setPlayerSize: function (w, h) {
+        this.instance.setPlayerSize(w, h);
+      },
+      /**
+       *目前只支持H5播放器。
+       暂不支持不同格式视频间的之间切换。
+       暂不支持直播rtmp流切换。
+       */
+      replayByVidAndPlayAuth: function (vid, accId, accSecret, stsToken, authInfo, domainRegion) {
+        this.instance.replayByVidAndPlayAuth(vid, accId, accSecret, stsToken, authInfo, domainRegion);
+      },
+
+      /***
+       * 重新设置vid和权限,目前只支持H5播放器。
+       暂不支持不同格式视频间的之间切换。
+       暂不支持直播rtmp流切换。
+       * @param vid 视频ID
+       * @param playauth 播放权限
+       */
+      replayByVidAndAuthInfo: function (vid, playauth) {
+        this.instance.replayByVidAndAuthInfo(vid, playauth);
+      },
+
+      /**
+       * 目前只支持HTML5界面上的重载功能,暂不支持直播rtmp流切换m3u8)之间切换,暂不支持直播rtmp流切换
+       *@argument vid 视频id
+       *@argument playauth 播放凭证
+       */
+      reloaduserPlayInfoAndVidRequestMts: function (vid, playauth) {
+        this.instance.reloaduserPlayInfoAndVidRequestMts(vid, playauth);
+      },
+
+      /***
+       *设置截图参数
+       * @param width 宽度
+       * @param height 高度
+       * @param rate 截图质量
+       */
+      setSanpshotProperties: function (width, height, rate) {
+        this.instance.setSanpshotProperties(width, height, rate);
+      },
+      /**
+       * 设置封面地址
+       * @param cover 封面地址
+       */
+      setCover: function (cover) {
+        this.instance.setCover(cover);
+      },
+
+
+      reloadPlayer: function () {
+        this.isReload = true;
+        this.initAliplayer();
+        this.isReload = false;
+      }
+    }
+  };
+</script>
+
+<style>
+  @import url(//g.alicdn.com/de/prismplayer/2.8.2/skins/default/aliplayer-min.css);
+</style>
+
+
+
+<style scoped>
+  .btnPlay {
+    /* width: 40px;
+    height: 40px;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    -webkit-box-pack: center;
+    -ms-flex-pack: center;
+    justify-content: center;
+    z-index: 10;
+    position: absolute;
+    left: calc(50% - 20px);
+    top: calc(50% - 30px); */
+
+    z-index: 10;
+    background: rgba(0,0,0,0);
+
+
+    width: 64px;
+    height: 64px;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    -ms-transform: translate(-50%,-50%);
+    transform: translate(-50%,-50%);
+
+
+}
+</style>

+ 12 - 0
src/main.js

@@ -31,6 +31,18 @@ import 'form-making/dist/FormMaking.css'
 Vue.use(VueClipboard)
 
 Vue.use(FormMaking)
+
+import audio from 'vue-mobile-audio'
+Vue.use(audio)
+
+import LemonIMUI from '@/components/LemonUI';
+Vue.use(LemonIMUI);
+
+import qwIm  from "@/utils/webSocket";
+Vue.prototype.qwIm = qwIm
+
+
+
 // 全局方法挂载
 Vue.prototype.getDicts = getDicts
 Vue.prototype.getConfigKey = getConfigKey

+ 117 - 0
src/utils/WebsocketHeartbeat.js

@@ -0,0 +1,117 @@
+/**
+ * `WebsocketHeartbeat` constructor.
+ *
+ * @param {Object} opts
+ * {
+ *  url                  websocket链接地址
+ *  pingTimeout 未收到消息多少秒之后发送ping请求,默认15000毫秒
+    pongTimeout  发送ping之后,未收到消息超时时间,默认10000毫秒
+    reconnectTimeout
+    pingMsg
+ * }
+ * @api public
+ */
+    function WebsocketHeartbeat({
+        url, 
+        userId,
+        pingTimeout = 15000,
+        pongTimeout = 10000,
+        reconnectTimeout = 2000,
+        pingMsg = 'heartbeat'
+    }){
+        this.opts ={
+            url: url,
+            userId:userId,
+            pingTimeout: pingTimeout,
+            pongTimeout: pongTimeout,
+            reconnectTimeout: reconnectTimeout,
+            pingMsg: pingMsg
+        };
+        this.ws = null;//websocket实例
+    
+        //override hook function
+        this.onclose = () => {};
+        this.onerror = () => {};
+        this.onopen = () => {};
+        this.onmessage = () => {};
+        this.onreconnect = () => {};
+        this.createWebSocket();
+    }
+    WebsocketHeartbeat.prototype.createWebSocket = function(){
+        try {
+            this.ws = new WebSocket(this.opts.url);
+            this.initEventHandle();
+        } catch (e) {
+            this.reconnect();
+            throw e;
+        }     
+    };
+    
+    WebsocketHeartbeat.prototype.initEventHandle = function(){
+        this.ws.onclose = () => {
+            this.onclose();
+            this.reconnect();
+        };
+        this.ws.onerror = () => {
+            this.onerror();
+            this.reconnect();
+        };
+        this.ws.onopen = () => {
+            this.onopen();
+            //心跳检测重置
+            this.heartCheck();
+        };
+        this.ws.onmessage = (event) => {
+            var data=JSON.parse(event.data);
+            console.log("qxj onmessage data:"+JSON.stringify(data));
+            this.onmessage(event);
+            //如果获取到消息,心跳检测重置
+            //拿到任何消息都说明当前连接是正常的
+            this.heartCheck();
+        };
+    };
+    
+    WebsocketHeartbeat.prototype.reconnect = function(){
+        if(this.lockReconnect || this.forbidReconnect) return;
+        this.lockReconnect = true;
+        this.onreconnect();
+        //没连接上会一直重连,设置延迟避免请求过多
+        setTimeout(() => {
+            this.createWebSocket();
+            this.lockReconnect = false;
+        }, this.opts.reconnectTimeout);
+    };
+    WebsocketHeartbeat.prototype.send = function(msg){
+        let msgBean={'cmd':this.opts.pingMsg,'userId':this.opts.userId,'msg':msg};
+        this.ws.send(JSON.stringify(msgBean));
+    };
+    //心跳检测
+    WebsocketHeartbeat.prototype.heartCheck = function(){
+        this.heartReset();
+        this.heartStart();
+    };
+    WebsocketHeartbeat.prototype.heartStart = function(){
+        if(this.forbidReconnect) return;//不再重连就不再执行心跳
+        this.pingTimeoutId = setTimeout(() => {
+            //这里发送一个心跳,后端收到后,返回一个心跳消息,
+            //onmessage拿到返回的心跳就说明连接正常
+            this.send(this.opts.pingMsg);
+            //如果超过一定时间还没重置,说明后端主动断开了
+            this.pongTimeoutId = setTimeout(() => {
+                //如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
+                this.ws.close();
+            }, this.opts.pongTimeout);
+        }, this.opts.pingTimeout);
+    };
+    WebsocketHeartbeat.prototype.heartReset = function(){
+        clearTimeout(this.pingTimeoutId);
+        clearTimeout(this.pongTimeoutId);
+    };
+    WebsocketHeartbeat.prototype.close = function(){
+        //如果手动关闭连接,不再重连
+        this.forbidReconnect = true;
+        this.heartReset();
+        this.ws.close();
+    };
+    if(window) window.WebsocketHeartbeat = WebsocketHeartbeat;
+    export default WebsocketHeartbeat;

+ 49 - 0
src/utils/webSocket.js

@@ -0,0 +1,49 @@
+
+import WebsocketHeartbeat from "./WebsocketHeartbeat";
+
+var server = "";  
+if (process.env.NODE_ENV === 'development') {
+    server = "ws://localhost:7018/imserver/r:";
+}else{
+    server = "wss://im.yjf.runtzh.com/imserver/r:";  
+    //server = "ws://139.186.77.83:7018/imserver/r:";  
+}
+
+export default {
+    socket: {},
+    hasLogin:false,
+    initSocket: function(uid,reset) {
+        if(typeof(WebSocket) === "undefined"){
+            console.log("您的浏览器不支持socket")
+        }else{
+            // 实例化socket
+            if(!this.socket || reset){
+                console.log("实例化socket");
+                //this.socket = new WebSocket(server+uid);
+                this.socket=new WebsocketHeartbeat({
+                    url: server+uid,
+                    userId:'r:'+uid,
+                    pingTimeout: 15000,
+                    pongTimeout: 10000
+                });
+                let that=this;
+                // this.socket.onopen = function () {
+                //     console.log('connect success');
+                //     console.log('send massage: test');
+                //     websocketHeartbeatJs.send('test');
+                //     setTimeout(() => {
+                //         console.log(`wait ${websocketHeartbeatJs.opts.pingTimeout} ms will hava '${websocketHeartbeatJs.opts.pingMsg}'`);
+                //     }, 1500);
+                // }
+
+                // that.$notify.success({
+                //     title: '正在呼叫'+mobile,
+                //     showClose: false
+                // });
+
+                this.hasLogin=true;
+            }
+            
+        }
+    }
+}

+ 2 - 0
src/views/crm/customer/index.vue

@@ -23,6 +23,7 @@
           size="small"
           @keyup.enter.native="handleQuery" />
       </el-form-item>
+
       <el-form-item label="客户名称" prop="customerName">
         <el-input
           v-model="queryParams.customerName"
@@ -69,6 +70,7 @@
               />
         </el-select>
       </el-form-item>
+      
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker clearable size="small" style="width: 205.4px"
           v-model="dateRange"

+ 0 - 0
src/views/qw/components


+ 914 - 0
src/views/qw/qwChat/index.vue

@@ -0,0 +1,914 @@
+<template>
+  <div class="app-container">
+
+     <el-form class="search-form" :inline="true" >
+          <el-form-item label="公司名" prop="companyId">
+                <el-select filterable v-model="companyId" @change="companyChange" placeholder="请选择公司名" clearable size="small">
+                    <el-option
+                      v-for="item in companys"
+                      :key="item.companyId"
+                      :label="item.companyName"
+                      :value="item.companyId"
+                    />
+              </el-select>
+          </el-form-item>
+          <el-form-item >
+            <treeselect :clearable="false"  v-model="deptId"  :options="deptOptions" :show-count="true" placeholder="请选择归属部门" />
+          </el-form-item>
+          <el-form-item>
+              <el-select filterable v-model="userIds" @change="companyUserChange" placeholder="请选择员工" clearable size="small">
+                <el-option
+                  v-for="item in users"
+                  :key="item.userId"
+                  :label="item.nickName"
+                  :value="item.userId">
+                </el-option>
+              </el-select>
+          </el-form-item>
+
+
+         <el-form-item>
+              <el-select filterable v-model="qwUserId"  @change="qwUserChange" placeholder="请选择企微账号" clearable size="small">
+                <el-option
+                  v-for="item in accountList"
+                  :key="item.qwUserId"
+                  :label="item.account"
+                  :value="item.qwUserId">
+                </el-option>
+              </el-select>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+            <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+          </el-form-item>
+
+
+     </el-form>
+
+
+      <div class="imui-center qq-lemon-imui" v-show="showQW">
+        <lemon-imui  class="lemon-slot"
+          :width="windowWidth"
+          :height="windowHeight"
+          :user="UserData"
+          ref="IMUI"  
+          :theme="theme"
+          :hide-menu="hideMenu"
+          :hide-menu-avatar="hideMenuAvatar"
+          :hide-message-name="hideMessageName"
+          :hide-message-time="hideMessageTime"
+          :messageTimeFormat="messageTimeFormat"
+          @change-menu="handleChangeMenu"
+          @pull-messages="handlePullMessages"
+          @change-contact="handleChangeContact"
+          @change-conversation="handleChangeConversation"
+          @message-click="handleMessageClick"
+          @menu-avatar-click="handleMenuAvatarClick"
+          @pick-image="handleImageClick"
+          @pick-msg-records="handleMsgRecordsClick"
+          @send="handleSend">
+          <template #cover>
+                <div class="cover">
+                    <!-- <i class="lemon-icon-message"></i>
+                    <p>
+                        <b>自定义封面 Lemon</b>IMUI
+                    </p> -->
+                </div>
+          </template>
+
+          <template #message-title="contact">
+            <div>
+                <div style="display:flex;justify-content:space-between">
+                    <span>{{contact.displayName}}</span>
+                    <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')">
+                        居中
+                      </span> -->
+                    </span>
+                 </div>
+                  <!-- <div v-if="contact.isGroup" class="slot-group-menu">
+                      <span>聊天</span>
+                      <span>公告</span>
+                      <span>相册</span>
+                      <span>文件</span>
+                      <span>活动</span>
+                      <span>设置(左键弹出菜单)</span> 
+                </div> -->
+            </div>
+          </template>
+
+          <template #sidebar-contact-fixedtop="contact">
+                <div class="slot-contact-fixedtop">
+                  <input class="slot-search" placeholder="搜索通讯录" />
+                </div>
+          </template>
+
+         <template #message-side="contact">
+              <div v-if="contact.isGroup" class="slot-group">
+                    <div class="slot-group-title">群通知</div>
+                    <div class="slot-group-notice">
+                      进群请改备注,格式,工作地点-姓名,请大家配合谢谢
+                    </div>
+                    <div class="slot-group-title">群成员</div>
+                    <div class="slot-group-panel">
+                      <input class="slot-search" placeholder="搜索群成员" />
+                      <div class="slot-group-member">
+                        <div class="avatar"><img src="https://p.qqan.com/up/2020-2/2020022821001845128.jpg"></div>
+                        <div class="name">像梦一样自由</div> 
+                      </div>
+                      <div class="slot-group-member">
+                        <div class="avatar"><img src="https://p.qqan.com/up/2018-4/15244505348390471.jpg"></div>
+                        <div class="name">野火</div> 
+                      </div>
+                      <div class="slot-group-member">
+                        <div class="avatar"><img src="https://p.qqan.com/up/2021-1/20211411391666.jpg"></div>
+                        <div class="name">霸王花</div> 
+                      </div>
+                       <div class="slot-group-member">
+                        <div class="avatar"><img src="https://p.qqan.com/up/2021-1/2021113104111220.jpg"></div>
+                        <div class="name">森系Style</div> 
+                      </div>
+                      
+                    </div>
+               </div>
+          </template>
+
+        </lemon-imui>
+      </div>
+   
+       <el-dialog :visible.sync="dialogVisible" append-to-body width="35%">
+         <img style="width:100%;height:auto" :src="dialogImageUrl" alt="" />
+      </el-dialog>
+
+       <el-dialog :visible.sync="dialogVideoVisible"
+               :close-on-click-modal="false"
+               :destroy-on-close="true"
+               :width="aplayer.pWidth"
+               :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" 
+                :source="dialogVideoUrl" :cover="dialogVideoCover" ref="player">
+              </videoPlayer>
+       </el-dialog>
+
+       <Material ref="material" @pickImageData="handlePickImageDone"  v-model="imageArr" type="image" :num="1" :width="150" :height="150" />
+
+       <ChatRecords ref="chatRecords" :qwUser="qwUser" />
+
+       
+  </div>
+</template>
+
+<script>
+
+import store from "@/store";
+import { getQrCode } from '@/api/qw/login';
+import { getDeviceId,listAccount,getAccount} from '@/api/qw/account';
+import LemonMessageVoice from "@/components/LemonUI/components/message/voice";
+import { getContactsByUser,getConversations,getMessageList,getConversation,sendTextMsg,sendImageMsg,getRoomInfo} from '@/api/qw/im';
+import Conversations from "@/components/LemonUI/database/conversations";
+import EmojiData from "@/components/LemonUI/database/emoji";
+import '@/components/LemonUI/index.css';
+import Material from '@/components/Material/chat'
+import VideoPlayer from '@/components/VideoPlayer/VueAliplayer'
+import ChatRecords from '@/components/LemonUI/components/chatrecords/basic'
+
+import { getUserListByDeptId} from "@/api/company/companyUser";
+import { treeselect } from "@/api/company/companyDept";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import { getCompanyList } from "@/api/company/company";
+
+let pages = {};
+export default {
+  name: "qwChat",
+  components: {   
+    Material,
+    VideoPlayer,
+    ChatRecords,
+    Treeselect
+  },
+  watch: {
+    // 监听deptId
+    'deptId': 'currDeptChange'
+  },
+  data() {
+    return {
+      theme: "default",
+      IMUI:null,
+      hideMenuAvatar: false,
+      hideMenu: false,
+      hideMessageName: false,
+      hideMessageTime: true,
+      accountList:[],
+      qwUser:{
+          "userId":1688854331463712,"avatar":"https://wework.qpic.cn/wwpic/258414_QSUkkN5rTMGzwIc_1692169272/0","corpId":1970326754004217,"corpName":"重庆润方数字科技","nickName":"秦先觉","deviceId":"aeff5e72448d4d299e03d7291a21c2ea","phone":null,"account":"1261818888"
+      },
+      showQW:false,
+      UserData: {
+        id: "1000",
+        displayName: "June",
+        avatar: "https://p.qqan.com/up/2018-4/15244505348390471.jpg",
+      },
+      contactData:null,
+      conversationData:null,
+      isFromLogin:false,
+      dialogImageUrl: '',
+      dialogVisible: false,
+      dialogVideoVisible:false, 
+      dialogVideoUrl:'',
+      dialogVideoCover:'',
+      imageArr:[],
+      pickUploadImgData:null,
+      aplayer: {
+        vid: "bf9b7e4a36d84aea8cee769765fbc28b",
+        pWidth:"1040px",
+        width:"1000px",
+        height:"900px",
+        videoWidth:"1000px",
+        videoHeight:"900px"
+        
+      },
+      player: null,
+      roomMembers:[],
+      roomAdmins:[],
+      roomInfo:null,
+      windowWidth: document.documentElement.clientWidth*0.65,  //实时屏幕宽度
+      windowHeight: document.documentElement.clientHeight*0.7,   //实时屏幕高度
+      queryParams: {
+          pageNum: 1,
+          pageSize: 10,
+          conversationId: null,
+      },
+       companys:[],
+       deptOptions:[],
+       companyId:37,
+       companyUserId:undefined,
+       deptId:undefined,
+       userIds:undefined,
+       qwUserId:undefined,
+       users:[],
+
+    };
+  },
+  created(){
+      getCompanyList().then(response => {
+        this.companys = response.data;
+        if(this.companys!=null&&this.companys.length>0){
+          this.companyId=this.companys[0].companyId;
+          this.getTreeselect();
+        }
+      });
+  },
+  mounted() {
+
+    this.showQW=true;
+    this.setQwUserInfo();
+    this.getContacts();  //获取联系人信息
+    this.getConversation();   //获取会话信息
+   
+    const IMUI = this.$refs.IMUI;
+
+    IMUI.initMenus([
+      {
+        name: "messages",
+      },
+      // {
+      //   name: "contacts",
+      // },
+      // {
+      //   name: "custom1",
+      //   title: "自定义按钮1",
+      //   unread: 0,
+      //   render: menu => {
+      //     return <i class="lemon-icon-attah" />;
+      //   },
+      //   renderContainer: () => {
+      //     return (
+      //       <div class="article">
+      //         <ul>
+      //           <li class="article-item">
+      //             <h2>网红带货:产品真的值得买吗?</h2>
+      //           </li>
+      //           <li class="article-item">
+      //             甘肃夏河县发生5.7级地震 暂未接到人员伤亡报告
+      //           </li>
+      //           <li class="article-item">
+      //             北方多地风力仍强沙尘相伴,东北内蒙古等地迎雨雪
+      //           </li>
+      //           <li class="article-item">
+      //             英货车案:越南警方采集疑死者家属DNA作比对
+      //           </li>
+      //           <li class="article-item">
+      //             知名连锁咖啡店的蛋糕吃出活虫 曝光内幕太震惊
+      //           </li>
+      //         </ul>
+      //         <lemon-contact  props={{ contact: contactData1 }} style="margin:20px"/>
+      //         <lemon-contact props={{ contact: contactData3 }} style="margin:20px" />
+      //       </div>
+      //     );
+      //   },
+      //   isBottom: true,
+      // }
+      
+      ,
+      {
+          name: "custom2",
+          title: "切换账号",
+          unread: 0,
+          click: () => {
+            alert("拦截导航点击事件");
+          },
+          render: menu => {
+            return <i class="lemon-icon-group" />;
+          },
+          isBottom: true,
+       },
+    ]);
+    IMUI.initEmoji(EmojiData); 
+  
+  },
+
+  methods: {
+    companyChange(val){
+      console.log(val);
+      this.companyId=val;
+      this.companyUserId=null;
+      this.getTreeselect();
+    },
+    currDeptChange(val){
+      console.log(val)
+      this.deptId=val;
+      this.companyUserId=null;
+      this.getUserListByDeptId();
+    },
+     /** 查询部门下拉树结构 */
+    getTreeselect() {
+      var that=this;
+      var param={companyId:this.companyId}
+      treeselect(param).then((response) => {
+        this.deptOptions = response.data;
+        console.log(this.deptOptions)
+        if(response.data!=null&&response.data.length>0){
+          this.deptId=response.data[0].id;
+          //that.storeOrder()
+        }
+      });
+    },
+    getUserListByDeptId() {
+        this.userIds=undefined;
+        var data={deptId:this.deptId};
+        getUserListByDeptId(data).then(response => {
+          this.users = response.data;
+        });
+    },
+    companyUserChange(companyUserId){  
+       this.companyUserId=companyUserId;
+       this.qwListAccount();
+    },
+    qwUserChange(qwUserId){
+        let index= this.accountList.findIndex(item => item.qwUserId == qwUserId);
+        this.qwUser=this.accountList[index];
+        this.qwUserId=this.qwUser.qwUserId;
+        this.qwUser.userId=this.qwUserId;
+        this.setQwUserInfo();
+        this.getContacts();  //获取联系人信息
+        this.getConversation();   //获取会话信息 
+    },
+
+     qwListAccount(companyUserId) {
+        if(!this.companyUserId){
+             this.msgError("请选择员工");
+             return;
+        }
+        let queryParams={pageNum: 1, pageSize: 10,companyUserId:this.companyUserId};
+        listAccount(queryParams).then(response => {
+            this.accountList = response.rows;
+            if(this.accountList.length>0){
+                this.qwUser=this.accountList[0];
+                this.qwUserId=this.qwUser.qwUserId;
+                this.qwUser.userId=this.qwUserId;
+                this.setQwUserInfo();
+                this.getContacts();  //获取联系人信息
+                this.getConversation();   //获取会话信息
+            }else{
+                 this.msgError("该员工暂无企微号!");
+            }
+        });
+    },
+    setQwUserInfo(){
+        this.deviceId=this.qwUser.deviceId;
+        this.UserData.id=this.qwUser.userId;
+        this.UserData.displayName=this.qwUser.nickName;
+        this.UserData.avatar=this.qwUser.avatar;
+    },
+
+     /** 搜索按钮操作 */
+    handleQuery() {
+      this.qwListAccount();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      //this.handleQuery();
+    },
+
+
+    initSocket(userId) {
+               let that=this;
+               if(!!this.qwIm.socket){
+                  this.qwIm.initSocket(userId,true);
+                  // 监听socket连接
+                  this.qwIm.socket.onopen = function() {
+                      console.log("socket 连接成功...");
+                  };
+                  this.qwIm.socket.onerror = function() {
+                      console.log("连接错误")
+                  };
+                  // 监听socket消息
+                  const IMUI = this.$refs.IMUI;
+                  this.qwIm.socket.onmessage = function(res) {
+                       var data=JSON.parse(res.data);
+                       //console.log("收到服务端内容", JSON.stringify(data));
+                        if(data.cmd=="heartbeat"){ //收到心跳数据不处理
+                            console.log("heartbeat");
+                        }
+                        if(data.cmd=="offline"){
+                            that.$message.error('企微号已下线');
+                            that.$store.dispatch('qwLoginOut').then(() => {
+
+                            });
+                            setTimeout(() => {
+                               that.$router.push({path:'/qw/qwLogin'});
+                            }, 2000);
+                        }
+                        //接收消息
+                        if(data.cmd=="receiveMsg"){
+                             var msgData=JSON.parse(data.msg);
+                             if(!IMUI.hasConversation(msgData.toContactId)){  //收到新消息时是最新的会话,添加该会话
+                                    let queryParams={conversationId:msgData.toContactId,userId:that.qwUser.userId};
+                                    getConversation(queryParams).then(response => {
+                                          if(response.code==200){
+                                              var conversation=response.data;
+                                              IMUI.appendConversation(conversation); 
+                                              that.appendMessageAction(msgData);
+                                          }
+                                   });
+                             }else{
+                                  that.appendMessageAction(msgData);
+                                  let conversation=IMUI.findConversation(msgData.toContactId);
+                                  IMUI.topPopConversations(conversation);
+                                  
+                             }
+                        }
+                  };
+                  // 监听socket重连
+                  this.qwIm.socket.onreconnect = function(res) {
+                      console.log("socket 断线重连...")
+                  };
+              }
+    },
+    getRoomInfo(roomId){
+        getRoomInfo(roomId).then(response => {
+             this.roomMembers=response.members;
+             this.admins=response.members;
+             this.roomInfo=response.detail;
+        });
+    },
+    getContacts(){  
+      getContactsByUser(this.qwUser.userId).then(response => {
+           this.contactData = response.data;
+           this.$refs.IMUI.initContacts(this.contactData);
+      });
+    },
+    getConversation(){
+      let that=this;
+      const IMUI = this.$refs.IMUI;
+      getConversations(this.qwUser.userId).then(response => {
+           this.conversationData = response.data;
+           IMUI.initConversations(response.data);
+           var fstConversation=this.conversationData[0];
+           if(fstConversation){
+              IMUI.changeContact(fstConversation.conversationId);
+           }
+         
+      });
+    },
+    messageTimeFormat(time) {
+      return this.friendlyDate(time);
+    },
+    changeTheme() {
+       this.theme = this.theme == "default" ? "blue" : "default";
+    },
+    scrollToTop() {
+      document.body.scrollIntoView();
+    },
+    openDrawer(position) {
+            const IMUI = this.$refs.IMUI;
+            const params = {
+                position,
+                render: contact => {
+                return (
+                    <div style="padding:15px">
+                    <h5>{contact.displayName}</h5>
+                    <span style="cursor:pointer;" on-click={IMUI.closeDrawer}>关闭抽屉</span>
+                    </div>
+                );
+                },
+      };
+      if (position == "center") {
+        params.width = "50%";
+        params.height = "50%";
+      } else if (position == "rightInside") {
+        params.height = "90%";
+        params.offsetY = "10%";
+      }
+      this.IMUI.openDrawer(params);
+    },
+    handlePullMessages(contact, next,instance) {
+       const { IMUI } = this.$refs;
+       let isEnd = false;
+       getMessageList(this.queryParams).then(response => {
+          if(response.code==200){
+               isEnd=response.data.isLastPage;
+               next(response.data.list, isEnd);
+               if(!isEnd){
+                   pages[contact.conversationId]++;
+                   this.queryParams.pageNum=pages[contact.conversationId];
+               }
+          }
+      });
+    },
+    handleChangeConversation(conversation, instance) {
+        //console.log("qxj ChangeConversation:"+JSON.stringify(conversation));
+        if (!pages[conversation.conversationId]){
+            pages[conversation.conversationId] =1;
+        }
+       this.queryParams.pageNum=pages[conversation.conversationId];
+       this.queryParams.conversationId=conversation.conversationId;
+       if(conversation.unread>0){
+          conversation.unread=0;
+          instance.updateContact(conversation);
+       }
+       instance.closeDrawer();
+    },
+    handleChangeContact(contact, instance) {
+        
+    },
+    
+    //收到消息后添加消息显示
+    appendMessageAction(msgData){
+          if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){     //文本   text  image  video  voice 
+              const message = {
+                    id: msgData.id,
+                    status: msgData.status,
+                    type: msgData.type,
+                    sendTime: msgData.sendTime,
+                    content: msgData.content,
+                    params1: "1",
+                    params2: "2",
+                    toContactId: msgData.toContactId,
+                    fromUser: msgData.fromUser,
+              };
+              this.appendRemoteMessage(message);
+           }
+           else if(msgData.type=="file"){
+               const message = {
+                    id: msgData.id,
+                    status: msgData.status,
+                    type: msgData.type,
+                    sendTime: msgData.sendTime,
+                    content: msgData.content,
+                    toContactId: msgData.toContactId,
+                    fromUser: msgData.fromUser,
+                    fileName:msgData.fileName,
+                    fileSize:msgData.fileSize
+              };
+              this.appendRemoteMessage(message);
+           }
+           else{
+              const message = {
+                    id: msgData.id,
+                    status: msgData.status,
+                    type: msgData.type,
+                    sendTime: msgData.sendTime,
+                    content: msgData.content,
+                    toContactId: msgData.toContactId,
+                    fromUser: msgData.fromUser,
+              };
+              this.appendRemoteMessage(message);
+           }
+    },
+    //发送消息
+    handleSend(message, next, file) {
+      const IMUI = this.$refs.IMUI;
+      var params={};
+      if(message.type=="text"){  //text   image   voice   video
+          params={"conversationId":message.toContactId,"deviceId":this.qwUser.deviceId,"userId":this.qwUser.userId,"content":message.content};
+          sendTextMsg(params).then(response => {
+              if(response.code==200){
+                  var conversation=IMUI.findConversation(message.toContactId);
+                  conversation.lastSendTime=message.sendTime;
+                  conversation.lastContent=message.content;
+                  IMUI.topPopConversations(conversation);
+                  next();
+              }
+          });
+      }
+      if(message.type=="image"){
+          params={"conversationId":message.toContactId,"deviceId":this.qwUser.deviceId,"userId":this.qwUser.userId,"materialId":this.pickUploadImgData.id};
+          sendImageMsg(params).then(response => {
+              if(response.code==200){
+                  var conversation=IMUI.findConversation(message.toContactId);
+                  conversation.lastSendTime=message.sendTime;
+                  conversation.lastContent="[图片]";
+                  IMUI.topPopConversations(conversation);
+                  next();
+              }
+          });
+      }
+      // setTimeout(() => {
+      //   next();
+      // }, 1000);
+    },
+    handleMenuAvatarClick() {
+      console.log("Event:menu-avatar-click");
+    },
+    //聊天工具栏点击图片
+    handleImageClick() { 
+      //this.$refs.material.openMaterial(this.qwUser.deviceId);
+    },
+    handleMsgRecordsClick() { 
+      console.log("qxj handleMsgRecordsClick");
+      this.$refs.chatRecords.open(this.qwUser,this.queryParams.conversationId,this.conversationData);
+    },
+    //选择图片框确定按钮回调
+    handlePickImageDone(data){
+        console.log("handlePickImageDone:"+JSON.stringify(data));
+        this.pickUploadImgData=data;
+        const IMUI = this.$refs.IMUI;
+        IMUI._handleRemoteImage(data.url);
+    },
+    tooglePlayVideo(data){
+       this.dialogVideoVisible=true;
+       this.dialogVideoUrl=data.content;
+       this.dialogVideoCover=data.url;
+       const player = this.$refs.player.instance
+       player && player.play()
+    },
+    handleMessageClick(e, key, message, instance) {
+      console.log("点击了消息", e, key, message);
+      //console.log("qxj message:"+JSON.stringify(message));
+      if(message.type=="image"){
+           var url=!!message.url?message.url:message.content;
+           this.handlePicturePreview(url);
+      }
+      else if(message.type=="video"){  
+           this.tooglePlayVideo(message);
+      }
+      else if(message.type=="file"){  
+           
+      }
+      if (key == "status") {
+        instance.updateMessage({
+          id: message.id,
+          status: "going",
+          content: "正在重新发送消息...",
+        });
+        setTimeout(() => {
+          instance.updateMessage({
+            id: message.id,
+            status: "succeed",
+            content: "发送成功",
+          });
+        }, 2000);
+      }
+    },
+
+    changeMenuAvatarVisible() {
+      this.hideMenuAvatar = !this.hideMenuAvatar;
+    },
+    changeMenuVisible() {
+      this.hideMenu = !this.hideMenu;
+    },
+    changeMessageNameVisible() {
+      this.hideMessageName = !this.hideMessageName;
+    },
+    changeMessageTimeVisible() {
+      this.hideMessageTime = !this.hideMessageTime;
+    },
+    removeMessage() {
+        const { IMUI } = this.$refs;
+        const messages = IMUI.getCurrentMessages();
+        const id = messages[messages.length - 1].id;
+        if (messages.length > 0) {
+            IMUI.removeMessage(id);
+        }
+    },
+    updateMessage() {
+        const { IMUI } = this.$refs;
+        const messages = IMUI.getCurrentMessages();
+        const message = messages[messages.length - 1];
+        if (messages.length > 0) {
+            const update = {
+            id: message.id,
+            status: "succeed",
+            type: "file",
+            fileName: "被修改成文件了.txt",
+            fileSize: "4200000",
+            };
+            if (message.type == "event") {
+            update.fromUser = this.user;
+            }
+            IMUI.updateMessage(update);
+            IMUI.messageViewToBottom();
+      }
+    },
+    appendCustomMessage() {
+        const { IMUI } = this.$refs;
+        const message = {
+            id: generateRandId(),
+            status: "succeed",
+            type: "voice",
+            sendTime: getTime(),
+            content: "语音消息",
+            params1: "1",
+            params2: "2",
+            toContactId: "contact-1",
+            fromUser: this.user,
+        };
+        IMUI.appendMessage(message, true);
+    },
+    appendMessage() {
+        const { IMUI } = this.$refs;
+        const contact = IMUI.currentContact;
+        const message = generateMessage("contact-3");
+        message.fromUser = {
+            ...message.fromUser,
+            ...this.user,
+        };
+        IMUI.appendMessage(message, true);
+        console.log("🚀 ~ file: App.vue ~ line 1508 ~ appendMessage ~ message", message)
+    },
+    appendEventMessage() {
+      const { IMUI } = this.$refs;
+      const message = {
+        id: generateRandId(),
+        type: "event",
+        content: (
+          <span>
+            邀请你加入群聊{" "}
+            <span
+              style="color:#333;cursor:pointer"
+              on-click={() => alert("OK")}>
+              接受
+            </span>
+          </span>
+        ),
+        toContactId: "contact-3",
+        sendTime: getTime(),
+      };
+      IMUI.appendMessage(message, true);
+    },
+    appendRemoteMessage(message) {  //从服务端返回的消息
+        const { IMUI } = this.$refs;
+        IMUI.appendMessage(message, true);
+    },
+    updateContact() {
+      this.$refs.IMUI.updateContact({
+        id: "contact-3",
+        unread: 10,
+        displayName: generateRandWord(),
+        lastSendTime: getTime(),
+        lastContent: "修改昵称为随机字母",
+      });
+    },
+    handleChangeMenu() {
+      console.log("Event:change-menu");
+    },
+    openCustomContainer() {
+
+    },
+    handlePicturePreview(url) {
+        this.dialogImageUrl = url;
+        this.dialogVisible = true;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+    .imui-center{
+        margin-bottom:"60px"; 
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 80vh;
+    }
+    .lemon-wrapper{
+        border:"1px solid #ddd";
+        height: "60%" !important;
+    }
+    .lemon-drawer{
+        border:"1px solid #ddd";
+        border-left:"0";
+    }
+    .more {
+        font-size: 12px;
+        line-height: 24px;
+        height: 24px;
+        position: absolute;
+        top: 14px;
+        right: 14px;
+        cursor: pointer;
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+        color: #f1f1f1;
+        display: inline-block;
+        border-radius: 4px;
+        background: #111;
+        padding: 0 8px;
+    }
+
+    /deep/.el-dialog__headerbtn{
+         top:10px;
+    }
+
+    /deep/.el-dialog:not(.is-fullscreen) {
+        margin-top: 0 !important;
+    }
+
+    .search-form{
+       margin: 20px 30px 0px 30px;
+    }
+
+    .el-select{
+      margin: 5px 10px;
+    }
+
+    .vue-treeselect{
+      width: 217px;
+      height: 36px;
+    }
+
+  .vue-treeselect__control{
+    display: block;
+   }
+
+
+   /deep/.el-form-item{
+        margin-bottom: 0;
+        vertical-align: middle !important;
+   }
+
+  
+
+    
+</style>
+
+
+<style lang="stylus">
+   .lemon-container__title
+     padding-bottom:10px;
+   .slot-group
+      width:200px;
+      .slot-group-title
+        padding:0 0 10px 0;
+        .slot-group-notice
+           padding: 10px 10px;
+     
+      .slot-search
+           width:calc(100% - 20px) ;
+           margin:5px 10px;
+           padding:3px 0px;
+
+      .slot-group-member
+          display: flex;
+          padding: 5px 0;
+          flex:1;
+          font-size: 14px;
+          align-items: center;
+          .avatar
+              width: 30px;
+              height: 30px;
+              line-height: 30px;
+              margin-right:5px;
+              img 
+                vertical-align: middle;
+                border-style: none;
+                width: 100%;
+                height: 100%;
+                line-height: 30px;
+                border-radius: 50%;
+</style>

+ 3 - 0
src/views/store/components/productOrder.vue

@@ -322,6 +322,7 @@
       </el-table>
       </el-card>
     </div>
+
     <el-dialog :title="edit.title" :visible.sync="edit.open" width="600px" append-to-body>
       <el-form ref="editForm" :model="editForm" :rules="editRules" label-width="100px">
         <el-form-item label="订单类型" prop="orderType"  >
@@ -406,6 +407,7 @@
         </el-table-column>
       </el-table>
     </el-dialog>
+
     <el-dialog :title="erpDialog.title" :visible.sync="erpDialog.open" width="600px" append-to-body>
       <div v-if="order!=null&&order.extendOrderId!=null&&order.status==1"  v-hasPermi="['store:storeOrder:updateExpress']"  >
         <el-button size="mini" @click="updateExpress()" >同步物流发货</el-button>
@@ -454,6 +456,7 @@
         </el-row>
       </div>
     </el-dialog>
+    
   </div>
 </template>
 

+ 2 - 0
src/views/store/statistics/storeOrder.vue

@@ -38,6 +38,8 @@
                       </el-option>
                     </el-select>
                 </el-form-item>
+
+                
                 <el-form-item label="筛选日期" prop="createTime">
                   <el-date-picker clearable size="small" style="width: 205.4px"
                     v-model="dateRange"

+ 2 - 7
src/views/store/storeOrder/index.vue

@@ -239,7 +239,6 @@
                   <div class="sku">{{ JSON.parse(item.jsonInfo).sku}}</div>
                   <div class="price">¥{{JSON.parse(item.jsonInfo).price}}×{{item.num}}</div>
                 </div>
-                
               </div>
           </template>
       </el-table-column> -->
@@ -264,7 +263,7 @@
       </el-table-column>
       <el-table-column label="订单类型" align="center" prop="orderType" >
           <template slot-scope="scope">
-              <el-tag prop="status" v-for="(item, index) in orderTypeOptions"    v-if="scope.row.orderType==item.dictValue">{{item.dictLabel}}</el-tag>
+              <el-tag prop="orderType" v-for="(item, index) in orderTypeOptions"    v-if="scope.row.orderType==item.dictValue">{{item.dictLabel}}</el-tag>
           </template>
       </el-table-column>
       <el-table-column label="状态" align="center" prop="status" >
@@ -308,14 +307,10 @@
       :limit.sync="queryParams.pageSize"
       @pagination="getList"
     />
-    <el-drawer
-     size="75%"
-      :title="show.title" :visible.sync="show.open"
-      >
+    <el-drawer size="75%" :title="show.title" :visible.sync="show.open">
       <product-order  ref="order" />
     </el-drawer>
 
-
      <el-dialog :title="title" v-if="open" :visible.sync="open" width="1000px" append-to-body>
         <el-form ref="form" :model="form" :rules="rules" label-width="120px">
            <el-form-item label="会员信息" prop="userId">

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio