| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870 | <template>  <s-layout class="chat-wrap" title="客服" navbar="inner">    <div class="status">      {{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}    </div>    <div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>    <view class="chat-box" :style="{ height: pageHeight + 'px' }">      <scroll-view        :style="{ height: pageHeight + 'px' }"        scroll-y="true"        :scroll-with-animation="false"        :enable-back-to-top="true"        :scroll-into-view="chat.scrollInto"      >        <button          class="loadmore-btn ss-reset-button"          v-if="            chatList.length &&            chatHistoryPagination.lastPage > 1 &&            loadingMap[chatHistoryPagination.loadStatus].title          "          @click="onLoadMore"        >          {{ loadingMap[chatHistoryPagination.loadStatus].title }}          <i            class="loadmore-icon sa-m-l-6"            :class="loadingMap[chatHistoryPagination.loadStatus].icon"          ></i>        </button>        <view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">          <view class="ss-flex ss-row-center ss-col-center">            <!-- 日期 -->            <view v-if="item.from !== 'system' && showTime(item, index)" class="date-message">              {{ formatTime(item.date) }}            </view>            <!-- 系统消息 -->            <view v-if="item.from === 'system'" class="system-message">              {{ item.content.text }}            </view>          </view>          <!-- 常见问题 -->          <view v-if="item.mode === 'template' && item.content.list.length" class="template-wrap">            <view class="title">猜你想问</view>            <view              class="item"              v-for="(item, index) in item.content.list"              :key="index"              @click="onTemplateList(item)"            >              * {{ item.title }}            </view>          </view>          <view            v-if="              (item.from === 'customer_service' && item.mode !== 'template') ||              item.from === 'customer'            "            class="ss-flex ss-col-top"            :class="[              item.from === 'customer_service'                ? `ss-row-left`                : item.from === 'customer'                ? `ss-row-right`                : '',            ]"          >            <!-- 客服头像 -->            <image              v-show="item.from === 'customer_service'"              class="chat-avatar ss-m-r-24"              :src="                sheep.$url.cdn(item?.sender?.avatar) ||                sheep.$url.static('/static/img/shop/chat/default.png')              "              mode="aspectFill"            ></image>            <!-- 发送状态 -->            <span              v-if="                item.from === 'customer' &&                index == chatData.chatList.length - 1 &&                chatData.isSendSucces !== 0              "              class="send-status"            >              <image                v-if="chatData.isSendSucces == -1"                class="loading"                :src="sheep.$url.static('/static/img/shop/chat/loading.png')"                mode="aspectFill"              ></image>              <!-- <image                v-if="chatData.isSendSucces == 1"                class="warning"                :src="sheep.$url.static('/static/img/shop/chat/warning.png')"                mode="aspectFill"                @click="onAgainSendMessage(item)"              ></image> -->            </span>            <!-- 内容 -->            <template v-if="item.mode === 'text'">              <view class="message-box" :class="[item.from]">                <div                  class="message-text ss-flex ss-flex-wrap"                  @click="onRichtext"                  v-html="replaceEmoji(item.content.text)"                ></div>              </view>            </template>            <template v-if="item.mode === 'image'">              <view class="message-box" :class="[item.from]" :style="{ width: '200rpx' }">                <su-image                  class="message-img"                  isPreview                  :previewList="[sheep.$url.cdn(item.content.url)]"                  :current="0"                  :src="sheep.$url.cdn(item.content.url)"                  :height="200"                  :width="200"                  mode="aspectFill"                ></su-image>              </view>            </template>            <template v-if="item.mode === 'goods'">              <GoodsItem                :goodsData="item.content.item"                @tap="                  sheep.$router.go('/pages/goods/index', {                    id: item.content.item.id,                  })                "              />            </template>            <template v-if="item.mode === 'order'">              <OrderItem                from="msg"                :orderData="item.content.item"                @tap="                  sheep.$router.go('/pages/order/detail', {                    id: item.content.item.id,                  })                "              />            </template>            <!-- user头像 -->            <image              v-show="item.from === 'customer'"              class="chat-avatar ss-m-l-24"              :src="sheep.$url.cdn(customerUserInfo.avatar)"              mode="aspectFill"            >            </image>          </view>        </view>        <view id="scrollBottom"></view>      </scroll-view>    </view>    <su-fixed bottom>      <view class="send-wrap ss-flex">        <view class="left ss-flex ss-flex-1">          <uni-easyinput            class="ss-flex-1 ss-p-l-22"            :inputBorder="false"            :clearable="false"            v-model="chat.msg"            placeholder="请输入你要咨询的问题"          ></uni-easyinput>        </view>        <text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>        <text          v-if="!chat.msg"          class="sicon-edit"          :class="{ 'is-active': chat.toolsMode == 'tools' }"          @tap.stop="onTools('tools')"        ></text>        <button v-if="chat.msg" class="ss-reset-button send-btn" @tap="onSendMessage">          发送        </button>      </view>    </su-fixed>    <su-popup      :show="chat.showTools"      @close="        chat.showTools = false;        chat.toolsMode = '';      "    >      <view class="ss-modal-box ss-flex-col">        <view class="send-wrap ss-flex">          <view class="left ss-flex ss-flex-1">            <uni-easyinput              class="ss-flex-1 ss-p-l-22"              :inputBorder="false"              :clearable="false"              v-model="chat.msg"              placeholder="请输入你要咨询的问题"            ></uni-easyinput>          </view>          <text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>          <text></text>          <text            v-if="!chat.msg"            class="sicon-edit"            :class="{ 'is-active': chat.toolsMode == 'tools' }"            @tap.stop="onTools('tools')"          ></text>          <button v-if="chat.msg" class="ss-reset-button send-btn" @tap="onSendMessage">            发送          </button>        </view>        <view class="content ss-flex ss-flex-1">          <template v-if="chat.toolsMode == 'emoji'">            <swiper              class="emoji-swiper"              :indicator-dots="true"              circular              indicator-active-color="#7063D2"              indicator-color="rgba(235, 231, 255, 1)"              :autoplay="false"              :interval="3000"              :duration="1000"            >              <swiper-item v-for="emoji in emojiPage" :key="emoji">                <view class="ss-flex ss-flex-wrap">                  <template v-for="item in emoji" :key="item">                    <image                      class="emoji-img"                      :src="sheep.$url.cdn(`/static/img/chat/emoji/${item.file}`)"                      @tap="onEmoji(item)"                    >                    </image>                  </template>                </view>              </swiper-item>            </swiper>          </template>          <template v-else>            <view class="image">              <s-uploader                file-mediatype="image"                :imageStyles="{ width: 50, height: 50, border: false }"                @select="onSelect({ type: 'image', data: $event })"              >                <image                  class="icon"                  :src="sheep.$url.static('/static/img/shop/chat/image.png')"                  mode="aspectFill"                ></image>              </s-uploader>              <view>图片</view>            </view>            <view class="goods" @tap="onShowSelect('goods')">              <image                class="icon"                :src="sheep.$url.static('/static/img/shop/chat/goods.png')"                mode="aspectFill"              ></image>              <view>商品</view>            </view>            <view class="order" @tap="onShowSelect('order')">              <image                class="icon"                :src="sheep.$url.static('/static/img/shop/chat/order.png')"                mode="aspectFill"              ></image>              <view>订单</view>            </view>          </template>        </view>      </view>    </su-popup>    <SelectPopup      :mode="chat.selectMode"      :show="chat.showSelect"      @select="onSelect"      @close="chat.showSelect = false"    />  </s-layout></template><script setup>  import sheep from '@/sheep';  import { computed, reactive, toRefs } from 'vue';  import { onLoad } from '@dcloudio/uni-app';  import { emojiList, emojiPage } from './emoji.js';  import SelectPopup from './components/select-popup.vue';  import GoodsItem from './components/goods.vue';  import OrderItem from './components/order.vue';  import { useChatWebSocket } from './socket';  const {    socketInit,    state: chatData,    socketSendMsg,    formatChatInput,    socketHistoryList,    onDrop,    onPaste,    getFocus,    // upload,    getUserToken,    // socketTest,    showTime,    formatTime,  } = useChatWebSocket();  const chatList = toRefs(chatData).chatList;  const customerServiceInfo = toRefs(chatData).customerServerInfo;  const chatHistoryPagination = toRefs(chatData).chatHistoryPagination;  const customerUserInfo = toRefs(chatData).customerUserInfo;  const socketState = toRefs(chatData).socketState;  const sys_navBar = sheep.$platform.navbar;  const chatConfig = computed(() => sheep.$store('app').chat);  const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;  const pageHeight = safeArea.height - 44 - 35 - 50;  const chatStatus = {    online: {      text: '在线',      colorVariate: '#46c55f',    },    offline: {      text: '离线',      colorVariate: '#b5b5b5',    },    busy: {      text: '忙碌',      colorVariate: '#ff0e1b',    },  };  // 加载更多  const loadingMap = {    loadmore: {      title: '查看更多',      icon: 'el-icon-d-arrow-left',    },    nomore: {      title: '没有更多了',      icon: '',    },    loading: {      title: '加载中... ',      icon: 'el-icon-loading',    },  };  const onLoadMore = () => {    chatHistoryPagination.value.page < chatHistoryPagination.value.lastPage && socketHistoryList();  };  const chat = reactive({    msg: '',    scrollInto: '',    showTools: false,    toolsMode: '',    showSelect: false,    selectMode: '',    chatStyle: {      mode: 'inner',      color: '#F8270F',      type: 'color',      alwaysShow: 1,      src: '',      list: {},    },  });  // 点击工具栏开关  function onTools(mode) {    if (!socketState.value.isConnect) {      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');      return;    }    if (!chat.toolsMode || chat.toolsMode === mode) {      chat.showTools = !chat.showTools;    }    chat.toolsMode = mode;    if (!chat.showTools) {      chat.toolsMode = '';    }  }  function onShowSelect(mode) {    chat.showTools = false;    chat.showSelect = true;    chat.selectMode = mode;  }  async function onSelect({ type, data }) {    let msg = '';    switch (type) {      case 'image':        const { path, fullurl } = await sheep.$api.app.upload(data.tempFiles[0].path, 'default');        msg = {          from: 'customer',          mode: 'image',          date: new Date().getTime(),          content: {            url: fullurl,            path: path,          },        };        break;      case 'goods':        msg = {          from: 'customer',          mode: 'goods',          date: new Date().getTime(),          content: {            item: {              id: data.goods.id,              title: data.goods.title,              image: data.goods.image,              price: data.goods.price,              stock: data.goods.stock,            },          },        };        break;      case 'order':        msg = {          from: 'customer',          mode: 'order',          date: new Date().getTime(),          content: {            item: {              id: data.id,              order_sn: data.order_sn,              create_time: data.create_time,              pay_fee: data.pay_fee,              items: data.items.filter((item) => ({                goods_id: item.goods_id,                goods_title: item.goods_title,                goods_image: item.goods_image,                goods_price: item.goods_price,              })),              status_text: data.status_text,            },          },        };        break;    }    if (msg) {      socketSendMsg(msg, () => {        scrollBottom();      });      // scrollBottom();      chat.showTools = false;      chat.showSelect = false;      chat.selectMode = '';    }  }  function onAgainSendMessage(item) {    if (!socketState.value.isConnect) {      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');      return;    }    if (!item) return;    const data = {      from: 'customer',      mode: 'text',      date: new Date().getTime(),      content: item.content,    };    socketSendMsg(data, () => {      scrollBottom();    });  }  function onSendMessage() {    if (!socketState.value.isConnect) {      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');      return;    }    if (!chat.msg) return;    const data = {      from: 'customer',      mode: 'text',      date: new Date().getTime(),      content: {        text: chat.msg,      },    };    socketSendMsg(data, () => {      scrollBottom();    });    chat.showTools = false;    // scrollBottom();    setTimeout(() => {      chat.msg = '';    }, 100);  }  // 点击猜你想问  function onTemplateList(e) {    if (!socketState.value.isConnect) {      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');      return;    }    const data = {      from: 'customer',      mode: 'text',      date: new Date().getTime(),      content: {        text: e.title,      },      customData: {        question_id: e.id,      },    };    socketSendMsg(data, () => {      scrollBottom();    });    // scrollBottom();  }  function onEmoji(item) {    chat.msg += item.name;  }  function selEmojiFile(name) {    for (let index in emojiList) {      if (emojiList[index].name === name) {        return emojiList[index].file;      }    }    return false;  }  function replaceEmoji(data) {    let newData = data;    if (typeof newData !== 'object') {      let reg = /\[(.+?)\]/g; // [] 中括号      let zhEmojiName = newData.match(reg);      if (zhEmojiName) {        zhEmojiName.forEach((item) => {          let emojiFile = selEmojiFile(item);          newData = newData.replace(            item,            `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(              '/static/img/chat/emoji/' + emojiFile,            )}"/>`,          );        });      }    }    return newData;  }  function scrollBottom() {    let timeout = null;    chat.scrollInto = '';    clearTimeout(timeout);    timeout = setTimeout(() => {      chat.scrollInto = 'scrollBottom';    }, 100);  }  onLoad(async () => {    const { error } = await getUserToken();    if (error === 0) {      socketInit(chatConfig.value, () => {        scrollBottom();      });    } else {      socketState.value.isConnect = false;    }  });</script><style lang="scss" scoped>  .page-bg {    width: 100%;    position: absolute;    top: 0;    left: 0;    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));    background-size: 750rpx 100%;    z-index: 1;  }  .chat-wrap {    // :deep() {    //   .ui-navbar-box {    //     background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));    //   }    // }    .status {      position: relative;      box-sizing: border-box;      z-index: 3;      height: 70rpx;      padding: 0 30rpx;      background: var(--ui-BG-Main-opacity-1);      display: flex;      align-items: center;      font-size: 30rpx;      font-weight: 400;      color: var(--ui-BG-Main);    }    .chat-box {      padding: 0 20rpx 0;      .loadmore-btn {        width: 98%;        height: 40px;        font-size: 12px;        color: #8c8c8c;        .loadmore-icon {          transform: rotate(90deg);        }      }      .message-item {        margin-bottom: 33rpx;      }      .date-message,      .system-message {        width: fit-content;        border-radius: 12rpx;        padding: 8rpx 16rpx;        margin-bottom: 16rpx;        background-color: var(--ui-BG-3);        color: #999;        font-size: 24rpx;      }      .chat-avatar {        width: 70rpx;        height: 70rpx;        border-radius: 50%;      }      .send-status {        color: #333;        height: 80rpx;        margin-right: 8rpx;        display: flex;        align-items: center;        .loading {          width: 32rpx;          height: 32rpx;          -webkit-animation: rotating 2s linear infinite;          animation: rotating 2s linear infinite;          @-webkit-keyframes rotating {            0% {              transform: rotateZ(0);            }            100% {              transform: rotateZ(360deg);            }          }          @keyframes rotating {            0% {              transform: rotateZ(0);            }            100% {              transform: rotateZ(360deg);            }          }        }        .warning {          width: 32rpx;          height: 32rpx;          color: #ff3000;        }      }      .message-box {        max-width: 50%;        font-size: 16px;        line-height: 20px;        // max-width: 500rpx;        white-space: normal;        word-break: break-all;        word-wrap: break-word;        padding: 20rpx;        border-radius: 10rpx;        color: #fff;        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));        &.customer_service {          background: #fff;          color: #333;        }        :deep() {          .imgred {            width: 100%;          }          .imgred,          img {            width: 100%;          }        }      }      :deep() {        .goods,        .order {          max-width: 500rpx;        }      }      .message-img {        width: 100px;        height: 100px;        border-radius: 6rpx;      }      .template-wrap {        // width: 100%;        padding: 20rpx 24rpx;        background: #fff;        border-radius: 10rpx;        .title {          font-size: 26rpx;          font-weight: 500;          color: #333;          margin-bottom: 29rpx;        }        .item {          font-size: 24rpx;          color: var(--ui-BG-Main);          margin-bottom: 16rpx;          &:last-of-type {            margin-bottom: 0;          }        }      }      .error-img {        width: 400rpx;        height: 400rpx;      }      #scrollBottom {        height: 120rpx;      }    }    .send-wrap {      padding: 18rpx 20rpx;      background: #fff;      .left {        height: 64rpx;        border-radius: 32rpx;        background: var(--ui-BG-1);      }      .bq {        font-size: 50rpx;        margin-left: 10rpx;      }      .sicon-edit {        font-size: 50rpx;        margin-left: 10rpx;        transform: rotate(0deg);        transition: all linear 0.2s;        &.is-active {          transform: rotate(45deg);        }      }      .send-btn {        width: 100rpx;        height: 60rpx;        line-height: 60rpx;        border-radius: 30rpx;        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));        font-size: 26rpx;        color: #fff;        margin-left: 11rpx;      }    }  }  .content {    width: 100%;    align-content: space-around;    border-top: 1px solid #dfdfdf;    padding: 20rpx 0 0;    .emoji-swiper {      width: 100%;      height: 280rpx;      padding: 0 20rpx;      .emoji-img {        width: 50rpx;        height: 50rpx;        display: inline-block;        margin: 10rpx;      }    }    .image,    .goods,    .order {      width: 33.3%;      height: 280rpx;      text-align: center;      font-size: 24rpx;      color: #333;      display: flex;      flex-direction: column;      align-items: center;      justify-content: center;      .icon {        width: 50rpx;        height: 50rpx;        margin-bottom: 21rpx;      }    }    :deep() {      .uni-file-picker__container {        justify-content: center;      }      .file-picker__box {        display: none;        &:last-of-type {          display: flex;        }      }    }  }</style><style>  .chat-img {    width: 24px;    height: 24px;    margin: 0 3px;  }  .full-img {    object-fit: cover;    width: 100px;    height: 100px;    border-radius: 6px;  }</style>
 |