開(kāi)發(fā)一個(gè)在線(xiàn)聊天

在線(xiàn)聊天技術(shù)選型

在線(xiàn)聊天因?yàn)樯婕暗交ハ嗤ㄐ牛圆捎胹ocket.io

前端框架 vue2

打包工具 vite

在線(xiàn)gitee地址: https://gitee.com/service-chat/service-chat

整體架構(gòu)

初始化之后的效果如下:

init 初始化

init 主要是從url參數(shù)中獲取用戶(hù)的id贼陶,然后調(diào)用signalrService

    // 初始化
    init() {
      this.sender.id = parseInt(this.$route.query.sendId);
      if (!(this.sender.id > 0)) {
        alert("請(qǐng)?zhí)砑觭endId參數(shù)");
        return false;
      }
      // 當(dāng)前產(chǎn)品
      let product = this.$store.state.productList.filter(
        (x) => x.Id === this.$route.query.productId
      );
      if (product.length > 0) {
        // 卡片信息內(nèi)容
        this.browseCard.Id = product[0].Id;
        this.browseCard.Name = product[0].Name;
        this.browseCard.ShortDescription = product[0].ShortDescription;
        this.browseCard.DefaultPictureUrl = product[0].DefaultPictureUrl;
        this.browseCard.Amount = "編碼:" + product[0].ProductCode;
        this.browseCard.Type = 1;
      }
      // 當(dāng)前用戶(hù)
      let userInfo = this.$store.state.userList.filter(
        (x) => x.id == this.sender.id
      )[0];
      // 快速回復(fù)
      this.fastReplay = this.$store.state.fastReply;
      if (userInfo) {
        this.sender.name = userInfo.name;
        // 修改昵稱(chēng)時(shí)的臨時(shí)記錄昵稱(chēng)
        this.temporaryUserName = userInfo.name;
        this.sender.isService = userInfo.isService;
        this.sender.receptNum = userInfo.receptNum;
        // 修改接待用戶(hù)數(shù)量時(shí)的臨時(shí)記錄接待用戶(hù)數(shù)量
        this.temporaryReceptNumber = userInfo.receptNum;
      } else {
        alert("請(qǐng)保證sendId參數(shù)在userList.json文件中存在");
        return false;
      }
      // 發(fā)送歡迎語(yǔ)
      let welCome = this.$store.state.robotReply.filter(
        (x) => x.Answer.indexOf("歡迎語(yǔ)") !== -1
      );
      if (welCome.length > 0) {
        this.signalrService(welCome[0], 1, 4, false);
      }
    },

signalrService

當(dāng)初次初始化的時(shí)候,只是把當(dāng)前的內(nèi)容發(fā)送到當(dāng)前會(huì)話(huà)內(nèi)容里邊去鸠澈。

// 1.信息組裝
// 發(fā)送者身份:0 機(jī)器人顿苇,1 客服員巷挥,2.會(huì)員
// 信息類(lèi)型 :0 文本蜂怎,1 圖片楼吃,2 表情,3 商品卡片/訂單卡片亲轨,4 機(jī)器人回復(fù)
    signalrService(
      content,
      identity,
      type,
      isSendOther = true,
      isRobot = false
    ) {
      // 發(fā)送信息
      if (this.sendState) {
        let createDate = this.nowTime();
        let noCode = +new Date();
        this.infoTemplate = {
          SendId: this.sender.id,
          ReviceId: isRobot ? 0 : this.revicer.id,
          Content: content,
          Identity: identity,
          Type: type,
          State: isRobot || !this.sender.onlineState ? 1 : 0,
          // 發(fā)送時(shí)間戳
          NoCode: noCode,
          OutTradeNo: this.revicer.outTradeNo,
          CreateDateUtc: createDate,
          Title: null,
          Description: null,
          Label: null,
          Thumbnail: null,
          NoSend: true,
        };
        // 發(fā)送到當(dāng)前會(huì)員內(nèi)容里邊中
        this.toSendInfo(this.infoTemplate);
        if (isSendOther) {
          this.sendMsg(this.infoTemplate);
        }
        this.sendState = isRobot || !this.sender.onlineState ? true : false;
        this.sendInfo = type === 2 ? this.sendInfo : "";
        this.toBottom(100);
      } else {
        this.showMsg("發(fā)送太快啦趋惨,請(qǐng)稍后再試");
      }
    }

和機(jī)器人對(duì)話(huà)

如果客服是機(jī)器人的話(huà),用戶(hù)依然可以發(fā)送一些信息給機(jī)器人惦蚊,比如發(fā)送一些信息器虾,效果如下:

當(dāng)然也可以點(diǎn)擊機(jī)器人發(fā)送過(guò)來(lái)的信息,比如查看如何操作退款蹦锋,如何操作提貨等

發(fā)送信息給機(jī)器人

可以和機(jī)器人聊天兆沙,可以把一些用戶(hù)常見(jiàn)的問(wèn)題,形成標(biāo)準(zhǔn)答案莉掂,當(dāng)用戶(hù)輸入的問(wèn)題的時(shí)候葛圃,如果用戶(hù)輸入的問(wèn)題在問(wèn)題庫(kù)里邊,可以直接按照標(biāo)準(zhǔn)問(wèn)題答案進(jìn)行回復(fù)憎妙。

發(fā)送消息給機(jī)器人是使用的sendToRobot

 // 機(jī)器人聊天
sendToRobot() {
  console.log(1223);
  if (this.sendInfo != "") {
    let createDate = this.nowTime();
    let noCode = +new Date();
    let content = this.sendInfo;
    this.sendInfo = "";
    // 封裝消息
    this.infoTemplate = {
      SendId: this.sender.id,
      ReviceId: 0,
      Content: content,
      Identity: 2,
      Type: 0,
      State: 0,
      NoCode: noCode,
      OutTradeNo: null,
      CreateDateUtc: createDate,
      Title: null,
      Description: null,
      Label: null,
      Thumbnail: null,
      NoSend: true,
    };
    // 把消息加入到消息會(huì)話(huà)內(nèi)容里邊
    this.toSendInfo(this.infoTemplate);
    // 把信息拉到最低下库正,因?yàn)橄⑿枰故咀钚碌?    this.toBottom(100);
    // 觸發(fā)socket的sendToRobot事件
    this.socket.emit("sendToRobot", this.infoTemplate);
    // 設(shè)定一個(gè)時(shí)間,如果超過(guò)了固定時(shí)間厘唾,就設(shè)置為發(fā)送失敗
    this.sendFailed(this.infoTemplate);
  } else {
    return null;
  }
}

在后端接收sendToRobot事件褥符,然后看看是否有發(fā)送過(guò)來(lái)問(wèn)題的固定答案,然后觸發(fā)changOrShowMsg

//發(fā)送信息給機(jī)器人
socket.on("sendToRobot", (data) => {
  let welCome = robotReply.filter(
    (x) => x.Answer.indexOf(data.Content) !== -1
  );
  socket.emit("reviceFromRobot", {
    content:
      welCome.length > 0
        ? welCome[0]
        : "非常對(duì)不起哦抚垃,不知道怎么回答這個(gè)問(wèn)題呢喷楣,我會(huì)努力學(xué)習(xí)的。",
    flag: welCome.length > 0 ? true : false,
  });
  socket.emit("changOrShowMsg", data);
});

當(dāng)前端接收到changOrShowMsg后鹤树,把消息設(shè)置為發(fā)送成功

// 修改信息狀態(tài)
this.socket.on("changOrShowMsg", (data) => {
  this.sendState = true;
  // 清除sendFailed設(shè)置的定時(shí)器铣焊,然后設(shè)置成功
  clearTimeout(this.msgTimer);
  this.conversition.forEach((x) => {
    if (x.NoCode !== null && x.NoCode === data.NoCode) {
      x.State = 1;
    }
  });
});

人工聊天

如果覺(jué)得客服機(jī)器人不能滿(mǎn)足需求的時(shí)候,可以通過(guò)點(diǎn)擊轉(zhuǎn)人工轉(zhuǎn)人工客服魂迄,和京東淘寶都類(lèi)似粗截,因?yàn)楹芏嗲闆r下,機(jī)器人都不能滿(mǎn)足用戶(hù)的需求捣炬,所以需要轉(zhuǎn)人工

客服不在線(xiàn)

調(diào)用函數(shù)是callPeople

// 呼叫客服
callPeople() {
  // 顯示loading
  this.loading();
  // 呼叫客服
  this.joinChat();
},

呼叫客服熊昌,其實(shí)就是看看有沒(méi)有客服在線(xiàn)

//加入會(huì)話(huà)
joinChat() {
  // 呼叫客服
  this.socket.emit("joinChat", {
    SendId: this.sender.id,
    ReviceId: this.revicer.id,
    SendName: this.sender.name,
    ReviceName: this.revicer.name,
    IsService: this.sender.isService,
    NoCode: this.noCode,
  });
},

在后端監(jiān)聽(tīng)joinChat事件,邏輯比較清晰湿酸,就是監(jiān)聽(tīng)到有用戶(hù)想加入進(jìn)來(lái)的時(shí)候婿屹,判斷當(dāng)前的是否有客服在線(xiàn),如果有客服在線(xiàn)推溃,則看下是否有空閑時(shí)間的客服昂利,如果每個(gè)客服都很忙,達(dá)到了最大服務(wù)用戶(hù)數(shù)量铁坎,則顯示客服較忙蜂奸,稍微再等會(huì),如果有空閑的客服硬萍,則把客服分配服務(wù)于當(dāng)前用戶(hù)扩所。

// 加入聊天
socket.on("joinChat", (data) => {
  let serviceList = null;
  let index = 0;
  // 如果發(fā)送消息的不是客服
  if (!data.IsService) {
    // 當(dāng)前登錄的客服列表
    serviceList = users.filter((x) => x.IsService === true);
    // 當(dāng)前登錄的客服列表的人數(shù)
    let serviceCount = serviceList.length;
    for (let i = serviceCount - 1; i >= 0; i--) {
      let item = serviceList[i];
      // 當(dāng)前登錄的用戶(hù)列表
      let number = users.filter((x) => x.ReviceId === item.SendId).length;
      // 當(dāng)前客服可以接待的最大用戶(hù)數(shù)量
      let num = userList.filter((x) => x.id === item.SendId)[0].receptNum;
      // 如果當(dāng)前登錄的用戶(hù)數(shù)量大于當(dāng)前客服可以接待的數(shù)量,把該客服刪除
      if (number >= num) {
        serviceList.splice(i, 1);
      }
    }
    // 如果當(dāng)前登錄的客服數(shù)量大于0并且每個(gè)客服已經(jīng)達(dá)到的最大的服務(wù)用戶(hù)數(shù)量
    if (serviceCount > 0 && serviceList.length <= 0) {
      socket.emit("joinError", {
        msg: "當(dāng)前咨詢(xún)?nèi)藬?shù)較多朴乖,請(qǐng)稍后再試",
      });
      return;
      // 還有剩余客服
    } else if (serviceList.length > 0) {
      // 隨機(jī)分配客服
      index = randomNum(0, serviceList.length - 1);
      socket.emit("joinTip", {
        ReviceName: serviceList[index].SendName,
        ReviceId: serviceList[index].SendId,
        ReviceOutTradeNo: serviceList[index].OutTradeNo,
      });
      // 讓會(huì)員加入房間
      socket.join(serviceList[index].OutTradeNo);
      // 如果沒(méi)有客服在線(xiàn)祖屏,則返回暫無(wú)客服在線(xiàn)
    } else {
      socket.emit("joinError", {
        msg: "暫無(wú)客服在線(xiàn)",
      });
      return;
    }
  } else {
    // 如果發(fā)送消息的是客服,則加入到聊天室里邊
    socket.join(socket.id);
  }
  // 若該用戶(hù)已登錄买羞,將舊設(shè)備登錄的用戶(hù)強(qiáng)制下線(xiàn)袁勺,多個(gè)用戶(hù)多端登錄
  let oldUser = users.filter((x) => x.SendId === data.SendId);
  if (oldUser.length > 0) {
    socket.to(oldUser[0].OutTradeNo).emit("squeezeOut", {
      noCode: oldUser[0].NoCode,
    });
  }
  // 存在用戶(hù)信息時(shí)將舊記錄刪除并且重新記錄
  users = users.filter((x) => x.SendId !== data.SendId);
  let user = {
    SendId: data.SendId,
    SendName: data.SendName,
    ReviceId: serviceList ? serviceList[index].SendId : data.ReviceId,
    ReviceName: serviceList ? serviceList[index].SendName : data.ReviceName,
    NoCode: data.NoCode,
    OutTradeNo: socket.id,
    Room: data.IsService ? socket.io : serviceList[index].OutTradeNo,
    IsService: data.IsService,
    IsSelect: false,
    SessionContent: data.SendName + "加入會(huì)話(huà)",
    UnRead: 0,
    CloseSession: false,
  };
  // 用戶(hù)重新加入
  users.push(user);

  // 把登錄成功的sendId記錄下來(lái)
  socket.SendId = data.SendId;
  io.emit("joinSuccess", {
    user,
    users,
  });
});

前面沒(méi)有上線(xiàn)客服,所以當(dāng)用戶(hù)想轉(zhuǎn)人工的時(shí)候畜普,只能顯示暫無(wú)客服期丰,現(xiàn)在看下客服端是什么樣的。

效果如下:


客服可以設(shè)置上線(xiàn)或者離線(xiàn)吃挑,當(dāng)客服上線(xiàn)之后钝荡,這個(gè)時(shí)候,當(dāng)用戶(hù)選擇客服聊天后儒鹿,就可以選擇客服了化撕。

調(diào)用

// 修改在線(xiàn)狀態(tài)
changeOnLine() {
  if (!this.sender.onlineState) {
    this.loading();
    // 客服上線(xiàn)
    this.socket.emit("joinChat", {
      SendId: this.sender.id,
      SendName: this.sender.name,
      ReviceId: -1,
      ReviceName: this.revicer.name,
      IsService: true,
      NoCode: this.noCode,
    });
  } else {
    // 離線(xiàn)
    this.loading();
    this.isSelectSession = false;
    this.socket.emit("offLine", {
      SendId: this.sender.id,
      NoCode: this.noCode,
    });
  }
},

后端如果接收到客服上線(xiàn),就把客服加入到socket,也就是joinChat

// 如果發(fā)送消息的是客服约炎,則加入到聊天室里邊
socket.join(socket.id);

如果客服已經(jīng)在線(xiàn)了植阴,就可以轉(zhuǎn)人工和客服聊天了,

 // 隨機(jī)分配客服
index = randomNum(0, serviceList.length - 1);
socket.emit("joinTip", {
  ReviceName: serviceList[index].SendName,
  ReviceId: serviceList[index].SendId,
  ReviceOutTradeNo: serviceList[index].OutTradeNo,
});
// 讓會(huì)員加入房間
socket.join(serviceList[index].OutTradeNo);

可以看到后端接收到信息后圾浅,觸發(fā)joinTip掠手,然后用戶(hù)就可以和客服聊天了。

發(fā)送信息狸捕,通過(guò)后端通過(guò)sendMsg來(lái)處理

// 發(fā)送消息
socket.on("sendMsg", (data) => {
  // 設(shè)置用戶(hù)未讀
  users.map((x) => {
    if (x.SendId === data.SendId) {
      x.SessionContent = data.Content;
      x.UnRead = 1;
      return x;
    }
  });
  //
  let sender = users.filter((x) => x.SendId === data.SendId);
  let revicer = users.filter((x) => x.SendId === data.ReviceId);
  if (sender.length < 0) {
    socket.emit("offLineTip", {
      msg: "您已掉線(xiàn)喷鸽,請(qǐng)重新連接",
    });
    return;
  }
  if (revicer.length < 0) {
    socket.emit("offLineTip", {
      msg: "對(duì)方已離線(xiàn)",
    });
    return;
  }
  data.State = 1;
  // 向socket觸發(fā)reviceMsg
  socket.to(data.OutTradeNo).emit("reviceMsg", data);
  socket.emit("changOrShowMsg", data);
});

可以看到,是通過(guò)socket.to(data.OutTradeNo).emit("reviceMsg", data); 來(lái)觸發(fā)

// 接收信息
this.socket.on("reviceMsg", (data) => {
  if (this.sender.isService && data.ReviceId == this.sender.id) {
    this.playMusic();
    this.currentSessionPeople.forEach((x) => {
      if (x.SendId === data.SendId) {
        if (!x.IsSelect) x.UnRead++;
        switch (data.Type) {
          case 0:
            x.SessionContent = data.Content;
            break;
          case 1:
            x.SessionContent = "圖片";
            break;
          case 2:
            x.SessionContent = "表情";
            break;
          case 3:
            x.SessionContent = "卡片";
            break;
        }
      }
    });
  }
  if (this.sender.onlineState) this.toSendInfo(data);
});

發(fā)送圖片

不管是用戶(hù)或者是客服發(fā)送圖片都是調(diào)用sendMsg

//發(fā)送圖片
sendImage(e) {
  const fileObj = e.target.files[0];
  let identity = this.sender.isService ? 1 : 2;
  if (fileObj != null) {
    // 判斷是否是圖片
    if (!/image\/\w+/.test(fileObj.type)) {
      return alert("請(qǐng)選擇圖片文件!", { icon: 5, time: 1000 });
    }
    var fd = new FormData();
    fd.append("file", fileObj);
    // 判斷圖片大小
    if (fileObj.size > 1024 * 1024 * 2 && fileObj.size < 1024 * 1024 * 10) {
      let reader = new FileReader();
      reader.readAsDataURL(fileObj);
      reader.onload = (e) => {
        let image = new Image(); //新建一個(gè)img標(biāo)簽(還沒(méi)嵌入DOM節(jié)點(diǎn))
        image.src = e.target.result;
        image.onload = () => {
          let canvas = document.createElement("canvas"),
            context = canvas.getContext("2d"),
            imageWidth = image.width / 2, //壓縮后圖片的大小
            imageHeight = image.height / 2,
            data = "";
          canvas.width = imageWidth;
          canvas.height = imageHeight;
          context.drawImage(image, 0, 0, imageWidth, imageHeight);
          data = canvas.toDataURL("image/jpeg");
          let newFile = this.dataURLtoFile(data); //壓縮完成
          fd = new FormData();
          fd.append("file", newFile);
          // 顯示出來(lái)
          this.signalrService(data, identity, 1);
          this.$refs.referenceUpload.value = null;
        };
      };
    } else if (fileObj.size > 1024 * 1024 * 10) {
      return alert("上傳圖片不能超過(guò)10M!", { icon: 5, time: 1000 });
    } else {
      let reader = new FileReader();
      reader.readAsDataURL(fileObj);
      reader.onload = (e) => {
        this.signalrService(e.target.result, identity, 1);
        this.$refs.referenceUpload.value = null;
      };
    }
  }
},

后面的處理就和發(fā)送文字類(lèi)似了

發(fā)送表情

發(fā)送表情是直接把圖片作為發(fā)送內(nèi)容進(jìn)行發(fā)送的灸拍,使用如下代碼:

<template v-for="(item, index) in expressions">
  <li>
    <img
      class="customerSendExpression"
      v-bind:src="item.image"
      v-bind:title="item.title"
      @click="toSend(item.image, 2, 2)"
    />
  </li>
</template>

本文由mdnice多平臺(tái)發(fā)布

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末做祝,一起剝皮案震驚了整個(gè)濱河市砾省,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌混槐,老刑警劉巖编兄,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異声登,居然都是意外死亡狠鸳,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)悯嗓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)件舵,“玉大人,你說(shuō)我怎么就攤上這事脯厨∏觯” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵俄认,是天一觀的道長(zhǎng)个少。 經(jīng)常有香客問(wèn)我,道長(zhǎng)眯杏,這世上最難降的妖魔是什么夜焦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮岂贩,結(jié)果婚禮上茫经,老公的妹妹穿的比我還像新娘。我一直安慰自己萎津,他們只是感情好卸伞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著锉屈,像睡著了一般荤傲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颈渊,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天遂黍,我揣著相機(jī)與錄音,去河邊找鬼俊嗽。 笑死雾家,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绍豁。 我是一名探鬼主播芯咧,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了敬飒?” 一聲冷哼從身側(cè)響起邪铲,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驶拱,沒(méi)想到半個(gè)月后霜浴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體晶衷,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蓝纲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晌纫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片税迷。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锹漱,靈堂內(nèi)的尸體忽然破棺而出箭养,到底是詐尸還是另有隱情,我是刑警寧澤哥牍,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布毕泌,位于F島的核電站,受9級(jí)特大地震影響嗅辣,放射性物質(zhì)發(fā)生泄漏撼泛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一澡谭、第九天 我趴在偏房一處隱蔽的房頂上張望愿题。 院中可真熱鬧,春花似錦蛙奖、人聲如沸潘酗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)仔夺。三九已至,卻和暖如春攒砖,著一層夾襖步出監(jiān)牢的瞬間缸兔,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工祭衩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灶体,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓掐暮,卻偏偏與公主長(zhǎng)得像蝎抽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容