vue 聊天實現 quill-image-drop-module上傳文件到後端

  1. 個人代碼,該文章只有聊天窗口主體,其它部份的組件都比較簡單就不發出來了。
  2. 個人覺得比較難的地方是將富文本的圖片base64轉成文件傳給後端。
  3. 這個不是教學文章,只是提供一個思路,所以懶得解釋那麼多了。反正我自己是肯定能看懂的
    在這裏插入圖片描述
 <template>
  <div class="edit_container" style="">
    <div class="header">
      <div class="title" style="position:relative">
        <span>
          <i class="iconfont iconpindaofangjian"></i>
          {{quillTitle}}-聊天室
        </span>
        <i class="iconfont iconyoucedaohang-tongxunlu _btn-suspend biankuang"
           @click="popoverMembers=true"/>
        <transition name="el-zoom-in-top">>
          <div class="members" style="" v-show="popoverMembers">
            <div class="members-title" >
              <span>
                <i class="iconfont iconyoucedaohang-tongxunlu"
                   style="border: 0px solid rgba(242, 242, 244, 1);"></i>
                房間成員
              </span>
              <span style="float: right">
                <i class="iconfont iconguanbi _btn-suspend" @click="popoverMembers=false"></i>
              </span>
            </div>
            <div style="height: 1px;width: 360px;background-color: #EDEDED"></div>
            <div class="members-main">
              <span class="members-count">
                房間人數{{members.length}}
              </span>
              <div class="members-users _btn-suspend">
                <div v-for="(medium,index) in members" :key="index"
                     class="members-user"
                     @click="onBusinessCard(medium.ID)">
                  <!--頭像-->
                  <img
                        class="icon-size "
                        :src=" $store.state.business[medium.Business].icon "/>
                  <div :style="medium.Light?'-webkit-filter: grayscale(100%);':''">
                    <el-avatar
                      size="medium"
                      :src="medium.PhotoUrl"
                      slot="reference"/>
                  </div>
                  <span>
                    {{medium.Name}}
                  </span>
                </div>
              </div>
            </div>
          </div>
        </transition>
      </div>
    </div>
    <div class="dialog-box">
      <el-scrollbar wrapStyle="overflow-x: hidden;" ref="myScrollbar">
        <div v-for="(message,index) in messageList"
             :key="index"
             :id="'msg'+message.Id">
          <div class="time">
            <span v-html="message.TimeHtml"></span>
          </div>
          <div class="message"
               :class="isMy(message.Sender.ID)?'my-message':'opposite-message'">
            <!--頭像-->
            <span @click="onBusinessCard(message.Sender.ID)" class="_btn-suspend">
              <el-avatar
                size="medium"
                :src="message.Sender.FullPhotoUrl"
                slot="reference"/>
            </span>
            <div class="message-main">
              <span class="username" v-if="!isMy(message.Sender.ID)">
                {{message.Sender.RealName}}
                <el-image :src="$store.state.business[message.Sender.Business].icon" class="_icon" fit="cover"/>
                {{$store.state.business[message.Sender.Business].title}}
              </span>
              <!--聊天內容-->
              <div v-html="message.Content" :class="message.Content.includes('img')?'imgMessage':'text'"/>
            </div>

          </div>
        </div>
        <p id="bottom"/>
      </el-scrollbar>
      <!--個人信息-->
      <BusinessCard :id="businessCardUserId"
                    :visible="businessCardBisible"
                    @close="businessCardBisible=false"/>

    </div>
    <div class="inputWindow">
      <div
        v-loading="imageLoading"
        element-loading-text="請稍等,圖片上傳中">

        <quill-editor
          v-model="content"
          ref="myQuillEditor"
          :options="editorOption"
          @blur="onEditorBlur($event)"
          @focus="onEditorFocus($event)"
          @change="onEditorChange($event)">
        </quill-editor>
        <input style="display: none"
               id="imgInput"
               type="file"
               name="avator"
               multiple accept="image/jpg,image/jpeg,image/png,image/gif"
               @change="handleUpload">
      </div>

      <div class="footer">
        <span>Enter 發送,Ctrl+Shift 換行</span>
        &nbsp;&nbsp;
        <button @click="saveHtml" class="event">發送</button>
      </div>
    </div>
  </div>
</template>

<script>

  import * as Quill from 'quill' //引入編輯器
  import BusinessCard from './BusinessCard' //引入編輯器
  import {ImageDrop} from 'quill-image-drop-module';

  Quill.register('modules/imageDrop', ImageDrop);

  /*['bold', 'italic', 'underline', 'strike'],    //加粗,斜體,下劃線,刪除線
    ['blockquote', 'code-block'],     //引用,代碼塊
    [{ 'header': 1 }, { 'header': 2 }],        // 標題,鍵值對的形式;1、2表示字體大小
    [{ 'list': 'ordered'}, { 'list': 'bullet' }],     //列表
    [{ 'script': 'sub'}, { 'script': 'super' }],   // 上下標
    [{ 'indent': '-1'}, { 'indent': '+1' }],     // 縮進
    [{ 'direction': 'rtl' }],             // 文本方向
    [{ 'size': ['small', false, 'large', 'huge'] }], // 字體大小
    [{ 'header': [1, 2, 3, 4, 5, 6, false] }],     //幾級標題
    [{ 'color': [] }, { 'background': [] }],     // 字體顏色,字體背景顏色
    [{ 'font': [] }],     //字體
    [{ 'align': [] }],    //對齊方式
    ['clean'],    //清除字體樣式
    ['image','video']    //上傳圖片、上傳視頻*/
  export default {
    name: "Quill",
    components: {
      BusinessCard
    },
    props: {
      strMessageMap: {
        type: String
      },
      id: {
        type: Number
      },
      quillTitle: {
        type: String,
        default: ""
      }
    },
    data() {
      return {
        content: ``,
        editorOption: {
          modules: {
            imageDrop: false,
            toolbar: [
              ['image']
            ]
          },
          theme: 'snow',
          placeholder: '',
          scrollTop: 0,
        },
        messageList: [],
        Sender: {},
        businessCardBisible: false,
        businessCardUserId: 0,
        userID: this.$store.state.userInfo.ID,
        scrollbarEl: {},
        /*時間標記*/
        timeSign: 0,
        addImgRange: {},
        imageLoading: false,
        /*聊天域對象*/
        domain:{},
        /*聊天成員*/
        members:{},
        popoverMembers:false,
      }
    },
    watch: {
      id(val) {
        this.messageList = []
        this.domainInit()
      },
      //監聽的變量名發生改變後觸發
      messageMap(val) {
        let scrollbarEl = this.scrollbarEl
        this.getNewMessage().then(res => {
          if (scrollbarEl.scrollHeight < (scrollbarEl.scrollTop + scrollbarEl.clientHeight) + 1080) {
            this.bottomScrollIntoView()
          }
        })
      },
    },
    computed: {
      messageMap() {
        //監聽傳進來的vuex變量名
        return this.$store.state[this.strMessageMap]
      },
      editor() {
        return this.$refs.myQuillEditor.quill
      },
    },
    mounted() {
      /*滾動到頂部加載數據*/
      this.handleScroll()
      //綁定回車鍵事件
      this.editor.keyboard.bindings[13].splice(0, 0, {
        key: 13, handler: () => {
          this.saveHtml()
        }
      })
      let _this = this
      let imgHandler = async function (image) {
        this.addImgRange = _this.editor.getSelection()
        if (image) {
          let fileInput = document.getElementById("imgInput") //隱藏的file文本ID
          fileInput.click() //加一個觸發事件
        }
      }
      this.editor.getModule("toolbar").addHandler("image", imgHandler)
      this.domainInit()
    },
    methods: {
      /*平滑滾動到底部*/
      bottomScrollIntoView(){
        /*scrollTop爲滾動條在Y軸上的滾動距離。clientHeight爲內容可視區域的高度。scrollHeight爲內容可視區域的高度加上溢出(滾動)的距離。*/
        setTimeout(()=>{
          /*平滑滾動*/
          document.querySelector("#bottom").scrollIntoView({behavior:"smooth"})
        },50)
      },
      /*初始化domain*/
      domainInit(){
        let r = (res)=>{
          this.domain=res
          /*獲取房間人數*/
          this.$IChatProxy.GetChatMembers(res.ID,res=>{
            this.members=res
            console.log('成員')
            console.log(res.members)
          })
          this.getMessageArray()
        }
        if (this.strMessageMap === 'houseMessageMap') {
          this.$IChatProxy.GetChatDomainByHouse(this.id,res=>r(res))
        } else if (this.strMessageMap === 'roomMessageMap') {
          this.$IChatProxy.GetChatDomainByRoom(this.id,res=>r(res))
        } else if (this.strMessageMap === 'channelIDMessageMap') {
          this.$IChatProxy.GetChatDomainByChannel(this.id,res=>r(res))
        } else if (this.strMessageMap === 'userMessageMap') {
          this.$IChatProxy.GetChatDomainByUser(this.id,res=>r(res))
        }

      },
      handleUpload(e) {
        let files = e.target.files;
        if (files.length == 0) return;
        let pic_list = []
        Object.keys(files).forEach(key => {
        let isLt20M = files[key].size / 1024 / 1024 < 20;
        if (!isLt20M) {
          this.$message.warning('上傳圖片大小不能超過 20M!');
          return;
        }
          pic_list.push(files[key])
        });
        this.imageLoading = true
        try {
          pic_list.forEach(file => {
           this.uploadImgReq(file).then(res=>{
             /*做圖文混編時使用*/
             /*let url = res.data
             this.addImgRange = this.editor.getSelection()
             url = url.indexOf('http') != -1 ? url : 'http:' + url
             this.editor.insertEmbed(this.addImgRange != null ? this.addImgRange.index : 0,
               'image',
               url,
               Quill.sources.USER)*/
            })
          })
        } catch (e) {
          this.$message.warning("圖片上傳失敗")
        }
        this.imageLoading = false
      },
      uploadImgReq(file) {
        // 這裏實現你自己的圖片上傳
        return new Promise((resolve, reject) => {
          if (true) {
            //創建form對象
            let param = new FormData();
            //通過append向form對象添加數據
            param.append('chatDomainID',this.domain.ID);
            param.append('image',file);
            let config  = {
              headers:{
                'AccessToken': localStorage.getItem('token')
              }
            }
            this.$axios.post(`${this.$IChatProxy.url}31/SendImageMessage`,param,config).then(res=>{
              resolve(res)
            })
          } else {
            reject({message: '圖片上傳失敗'})
          }
        })
      },
      showText(timeString) {
        let date = new Date(timeString)
        let hh = date.getHours()
        let mm = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
        let str = timeString;
        let today = new Date()
        today.setHours(0)
        today.setMinutes(0)
        today.setSeconds(0)
        // today 爲今天凌晨的時間
        let dayTime = 24 * 60 * 60 * 1000
        let delta = today - date // 得到相差的時間 ms
        if (delta > 0) {
          if (delta <= dayTime) {
            str = '昨天&nbsp;&nbsp;' + hh + ':' + mm
          } else if (delta <= 2 * dayTime) {
            str = '前天&nbsp;&nbsp;' + hh + ':' + mm
          }
        } else if (-delta < dayTime) {
          str = hh + ':' + mm
        }
        if (this.timeSign == 0 || (date.getMinutes() - this.timeSign > 5 || date.getMinutes() - this.timeSign < 0)) {
          this.timeSign = date.getMinutes()
          return str
        } else {
          return ''
        }
      },
      /*滾動到頂部加載新數據*/
      handleScroll() {
        this.scrollbarEl = this.$refs.myScrollbar.wrap
        this.scrollbarEl.onscroll = () => {
          if (this.scrollbarEl.scrollTop == 0 && this.messageList.length > 0) {
            this.getMessageArray(this.messageList[0].Id)
          }
        }
      },
      /*打開個人信息*/
      onBusinessCard(id) {
        this.businessCardBisible = !this.businessCardBisible
        this.businessCardUserId = id
      },
      /*判斷是否爲自己發出的數據*/
      isMy(id) {
        return id == this.userID
      },
      /*獲取消息*/
      getMessageArray(startId = 0) {
        let addmessage = (res) => {
          if (res.length == 0) return //this.$message.warning('已經沒有歷史消息了')
          this.messageList.push()
          let data = this.megProcessing(res)
          this.messageList.splice(0, 0, ...data)
          this.$nextTick(() => {
            if(startId){
              document.querySelector("#msg" + startId).scrollIntoView(true)
            }else{
              document.querySelector("#bottom").scrollIntoView(true)
              // this.bottomScrollIntoView()
            }
          })
        }
        this.$IChatProxy.GetChatWithDomain(this.domain.ID, startId, res => {
          addmessage(res)
        })
      },
      /*獲取新消息*/
      getNewMessage() {
        return new Promise(resolve => {
          this.$IChatProxy.GetChatWithDomain(this.domain.ID, -1, res => {
            let data = this.megProcessing(res)
            this.messageList.push(...data)
            this.$nextTick(() => {
              resolve(res)
            })
          })
        })
      },
      /*數據處理*/
      megProcessing(res) {
        for (let message of res) {
          message.Content = decodeURIComponent(message.Content)
          message.TimeHtml = this.showText(message.Time)
        }
        return res
      },
      onEditorReady(editor) { // 準備編輯器
      },
      // 失去焦點事件
      onEditorBlur() {
      },
      // 獲得焦點事件
      onEditorFocus() {
      },
      // 內容改變事件
      onEditorChange(val) {
        // console.log(val); // 富文本獲得焦點時的內容
        //editor.enable(false); // 在獲取焦點的時候禁用
      },
      saveHtml: function (event) {
        if (this.content.length <= 0) return
        this.content = encodeURIComponent(this.content)
        this.$IChatProxy.SendTextMessage(this.domain.ID,this.content,res=>{
        })
        this.content = ""
      }
    }
  }
</script>

<style scoped lang="scss">
  .el-popup-parent--hidden /deep/ .quill-popover {
    background-color: #2c3e50;
  }

  .dialog-box /deep/ .el-scrollbar {
    background: #F3F3F7;
    border:1px solid #EEEDF0;
    box-shadow: 0px -1px 0px 0px rgba(237, 237, 237, 1);
    height: 100%;
    width: 100%;
  }

  .el-scrollbar-main /deep/ .el-scrollbar__wrap {
    width: 110%;
    height: 110%;
  }

  .edit_container {
    height: auto;
    width: 100%;
    text-align: left;

    .header {
      background: rgba(255, 255, 255, 1);
      border: 1px solid rgba(237, 237, 237, 1);
      box-shadow: 0px -1px 0px 0px rgba(237, 237, 237, 1);
      border-radius: 6px 6px 0px 0px;
      display: flex;
      align-items: center;
      padding: 0 10px;
      height: 52px;

      .title {
        width: 100%;
        display: flex;
        justify-content: space-between;
        font-weight: bold;
        color: $color-text;
        line-height: 14px;

        .iconpindaofangjian:before {
          color: $color-primary;
        }
        .iconyoucedaohang-tongxunlu:before {
          font-size: 22px;
          padding: 4px;
          color: $color-primary;
          border-radius: 50%;
        }
        .biankuang:before{
          border: 1px solid rgba(242, 242, 244, 1);
        }
        .iconguanbi:before{
          border-radius: 50%;
          font-size: 22px;
          padding: 4px;
          border: 1px solid rgba(242, 242, 244, 1);
          color: $color-shadow;
        }

      }
      .members{
        border-radius:6px;
        box-shadow:0px 2px 8px 0px rgba(0, 0, 0, 0.06);
        border:1px solid rgba(237, 237, 237, 1);
        position:absolute;
        z-index: 100;
        right: -10px;
        top: -18px;
        background-color: $color-popup;
        display: flex;
        width: 360px;
        height: 480px;
        flex-direction:column;
        .members-title{
          line-height:25px;
          padding: 11px 10px 12px 10px;
        }
        .members-main{
          padding: 20px 10px;
          .members-count{
            color: $color-text;
            font-weight:400;
          }
          /*用戶組*/
          .members-users{
            display: flex;
            flex-wrap:wrap;

            /*用戶*/
            .members-user{
              width: 48px;
              padding: 10px 10px;
              font-size: 12px;
              text-align: center;
              position: relative;
                .icon-size {
                position: absolute;
                width:15px;
                height:15px;
                right: 12px;
                top: 5px;
              }
            }
          }
        }
        div{
          line-height:15px;
        }
      }
      .btn-users {
        width: 36px;
        height: 36px;
        background: rgba(255, 255, 255, 1);
        border: 1px solid rgba(242, 242, 244, 1);
        border-radius: 50%;
      }
    }

    /*對話窗口*/
    .dialog-box {
      height: 450px;
      width: 100%;

      ._icon {
        width: 15px;
        margin-left: 5px;
      }

      /*其他人的消息*/
      .opposite-message {
        display: flex;
        flex-direction: row;
        /*信息主體*/
        .message-main {
          text-align: left;
        }

        .text {
          background: rgba(255, 255, 255, 1);
          border-radius: 2px 10px 10px 10px;
          font-weight: 400;
          color: rgba(20, 35, 56, 1);
        }
      }

      .time {
        display: flex;
        justify-content: center;
        color: $color-shadow;
      }

      /*我的消息*/
      .my-message {
        display: flex;
        flex-direction: row-reverse;
        /*信息主體*/
        .message-main {
          text-align: right;
        }
        .username {
          justify-content: flex-end;
        }
        .text {

          border-radius: 10px 2px 10px 10px;
          background: rgba(1, 123, 255, 1);
          font-weight: 400;
          color: rgba(242, 242, 244, 1);
        }
      }

      .message {
        padding: 10px 20px;
        .message-main {
          .username {
            display: flex;
            align-items: center;
            margin: 0 13px 5px 13px;
            font-size: 12px;
            color: $color-font;
          }
        }
        /deep/ p {
          margin: 0;
          line-height: 1;
        }
        .imgMessage{
          /deep/ img {
            margin: 5px 10px;
            max-width: 40%;
            border-radius: 10px;
          }
        }
        .text {
          display:inline-block !important;
          margin: 0 13px;
          /*max-width: 80%;*/
          padding: 12px 15px;
        }

      }

    }

    /*輸入窗口*/
    .inputWindow {
      height: 170px;
      background: rgba(255, 255, 255, 1);
      border: 1px solid rgba(237, 237, 237, 1);
      box-shadow: 0px -1px 0px 0px rgba(237, 237, 237, 1);
      border-radius: 0px 0px 6px 6px;
      /*富文本工具欄*/
      /deep/ .ql-toolbar {
        padding: 2px;
        height: 30px;

        /deep/ .ql-formats {
          display: flex;
        }
      }

      /*輸入框*/
      /deep/ .ql-container {
        height: 100px;
        /deep/ img {
          height: 80px;
        }
      }

      /deep/ .ql-snow {
        border: 0px solid #ccc;
      }

      /deep/ .ql-editor {
        padding: 5px;
      }
    }


    /*底部*/
    .footer {
      display: flex;
      justify-content: flex-end;
      align-items: center;

      .event {
        width: 75px;
        height: 32px;
        background: rgba(255, 255, 255, 1);
        border: 1px solid rgba(237, 237, 237, 1);
        border-radius: 6px;
        margin-right: 20px;
      }

      span {
        font-size: 12px;
        font-family: Microsoft YaHei;
        font-weight: 400;
        color: rgba(171, 172, 176, 1);
        line-height: 2px;
      }
    }

    /*漢化*/
    .editor {
      line-height: normal !important;
      height: 800px;
    }

    .ql-snow .ql-tooltip[data-mode=link]::before {
      content: "請輸入鏈接地址:";
    }

    .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
      border-right: 0px;
      content: '保存';
      padding-right: 0px;
    }

    .ql-snow .ql-tooltip[data-mode=video]::before {
      content: "請輸入視頻地址:";
    }

    .ql-snow .ql-picker.ql-size .ql-picker-label::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item::before {
      content: '14px';
    }

    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
      content: '10px';
    }

    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
      content: '18px';
    }

    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
      content: '32px';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item::before {
      content: '文本';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
      content: '標題1';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
      content: '標題2';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
      content: '標題3';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
      content: '標題4';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
      content: '標題5';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
      content: '標題6';
    }

    .ql-snow .ql-picker.ql-font .ql-picker-label::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item::before {
      content: '標準字體';
    }

    .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
      content: '襯線字體';
    }

    .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
      content: '等寬字體';
    }

    .ql-align-center {
      text-align: center;
    }

    .ql-align-right {
      text-align: right;
    }

    .ql-align-left {
      text-align: left;
    }
  }


</style>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章