glhcp/uniapp/bundle/pages/chat/chat.vue

638 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="chat flex-col">
<view class="content" @tap="showEmoji = false">
<scroll-view style="height: 100%;" :scroll-y="true" :scroll-top="scrollTop" :scroll-into-view="intoView"
@scrolltoupper="scrollToupper">
<view class="loading flex row-center" v-if="pageStatus == 'loading'">
<u-loading mode="flower" size="40"></u-loading>
</view>
<view class="chat-lists">
<view class="chat-item" v-for="(item,index) in recoreds" :id="`chat-item_${item.id}`" :key="item.id"
:class="{
'right': item.from_type == 'user',
'left': item.from_type == 'kefu',
'visibility': showIndex > index
} ">
<!-- 普通聊天记录 -->
<template v-if="item.type == 1">
<!-- 时间 -->
<view class="text-center m-b-30 white" v-if="timeFormat(item,index)">
<view class="chat-tips xs">{{timeFormat(item,index)}}</view>
</view>
<view class="chat-info">
<image class="avatar" :src="$getImageUri(item.from_avatar)">
</image>
<!-- 文本 -->
<view class="text-box" v-if="item.msg_type == 1">
<rich-text :nodes="replaceEmoji(item.msg)" space="nbsp"></rich-text>
</view>
<!-- 图片 -->
<view class="image-box" v-if="item.msg_type == 2">
<image class="image" mode="widthFix" :src="$getImageUri(item.msg)"
@tap="previewImage($getImageUri(item.msg))">
</image>
</view>
<!-- 商品 -->
<view class="goods m-r-20 goods-box" v-if="item.msg_type == 3">
<view class="goods-img m-r-20">
<image style="width: 140rpx;height: 140rpx;"
:src="$getImageUri(item.goods.image)">
</image>
</view>
<view class="goods-info flex-1">
<view class="line-2">
{{item.goods.name}}
</view>
<view class="flex m-t-10 row-between">
<price-format :color="colorConfig.primary" :subscript-size="26"
:first-size="38" :second-size="26" :price="item.goods.min_price">
</price-format>
</view>
</view>
</view>
</view>
</template>
<!-- 通知类型记录 -->
<template v-else>
<view class="text-center white">
<view class="muted xs">{{item.msg}}</view>
</view>
</template>
</view>
</view>
<view class="error" v-if="isError">
<view class="error-msg text-center xs">{{errorMsg}}</view>
</view>
<view id="bottom"></view>
</scroll-view>
</view>
<view class="footer" @tap="showGoods = false">
<view class="footer-input flex">
<view class="album" @tap="uploadFile">
<image class="icon" src="@/static/images/icon_album.png"></image>
</view>
<view class="input-contain flex">
<input v-model="msg" class="text-area" confirm-type="send" maxlength="-1"
@focus="scrollToBottom" @confirm="sendText" />
<image class="icon" src="@/static/images/icon_emoji.png" @tap="handleEmojiShow"></image>
</view>
<button size="sm" class="send-btn" @tap="sendText">发送</button>
</view>
<view class="emoji-wrap" :class="{'emoji-show': showEmoji}">
<scroll-view style="height:100%;" scroll-y="true">
<emoji @input="handleEmojiInput"></emoji>
</scroll-view>
</view>
</view>
<view class="goods" v-if="showGoods">
<view class="close" @tap="showGoods = false">
<u-icon name="close-circle-fill" color="#ccc" size="40"></u-icon>
</view>
<view class="goods-img m-r-20">
<u-image width="140rpx" height="140rpx" :src="goodsInfo.image"></u-image>
</view>
<view class="goods-info flex-1">
<view class="line-2">
{{goodsInfo.name}}
</view>
<view class="flex m-t-10 row-between">
<price-format :color="colorConfig.primary" :subscript-size="26" :first-size="38" :second-size="26"
:price="goodsInfo.min_price">
</price-format>
<view class="send-btn" @tap="sendGoods">发送链接</view>
</view>
</view>
</view>
</view>
</template>
<script>
import Socket from '@/utils/socket'
import {
chatRecord
} from '@/api/user'
import {
getChatConfig
} from '@/api/app'
import {
getGoodsDetail
} from '@/api/store'
import {
client,
uploadFile,
getRect,
debounce
} from '@/utils/tools'
import {
timeFormatChat
} from '@/utils/date'
import {
mapMutations
} from 'vuex';
export default {
data() {
return {
pageStatus: 'loading',
scrollTop: '',
intoView: '',
page: 1,
msg: '',
socket: {},
kefu: {},
showEmoji: false,
recoreds: [],
errorMsg: '',
goodsInfo: {},
isError: false,
showGoods: false,
showIndex: -1
}
},
computed: {
// 设置记录
timeFormat() {
return (item, index) => {
let timeFmt = timeFormatChat(item.create_time_stamp)
if (index && item.create_time_stamp - this.recoreds[index - 1].create_time_stamp < 300 && !item
.show_time) {
timeFmt = ''
}
return timeFmt
}
},
// 表情转换
replaceEmoji() {
return (str) => str.replace(/\[em-([a-z_]+)\]/g, `<span class="em em-$1"></span>`)
},
// 获取图片域名
$getImageUri() {
return (url) => this.$store.state.app.config.base_domain + url
}
},
watch: {
kefu(val) {
if (val.id) {
this.setTitle(val.nickname)
}
}
},
methods: {
// 初始化
init() {
this.shopId = this.$Route.query.shop_id || 0
this.goodsId = this.$Route.query.goods_id
this.socket = new Socket(this.appConfig.ws_domain, {
token: this.$store.getters.token,
type: 'user',
client,
shop_id: this.shopId,
})
this.socket.addEvent('connect', () => {
this.setTitle('连接中...')
})
this.socket.addEvent('open', () => {
this.setTitle(this.kefu.nickname)
this.isError = false
})
this.socket.addEvent('message', (data) => {
switch (data.event) {
case 'login':
this.loginEvent(data.data)
break;
case 'chat':
this.chatEvent(data.data)
break;
case 'transfer':
this.transferEvent(data.data)
break;
case 'error':
this.errorEvent(data.data)
break;
}
})
this.socket.addEvent('error', (data) => {
this.setTitle('连接失败')
})
},
showTips(msg) {
if (!msg) {
setTimeout(() => {
this.$Router.replace({
path: `/bundle/pages/contact_offical/contact_offical?id=${this.shopId}`
});
}, 200)
return
}
uni.showModal({
title: '温馨提示',
content: msg,
success: (res) => {
if (res.confirm) {
this.$Router.replace({
path: `/bundle/pages/contact_offical/contact_offical?id=${this.shopId}`
});
} else if (res.cancel) {
this.$Router.back()
}
}
});
},
getConfig() {
return getChatConfig({
shop_id: this.shopId
}).then(res => {
return Promise.resolve(res)
}).catch(() => {
return Promise.reject()
})
},
// 获取数据
async getData() {
try {
const res = await this.getConfig()
if (res.code == 0) return this.showTips(res.msg)
await this.getChatRecord()
this.getGoods()
this.scrollToBottom()
if (!this.kefu.id) {
this.setTitle('客服不在线')
return
}
this.socket.connect()
} catch (e) {
}
},
getGoods() {
if (!this.goodsId) return
getGoodsDetail({
goods_id: this.goodsId
}).then(res => {
if (res.code == 1) {
this.goodsInfo = res.data
if (this.kefu.id) {
this.showGoods = true
}
}
})
},
// 图片预览
previewImage(url) {
uni.previewImage({
urls: [url]
});
},
// 上传图片
async uploadFile() {
const [error, success] = await uni.chooseImage({
count: 1
})
if (error) {
return
}
uni.showLoading({
title: '上传中...'
})
try {
const file = await uploadFile(success.tempFilePaths[0])
this.send(file.base_uri, 2)
uni.hideLoading()
} catch (e) {
this.$toast({
title: '上传失败,请稍后再试'
})
uni.hideLoading()
}
},
// 发送文本
sendText() {
if (!this.msg) return
this.send(this.msg, 1)
this.msg = ''
},
// 发送商品
sendGoods() {
this.showGoods = false
this.send(this.goodsId, 3)
},
// 获取聊天记录
async getChatRecord() {
const {
page,
pageStatus
} = this
if (pageStatus == 'finish') return
const res = await chatRecord({
shop_id: this.shopId,
page_no: page
})
if (res.code == 1) {
let toid = 0
this.page++
const {
kefu,
record
} = res.data
this.kefu = kefu
this.showIndex = record.list.length
if (this.recoreds.length) {
toid = this.recoreds[0].id
this.recoreds[0].show_time = true
}
this.recoreds.unshift(...record.list)
this.$nextTick(() => {
if (!record.more) {
this.pageStatus = 'finish'
}
this.scrollToItem(toid)
this.showIndex = -1
})
}
},
// 发送消息
send(msg, type) {
this.socket.send({
event: 'chat',
data: {
msg,
msg_type: type, // 暂定 1=>文本2=>图片3=>表情
to_id: this.kefu.id, // 接收人id客服发给用户则为user_id, 用户发给客服则为kefu_id
to_type: "kefu"
}
})
},
// 显示、隐藏表情库
handleEmojiShow() {
this.showEmoji = !this.showEmoji
if (!this.showEmoji) return
setTimeout(() => {
this.scrollToBottom()
}, 300)
},
scrollToupper() {
this.getChatRecord()
},
scrollToBottom() {
this.intoView = 'bottom'
this.$nextTick(() => {
this.intoView = ''
})
},
scrollToItem(id) {
this.intoView = `chat-item_${id}`
this.$nextTick(() => {
this.intoView = ''
})
},
handleEmojiInput(val) {
this.msg = this.msg + val
},
chatEvent(data) {
this.isError = false
if (data.from_type == 'kefu') {
uni.vibrateLong({
success: function() {
console.log('success');
}
});
}
if (data.shop_id != this.shopId) {
return
}
this.recoreds.push(data)
this.$nextTick(() => {
getRect('#bottom').then(res => {
if (res.bottom < 1000) {
this.scrollToItem(data.id)
}
})
})
},
errorEvent(data) {
this.errorMsg = data.msg
this.isError = true
this.$nextTick(() => {
this.scrollToBottom()
})
},
loginEvent(data) {
// 登录成功,发送用户上线通知
this.socket.send({
event: 'user_online',
data: {
kefu_id: this.kefu.id
}
})
},
transferEvent(data) {
this.kefu = data
},
setTitle(title) {
uni.setNavigationBarTitle({
title
})
}
},
async onLoad() {
this.scrollToupper = debounce(this.scrollToupper, 500, this)
this.init()
this.getData()
},
onUnload() {
this.socket.close()
},
onReady() {
}
}
</script>
<style lang="scss">
page {
pading: 0;
height: 100%;
}
.chat {
height: 100%;
.goods {
display: flex;
position: fixed;
width: 600rpx;
right: 20rpx;
bottom: calc(120rpx + env(safe-area-inset-bottom));
border-radius: 14rpx;
background: #fff;
padding: 20rpx;
.close {
position: absolute;
left: -20rpx;
top: -20rpx;
}
.send-btn {
padding: 8rpx 22rpx;
}
}
.content {
transition: all .3s;
flex: 1;
min-height: 0;
.loading {
padding: 20rpx;
height: 40px;
}
.chat-lists {
padding: 0 20rpx 30rpx;
overflow: hidden;
position: relative;
.chat-tips {
padding: 4rpx 20rpx;
border-radius: 21rpx;
display: inline-block;
text-align: center;
background-color: rgba(0, 0, 0, 0.2);
}
.chat-item {
padding-top: 30rpx;
&.visibility {
visibility: hidden;
}
.chat-info {
display: flex;
align-items: flex-start;
}
&.right {
.chat-info {
flex-direction: row-reverse;
.text-box {
background-color: #ED5349;
color: #fff;
}
}
}
.avatar {
width: 78rpx;
height: 78rpx;
border-radius: 14rpx;
flex: none;
}
.text-box {
max-width: 500rpx;
min-width: 80rpx;
background-color: #fff;
border-radius: 14rpx;
padding: 16rpx 20rpx;
margin: 0 20rpx;
word-break: break-word;
line-height: 40rpx;
}
.image-box {
max-width: 300rpx;
margin: 0 20rpx;
.image {
max-width: 100%;
}
}
.goods-box {
position: static;
width: 510rpx;
}
}
}
}
.error {
padding: 0 30rpx 30rpx;
.error-msg {
color: #bbb;
word-break: break-word;
}
}
.footer {
background: #f2f2f2;
padding-bottom: env(safe-area-inset-bottom);
.footer-input {
height: 100rpx;
padding: 0 20rpx;
.icon {
width: 52rpx;
height: 52rpx;
}
.input-contain {
margin: 0 20rpx;
background-color: #fff;
height: 68rpx;
border-radius: 60rpx;
flex: 1;
overflow: hidden;
padding: 0 10rpx 0 30rpx;
.text-area {
flex: 1;
height: 100rpx;
word-break: break-all;
}
}
}
}
.emoji-wrap {
height: 0;
transition: all .3s;
&.emoji-show {
height: 200px;
}
}
.send-btn {
padding: 0 25rpx;
color: #fff;
background-color: #ED5349;
border-radius: 60rpx;
}
}
</style>