客服:聊天消息区域抽离封装
parent
3355f5e853
commit
760dad0436
5
.env
5
.env
|
|
@ -5,12 +5,15 @@ SHOPRO_VERSION = v1.8.3
|
||||||
SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
|
SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
|
||||||
|
|
||||||
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
|
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
|
||||||
SHOPRO_DEV_BASE_URL = http://127.0.0.1:48080
|
SHOPRO_DEV_BASE_URL = http://192.168.1.105:48080
|
||||||
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
|
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
|
||||||
|
|
||||||
# 后端接口前缀(一般不建议调整)
|
# 后端接口前缀(一般不建议调整)
|
||||||
SHOPRO_API_PATH = /app-api
|
SHOPRO_API_PATH = /app-api
|
||||||
|
|
||||||
|
# 后端 websocket 接口前缀
|
||||||
|
SHOPRO_WEBSOCKET_PATH = /infra/ws
|
||||||
|
|
||||||
# 开发环境运行端口
|
# 开发环境运行端口
|
||||||
SHOPRO_DEV_PORT = 3000
|
SHOPRO_DEV_PORT = 3000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,456 @@
|
||||||
|
<template>
|
||||||
|
<view class="chat-box" :style="{ height: pageHeight + 'px' }">
|
||||||
|
<!-- 竖向滚动区域需要设置固定 height -->
|
||||||
|
<scroll-view
|
||||||
|
:style="{ height: pageHeight + 'px' }"
|
||||||
|
scroll-y="true"
|
||||||
|
:scroll-with-animation="false"
|
||||||
|
:enable-back-to-top="true"
|
||||||
|
:scroll-into-view="state.scrollInto"
|
||||||
|
>
|
||||||
|
<!-- 消息渲染 -->
|
||||||
|
<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.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)"
|
||||||
|
class="date-message">
|
||||||
|
{{ formatDate(item.date) }}
|
||||||
|
</view>
|
||||||
|
<!-- 系统消息 -->
|
||||||
|
<view v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM" class="system-message">
|
||||||
|
{{ item.content }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 消息体渲染管理员消息和用户消息并左右展示 -->
|
||||||
|
<view
|
||||||
|
v-if="item.contentType !== KeFuMessageContentTypeEnum.SYSTEM"
|
||||||
|
class="ss-flex ss-col-top"
|
||||||
|
:class="[
|
||||||
|
item.senderType === UserTypeEnum.ADMIN
|
||||||
|
? `ss-row-left`
|
||||||
|
: item.senderType === UserTypeEnum.MEMBER
|
||||||
|
? `ss-row-right`
|
||||||
|
: '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- 客服头像 -->
|
||||||
|
<image
|
||||||
|
v-show="item.senderType === UserTypeEnum.ADMIN"
|
||||||
|
class="chat-avatar ss-m-r-24"
|
||||||
|
:src="
|
||||||
|
sheep.$url.cdn(item?.senderAvatar) ||
|
||||||
|
sheep.$url.static('/static/img/shop/chat/default.png')
|
||||||
|
"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
|
||||||
|
<!-- 发送状态 -->
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
item.senderType === UserTypeEnum.MEMBER &&
|
||||||
|
index == chatList.length - 1 &&
|
||||||
|
isSendSuccess !== 0
|
||||||
|
"
|
||||||
|
class="send-status"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="isSendSuccess == -1"
|
||||||
|
class="loading"
|
||||||
|
:src="sheep.$url.static('/static/img/shop/chat/loading.png')"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
<!-- <image
|
||||||
|
v-if="chatData.isSendSuccess == 1"
|
||||||
|
class="warning"
|
||||||
|
:src="sheep.$url.static('/static/img/shop/chat/warning.png')"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="onAgainSendMessage(item)"
|
||||||
|
></image> -->
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<template v-if="item.contentType === KeFuMessageContentTypeEnum.TEXT">
|
||||||
|
<view class="message-box" :class="{'admin': item.senderType === UserTypeEnum.ADMIN}">
|
||||||
|
<mp-html :content="replaceEmoji(item.content)" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<template v-if="item.contentType === KeFuMessageContentTypeEnum.IMAGE">
|
||||||
|
<view class="message-box" :class="{'admin': item.senderType === UserTypeEnum.ADMIN}" :style="{ width: '200rpx' }">
|
||||||
|
<su-image
|
||||||
|
class="message-img"
|
||||||
|
isPreview
|
||||||
|
:previewList="[sheep.$url.cdn(item.content)]"
|
||||||
|
:current="0"
|
||||||
|
:src="sheep.$url.cdn(item.content)"
|
||||||
|
:height="200"
|
||||||
|
:width="200"
|
||||||
|
mode="aspectFill"
|
||||||
|
></su-image>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<template v-if="item.contentType === KeFuMessageContentTypeEnum.PRODUCT">
|
||||||
|
<GoodsItem
|
||||||
|
:goodsData="item.content.item"
|
||||||
|
@tap="
|
||||||
|
sheep.$router.go('/pages/goods/index', {
|
||||||
|
id: item.content.item.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="item.contentType === KeFuMessageContentTypeEnum.ORDER">
|
||||||
|
<OrderItem
|
||||||
|
from="msg"
|
||||||
|
:orderData="item.content.item"
|
||||||
|
@tap="
|
||||||
|
sheep.$router.go('/pages/order/detail', {
|
||||||
|
id: item.content.item.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- user头像 -->
|
||||||
|
<image
|
||||||
|
v-if="item.senderType === UserTypeEnum.MEMBER"
|
||||||
|
class="chat-avatar ss-m-l-24"
|
||||||
|
:src="sheep.$url.cdn(item?.senderAvatar) ||
|
||||||
|
sheep.$url.static('/static/img/shop/chat/default.png')"
|
||||||
|
mode="aspectFill"
|
||||||
|
>
|
||||||
|
</image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 视图滚动锚点 -->
|
||||||
|
<view id="scrollBottom"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import sheep from '@/sheep';
|
||||||
|
import OrderItem from '@/pages/chat/components/order.vue';
|
||||||
|
import GoodsItem from '@/pages/chat/components/goods.vue';
|
||||||
|
import { reactive, ref, unref } from 'vue';
|
||||||
|
import { formatDate } from '@/sheep/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { KeFuMessageContentTypeEnum,UserTypeEnum } from './constants';
|
||||||
|
import { emojiList } from '@/pages/chat/emoji';
|
||||||
|
|
||||||
|
const KEFU_MESSAGE_TYPE = 'kefu_message_type'; // 客服消息类型
|
||||||
|
const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
|
||||||
|
const pageHeight = safeArea.height - 44 - 35 - 50;
|
||||||
|
const state = reactive({
|
||||||
|
scrollInto: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatList = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
conversationId: 1001,
|
||||||
|
senderId: 1,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 2,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
||||||
|
content: "Hello, how are you?",
|
||||||
|
readStatus: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
conversationId: 1001,
|
||||||
|
senderId: 2,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 1,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
||||||
|
content: "I'm good, thanks! [流泪][流泪][流泪][流泪]",
|
||||||
|
readStatus: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
conversationId: 1002,
|
||||||
|
senderId: 3,
|
||||||
|
senderType: 2, // UserTypeEnum.ADMIN
|
||||||
|
receiverId: 4,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 2, // KeFuMessageContentTypeEnum.IMAGE
|
||||||
|
content: "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg",
|
||||||
|
readStatus: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
conversationId: 1002,
|
||||||
|
senderId: 4,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 3,
|
||||||
|
receiverType: 2, // UserTypeEnum.ADMIN
|
||||||
|
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
||||||
|
content: "This is a text message.",
|
||||||
|
readStatus: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
conversationId: 1003,
|
||||||
|
senderId: 5,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 6,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 3, // KeFuMessageContentTypeEnum.VOICE
|
||||||
|
content: "Voice content here",
|
||||||
|
readStatus: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
conversationId: 1003,
|
||||||
|
senderId: 6,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 5,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
||||||
|
content: "Another text message.",
|
||||||
|
readStatus: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
conversationId: 1004,
|
||||||
|
senderId: 7,
|
||||||
|
senderType: 2, // UserTypeEnum.ADMIN
|
||||||
|
receiverId: 8,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 1, // KeFuMessageContentTypeEnum.VIDEO
|
||||||
|
content: "Video content here",
|
||||||
|
readStatus: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
conversationId: 1004,
|
||||||
|
senderId: 8,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 7,
|
||||||
|
receiverType: 2, // UserTypeEnum.ADMIN
|
||||||
|
contentType: 5, // KeFuMessageContentTypeEnum.SYSTEM
|
||||||
|
content: "System message content",
|
||||||
|
readStatus: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
conversationId: 1005,
|
||||||
|
senderId: 9,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 10,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 10, // KeFuMessageContentTypeEnum.PRODUCT
|
||||||
|
content: "Product message content",
|
||||||
|
readStatus: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
conversationId: 1005,
|
||||||
|
senderId: 10,
|
||||||
|
senderType: 1, // UserTypeEnum.MEMBER
|
||||||
|
receiverId: 9,
|
||||||
|
receiverType: 1, // UserTypeEnum.MEMBER
|
||||||
|
contentType: 11, // KeFuMessageContentTypeEnum.ORDER
|
||||||
|
content: "Order message content",
|
||||||
|
readStatus: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const isSendSuccess = ref(-1)
|
||||||
|
//======================= 工具函数 =======================
|
||||||
|
/**
|
||||||
|
* 是否显示时间
|
||||||
|
* @param {*} item - 数据
|
||||||
|
* @param {*} index - 索引
|
||||||
|
*/
|
||||||
|
const showTime = (item, index) => {
|
||||||
|
if (unref(chatList)[index + 1]) {
|
||||||
|
let dateString = dayjs(unref(chatList)[index + 1].date).fromNow();
|
||||||
|
return dateString !== dayjs(unref(item).date).fromNow();
|
||||||
|
}
|
||||||
|
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 selEmojiFile(name) {
|
||||||
|
for (let index in emojiList) {
|
||||||
|
if (emojiList[index].name === name) {
|
||||||
|
return emojiList[index].file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.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));
|
||||||
|
|
||||||
|
&.admin {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const KeFuMessageContentTypeEnum = {
|
||||||
|
TEXT: 1, // 文本消息
|
||||||
|
IMAGE: 2, // 图片消息
|
||||||
|
VOICE: 3, // 语音消息
|
||||||
|
VIDEO: 4, // 视频消息
|
||||||
|
SYSTEM: 5, // 系统消息
|
||||||
|
// ========== 商城特殊消息 ==========
|
||||||
|
PRODUCT: 10,// 商品消息
|
||||||
|
ORDER: 11,// 订单消息"
|
||||||
|
};
|
||||||
|
export const UserTypeEnum = {
|
||||||
|
MEMBER: 1, // 会员 面向 c 端,普通用户
|
||||||
|
ADMIN: 2, // 管理员 面向 b 端,管理后台
|
||||||
|
};
|
||||||
|
|
@ -1,163 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<s-layout class="chat-wrap" title="客服" navbar="inner">
|
<s-layout class="chat-wrap" title="客服" navbar="inner">
|
||||||
|
<!-- 头部连接状态展示 -->
|
||||||
<div class="status">
|
<div class="status">
|
||||||
{{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
|
{{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 覆盖头部导航栏背景颜色 -->
|
||||||
<div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
|
<div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
|
||||||
<view class="chat-box" :style="{ height: pageHeight + 'px' }">
|
<!-- 聊天区域 -->
|
||||||
<scroll-view
|
<ChatBox></ChatBox>
|
||||||
: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>
|
<su-fixed bottom>
|
||||||
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
||||||
</su-fixed>
|
</su-fixed>
|
||||||
|
|
@ -166,7 +17,7 @@
|
||||||
@on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
|
@on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
|
||||||
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
||||||
</tools-popup>
|
</tools-popup>
|
||||||
|
<!-- 商品订单选择 -->
|
||||||
<SelectPopup
|
<SelectPopup
|
||||||
:mode="chat.selectMode"
|
:mode="chat.selectMode"
|
||||||
:show="chat.showSelect"
|
:show="chat.showSelect"
|
||||||
|
|
@ -177,17 +28,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { useChatWebSocket } from '@/pages/chat/socket';
|
||||||
|
import ChatBox from './components/chatBox.vue';
|
||||||
|
import { reactive, toRefs } from 'vue';
|
||||||
import sheep from '@/sheep';
|
import sheep from '@/sheep';
|
||||||
import { computed, reactive, toRefs } from 'vue';
|
import ToolsPopup from '@/pages/chat/components/toolsPopup.vue';
|
||||||
|
import MessageInput from '@/pages/chat/components/messageInput.vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import { emojiList } from './emoji.js';
|
import SelectPopup from '@/pages/chat/components/select-popup.vue';
|
||||||
import SelectPopup from './components/select-popup.vue';
|
|
||||||
import GoodsItem from './components/goods.vue';
|
|
||||||
import OrderItem from './components/order.vue';
|
|
||||||
import MessageInput from './components/messageInput.vue';
|
|
||||||
import ToolsPopup from './components/toolsPopup.vue';
|
|
||||||
import { useChatWebSocket } from './socket';
|
|
||||||
|
|
||||||
|
const sys_navBar = sheep.$platform.navbar;
|
||||||
const {
|
const {
|
||||||
socketInit,
|
socketInit,
|
||||||
state: chatData,
|
state: chatData,
|
||||||
|
|
@ -209,46 +59,6 @@
|
||||||
const customerUserInfo = toRefs(chatData).customerUserInfo;
|
const customerUserInfo = toRefs(chatData).customerUserInfo;
|
||||||
const socketState = toRefs(chatData).socketState;
|
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({
|
const chat = reactive({
|
||||||
msg: '',
|
msg: '',
|
||||||
scrollInto: '',
|
scrollInto: '',
|
||||||
|
|
@ -268,7 +78,41 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//======================= 聊天工具相关 =======================
|
function scrollBottom() {
|
||||||
|
let timeout = null;
|
||||||
|
chat.scrollInto = '';
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
chat.scrollInto = 'scrollBottom';
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
//======================= 聊天工具相关 start =======================
|
||||||
|
|
||||||
function handleToolsClose() {
|
function handleToolsClose() {
|
||||||
chat.showTools = false;
|
chat.showTools = false;
|
||||||
|
|
@ -366,137 +210,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAgainSendMessage(item) {
|
//======================= 聊天工具相关 end =======================
|
||||||
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 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 () => {
|
onLoad(async () => {
|
||||||
const { error } = await getUserToken();
|
socketInit({}, () => {
|
||||||
if (error === 0) {
|
scrollBottom();
|
||||||
socketInit(chatConfig.value, () => {
|
});
|
||||||
scrollBottom();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
socketState.value.isConnect = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped lang="scss">
|
||||||
.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 {
|
.chat-wrap {
|
||||||
// :deep() {
|
|
||||||
// .ui-navbar-box {
|
.page-bg {
|
||||||
// background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -511,172 +245,5 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--ui-BG-Main);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.chat-img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 0 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-img {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,678 @@
|
||||||
|
<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>
|
||||||
|
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
||||||
|
</su-fixed>
|
||||||
|
<!-- 聊天工具 -->
|
||||||
|
<tools-popup :show-tools="chat.showTools" :tools-mode="chat.toolsMode" @close="handleToolsClose"
|
||||||
|
@on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
|
||||||
|
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
||||||
|
</tools-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 } from './emoji.js';
|
||||||
|
import SelectPopup from './components/select-popup.vue';
|
||||||
|
import GoodsItem from './components/goods.vue';
|
||||||
|
import OrderItem from './components/order.vue';
|
||||||
|
import MessageInput from './components/messageInput.vue';
|
||||||
|
import ToolsPopup from './components/toolsPopup.vue';
|
||||||
|
import { useChatWebSocket } from './socket';
|
||||||
|
import { useWebSocket } from '@/sheep/hooks/useWebSocket';
|
||||||
|
|
||||||
|
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 handleToolsClose() {
|
||||||
|
chat.showTools = false;
|
||||||
|
chat.toolsMode = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEmoji(item) {
|
||||||
|
chat.msg += item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击工具栏开关
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
const websocket = useWebSocket()
|
||||||
|
onLoad(async () => {
|
||||||
|
websocket.socketInit({}, () => {
|
||||||
|
scrollBottom();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.chat-img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-img {
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,6 +3,7 @@ import sheep from '@/sheep';
|
||||||
// import chat from '@/sheep/api/chat';
|
// import chat from '@/sheep/api/chat';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import io from '@hyoga/uni-socket.io';
|
import io from '@hyoga/uni-socket.io';
|
||||||
|
import { baseUrl, websocketPath } from '@/sheep/config';
|
||||||
|
|
||||||
export function useChatWebSocket(socketConfig) {
|
export function useChatWebSocket(socketConfig) {
|
||||||
let SocketIo = null;
|
let SocketIo = null;
|
||||||
|
|
@ -14,7 +15,7 @@ export function useChatWebSocket(socketConfig) {
|
||||||
customerUserInfo: {}, //用户信息
|
customerUserInfo: {}, //用户信息
|
||||||
customerServerInfo: {
|
customerServerInfo: {
|
||||||
//客服信息
|
//客服信息
|
||||||
title: '连接中...',
|
title: '客服已接入',
|
||||||
state: 'connecting',
|
state: 'connecting',
|
||||||
avatar: null,
|
avatar: null,
|
||||||
nickname: '',
|
nickname: '',
|
||||||
|
|
@ -35,7 +36,7 @@ export function useChatWebSocket(socketConfig) {
|
||||||
|
|
||||||
chatConfig: {}, // 配置信息
|
chatConfig: {}, // 配置信息
|
||||||
|
|
||||||
isSendSucces: -1, // 是否发送成功 -1=发送中|0=发送成功|1发送失败
|
isSendSuccess: -1, // 是否发送成功 -1=发送中|0=发送成功|1发送失败
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,7 +50,11 @@ export function useChatWebSocket(socketConfig) {
|
||||||
if (state.socketState.isConnecting) return; // 重连中,返回false
|
if (state.socketState.isConnecting) return; // 重连中,返回false
|
||||||
|
|
||||||
// 启动初始化
|
// 启动初始化
|
||||||
SocketIo = io(config.chat_domain, {
|
SocketIo = io(baseUrl + websocketPath, {
|
||||||
|
path:websocketPath,
|
||||||
|
query:{
|
||||||
|
token: getAccessToken()
|
||||||
|
},
|
||||||
reconnection: true, // 默认 true 是否断线重连
|
reconnection: true, // 默认 true 是否断线重连
|
||||||
reconnectionAttempts: 5, // 默认无限次 断线尝试次数
|
reconnectionAttempts: 5, // 默认无限次 断线尝试次数
|
||||||
reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。
|
reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。
|
||||||
|
|
@ -302,8 +307,8 @@ export function useChatWebSocket(socketConfig) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 用户id,获取token
|
// 获取token
|
||||||
const getUserToken = async (id) => {
|
const getAccessToken = () => {
|
||||||
return uni.getStorageSync('token');
|
return uni.getStorageSync('token');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -803,9 +808,6 @@ export function useChatWebSocket(socketConfig) {
|
||||||
onDrop,
|
onDrop,
|
||||||
onPaste,
|
onPaste,
|
||||||
upload,
|
upload,
|
||||||
|
|
||||||
getUserToken,
|
|
||||||
|
|
||||||
state,
|
state,
|
||||||
|
|
||||||
socketTest,
|
socketTest,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import request from '@/sheep/request';
|
||||||
|
|
||||||
|
const KeFuApi = {
|
||||||
|
sendMessage: (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/promotion/kefu-message/send',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
custom: {
|
||||||
|
auth: true,
|
||||||
|
showLoading: true,
|
||||||
|
loadingMsg: '发送中',
|
||||||
|
showSuccess: true,
|
||||||
|
successMsg: '发送成功',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getConversation: () => {
|
||||||
|
return request({
|
||||||
|
url: '/promotion/kefu-conversation/get',
|
||||||
|
method: 'GET',
|
||||||
|
custom: {
|
||||||
|
auth: true,
|
||||||
|
showLoading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeFuApi;
|
||||||
|
|
@ -11,9 +11,10 @@ console.log(`[芋道商城 ${version}] http://doc.iocoder.cn`);
|
||||||
|
|
||||||
export const apiPath = import.meta.env.SHOPRO_API_PATH;
|
export const apiPath = import.meta.env.SHOPRO_API_PATH;
|
||||||
export const staticUrl = import.meta.env.SHOPRO_STATIC_URL;
|
export const staticUrl = import.meta.env.SHOPRO_STATIC_URL;
|
||||||
|
export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH;
|
||||||
export default {
|
export default {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
apiPath,
|
apiPath,
|
||||||
staticUrl,
|
staticUrl,
|
||||||
|
websocketPath
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { reactive, ref, unref } from 'vue';
|
||||||
|
import sheep from '@/sheep';
|
||||||
|
// import chat from '@/sheep/api/chat';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import io from '@hyoga/uni-socket.io';
|
||||||
|
import { baseUrl, websocketPath } from '@/sheep/config';
|
||||||
|
export function useWebSocket() {
|
||||||
|
const SocketIo = ref(null)
|
||||||
|
// chat状态数据
|
||||||
|
const state = reactive({
|
||||||
|
socketState: {
|
||||||
|
isConnect: true, //是否连接成功
|
||||||
|
isConnecting: false, //重连中,不允许新的socket开启。
|
||||||
|
tip: '',
|
||||||
|
},
|
||||||
|
chatConfig: {}, // 配置信息
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 连接初始化
|
||||||
|
* @param {Object} config - 配置信息
|
||||||
|
* @param {Function} callBack -回调函数,有新消息接入,保持底部
|
||||||
|
*/
|
||||||
|
const socketInit = (config, callBack) => {
|
||||||
|
state.chatConfig = config;
|
||||||
|
if (SocketIo.value && SocketIo.value.connected) return; // 如果socket已经连接,返回false
|
||||||
|
if (state.socketState.isConnecting) return; // 重连中,返回false
|
||||||
|
|
||||||
|
// 启动初始化
|
||||||
|
SocketIo.value = io(baseUrl + websocketPath, {
|
||||||
|
path:websocketPath,
|
||||||
|
query:{
|
||||||
|
token: getAccessToken()
|
||||||
|
},
|
||||||
|
reconnection: true, // 默认 true 是否断线重连
|
||||||
|
reconnectionAttempts: 5, // 默认无限次 断线尝试次数
|
||||||
|
reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。
|
||||||
|
reconnectionDelayMax: 5000, // 默认 5000, 重新连接等待的最长时间 默认 5000
|
||||||
|
randomizationFactor: 0.5, // 默认 0.5 [0-1],随机重连延迟时间
|
||||||
|
timeout: 20000, // 默认 20s
|
||||||
|
transports: ['websocket', 'polling'], // websocket | polling,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听连接
|
||||||
|
SocketIo.value.on('connect', async (res) => {
|
||||||
|
console.log('socket:connect');
|
||||||
|
// 消息返回
|
||||||
|
callBack && callBack(res)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误 error
|
||||||
|
SocketIo.value.on('error', (error) => {
|
||||||
|
console.log('error:', error);
|
||||||
|
});
|
||||||
|
// 重连失败 connect_error
|
||||||
|
SocketIo.value.on('connect_error', (error) => {
|
||||||
|
console.log('connect_error');
|
||||||
|
});
|
||||||
|
// 连接上,但无反应 connect_timeout
|
||||||
|
SocketIo.value.on('connect_timeout', (error) => {
|
||||||
|
console.log(error, 'connect_timeout');
|
||||||
|
});
|
||||||
|
// 服务进程销毁 disconnect
|
||||||
|
SocketIo.value.on('disconnect', (error) => {
|
||||||
|
console.log(error, 'disconnect');
|
||||||
|
});
|
||||||
|
// 服务重启重连上reconnect
|
||||||
|
SocketIo.value.on('reconnect', (error) => {
|
||||||
|
console.log(error, 'reconnect');
|
||||||
|
});
|
||||||
|
// 开始重连reconnect_attempt
|
||||||
|
SocketIo.value.on('reconnect_attempt', (error) => {
|
||||||
|
state.socketState.isConnect = false;
|
||||||
|
state.socketState.isConnecting = true;
|
||||||
|
console.log(error, 'reconnect_attempt');
|
||||||
|
});
|
||||||
|
// 重新连接中reconnecting
|
||||||
|
SocketIo.value.on('reconnecting', (error) => {
|
||||||
|
console.log(error, 'reconnecting');
|
||||||
|
});
|
||||||
|
// 重新连接错误reconnect_error
|
||||||
|
SocketIo.value.on('reconnect_error', (error) => {
|
||||||
|
console.log('reconnect_error');
|
||||||
|
});
|
||||||
|
// 重新连接失败reconnect_failed
|
||||||
|
SocketIo.value.on('reconnect_failed', (error) => {
|
||||||
|
state.socketState.isConnecting = false;
|
||||||
|
console.log(error, 'reconnect_failed');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 获取token
|
||||||
|
const getAccessToken = () => {
|
||||||
|
return uni.getStorageSync('token');
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
socketInit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ export const convertToInteger = (num) => {
|
||||||
* @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
|
* @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
|
||||||
* @returns {string} 返回拼接后的时间字符串
|
* @returns {string} 返回拼接后的时间字符串
|
||||||
*/
|
*/
|
||||||
export function formatDate(date, format) {
|
export function formatDate(date, format= 'YYYY-MM-DD HH:mm:ss') {
|
||||||
// 日期不存在,则返回空
|
// 日期不存在,则返回空
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return ''
|
return ''
|
||||||
|
|
@ -112,4 +112,4 @@ export function resetPagination(pagination) {
|
||||||
pagination.list = [];
|
pagination.list = [];
|
||||||
pagination.total = 0;
|
pagination.total = 0;
|
||||||
pagination.pageNo = 1;
|
pagination.pageNo = 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue