You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
522 lines
14 KiB
522 lines
14 KiB
<script setup>
|
|
import { ref, computed, inject, watch, nextTick, onMounted } from 'vue'
|
|
import { showToast } from 'vant'
|
|
import _ from 'lodash'
|
|
import { showImagePreview } from 'vant'
|
|
import xss from 'xss'
|
|
import footerBar from '@/components/footer-bar.vue'
|
|
import hotQuestionNew from '@/components/hot-question-new.vue'
|
|
import welcomes from '@/components/welcomes.vue'
|
|
import abstract from '@/components/abstract.vue'
|
|
import chatContent from '@/components/chat-content.vue'
|
|
import DynamicImage from '@/components/common/DynamicImage.vue'
|
|
import imageService from '@/utils/imageService'
|
|
import { messageSend } from '@/utils/api'
|
|
import { Post } from '@/utils/https'
|
|
import { SJHLCmdList } from '@/utils/constant'
|
|
|
|
defineOptions({
|
|
name: 'chat-new',
|
|
})
|
|
const { isWX, tokenExist, lostChatInfoNum } = defineProps({
|
|
isWX: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
tokenExist: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
lostChatInfoNum: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
})
|
|
const store = inject('appStore', null)
|
|
const chatArr = inject('chatArr', ref([]))
|
|
const isChatLoading = inject('isChatLoading', ref(false))
|
|
const sessionId = inject('sessionId', ref(''))
|
|
const userId = inject('userId', ref(''))
|
|
const isLostMessage = inject('isLostMessage', ref(null))
|
|
const mainContainerRef = ref(null)
|
|
|
|
// 参考 liics/chat-main 的 tokenExist:token 获取成功后才渲染聊天主内容
|
|
|
|
const historylist = ref([])
|
|
const historyLoading = ref(false)
|
|
const pageNum = ref(1)
|
|
const pageSize = ref(10)
|
|
const nolist = ref(false)
|
|
const nolistTxt = ref('没有更多数据了')
|
|
function getMessageOptions(item) {
|
|
return item?.message?.[0]?.options || {}
|
|
}
|
|
|
|
function isImageMessage(item) {
|
|
const raw = item?.raw || item || {}
|
|
const options = getMessageOptions(item)
|
|
const reqAndResType = String(raw.reqAndResType || '')
|
|
const msgType = String(raw.msgType || '')
|
|
const fromFlag = String(raw.fromFlag || '')
|
|
const isProtocolImage =
|
|
(reqAndResType === '2' && fromFlag === '1' && msgType !== '01') ||
|
|
(reqAndResType === '08' && msgType === '02')
|
|
const isTextAnswer = Boolean(raw.msgText) && (msgType === '01' || fromFlag !== '1') && !isProtocolImage
|
|
|
|
return !isTextAnswer && (
|
|
item?.message?.[0]?.type === 'image' ||
|
|
isProtocolImage ||
|
|
Boolean((item?.imageUrl || item?.imageKey || options.imageUrl || options.imageKey || raw.imgUrl || raw.imgKey || raw.fileKey) && !raw.msgText)
|
|
)
|
|
}
|
|
|
|
function getItemImage(item) {
|
|
const raw = item?.raw || item || {}
|
|
const options = getMessageOptions(item)
|
|
const reqAndResType = String(raw.reqAndResType || '')
|
|
const msgType = String(raw.msgType || '')
|
|
const fromFlag = String(raw.fromFlag || '')
|
|
const imageMessage =
|
|
(reqAndResType === '2' && fromFlag === '1' && msgType !== '01') ||
|
|
(reqAndResType === '08' && msgType === '02')
|
|
const directImage = [item?.imageUrl, options.imageUrl, raw.imgUrl]
|
|
.map((value) => String(value || ''))
|
|
.find((value) => /^(data:|blob:|https?:\/\/)/i.test(value))
|
|
|
|
return String(
|
|
directImage ||
|
|
item?.imageKey ||
|
|
options.imageKey ||
|
|
raw.imgKey ||
|
|
raw.fileKey ||
|
|
item?.imageUrl ||
|
|
options.imageUrl ||
|
|
raw.imgUrl ||
|
|
(imageMessage ? raw.msgText : '') ||
|
|
'',
|
|
)
|
|
}
|
|
|
|
function getItemText(item) {
|
|
if (isImageMessage(item)) return ''
|
|
return String(getMessageOptions(item).label || '').replace(/\\n/g, '\n')
|
|
}
|
|
|
|
function isImageUploading(item) {
|
|
return isImageMessage(item) && item?.uploadStatus === 'uploading'
|
|
}
|
|
|
|
function isImageFailed(item) {
|
|
return isImageMessage(item) && item?.uploadStatus === 'failed'
|
|
}
|
|
|
|
function getItemHtml(item) {
|
|
const content = getItemText(item)
|
|
if (!content) return ''
|
|
|
|
// liics renders msgText as sanitized HTML and converts escaped newlines to
|
|
// line breaks. Keep the same behavior for msgType=01 and rich msgType=02.
|
|
return xss(content.replace(/\\"/g, '"').replace(/\n/g, '<br/>'))
|
|
}
|
|
|
|
async function previewImage(item) {
|
|
if (isImageUploading(item) || isImageFailed(item)) return
|
|
|
|
const imageKey = getItemImage(item)
|
|
if (!imageKey) return
|
|
|
|
const imageUrl = await imageService.getImageByKey(imageKey)
|
|
if (typeof imageUrl === 'string' && imageUrl) showImagePreview([imageUrl])
|
|
}
|
|
|
|
const showTyping = computed(
|
|
() =>
|
|
isChatLoading.value &&
|
|
store.chatArr.length > 0 &&
|
|
store.chatArr[store.chatArr.length - 1].type === 'user',
|
|
)
|
|
|
|
function sendText(text) {
|
|
if (!text) return
|
|
store?.pushUserMessage?.(text)
|
|
messageSend(text).catch((err) => {
|
|
console.log(err)
|
|
store?.pushUserMessage?.('服务异常,请您稍后再试~', 'bot')
|
|
})
|
|
}
|
|
|
|
function handleAskClick(item) {
|
|
sendText(item)
|
|
}
|
|
|
|
// 更新未读状态
|
|
function updateReadStatus(datalist) {
|
|
if (isLostMessage.value != 1) {
|
|
// 是不是当通会话
|
|
return
|
|
}
|
|
|
|
if (datalist && datalist.length > 0) {
|
|
const readedStatus = []
|
|
datalist.forEach((item) => {
|
|
if (item.chatId) {
|
|
if (sessionId.value == item.sessionId && item.fromFlag == 2) {
|
|
readedStatus.push(item.chatId)
|
|
}
|
|
}
|
|
})
|
|
|
|
// 提交到后台
|
|
store
|
|
?.updateReadStatus?.(readedStatus)
|
|
?.then(() => {
|
|
console.log('更新成功', readedStatus)
|
|
})
|
|
?.catch((err) => {
|
|
console.log(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
function scrollMainTo(top) {
|
|
const el = mainContainerRef.value
|
|
if (!el) return
|
|
el.scrollTop = top
|
|
}
|
|
function getHeaderUrl(context, payload) {
|
|
let params = {
|
|
"source": store.source
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
Post(
|
|
"config/avatar",
|
|
params
|
|
).then(res => {
|
|
resolve(res.data);
|
|
console.log("获取头像URL==", res)
|
|
if (res.code && res.code != 0) {
|
|
// Toast(res.msg);
|
|
reject(res.msg)
|
|
return;
|
|
}
|
|
|
|
//本地环境显示图片
|
|
// if (window.location.href.indexOf('localhost') != -1) {
|
|
// res.data = res.data.replaceAll('../', `//ssp-dev.cpic.com.cn/`)
|
|
// }
|
|
|
|
store.setHeaderAvatar(JSON.parse(res.data));
|
|
resolve(res.data);
|
|
})
|
|
.catch(error => {
|
|
reject(error)
|
|
});
|
|
});
|
|
}
|
|
const getChatHistory = async () => {
|
|
const params = {
|
|
page: pageNum.value,
|
|
pageSize: pageSize.value,
|
|
sessionId: sessionId.value || store?.sessionId,
|
|
userId: userId.value || store?.userId,
|
|
}
|
|
historyLoading.value = true
|
|
|
|
Post('history/chatInfo', params)
|
|
.then((response) => {
|
|
historyLoading.value = false
|
|
if (response.code && response.code != 0) {
|
|
showToast(response.msg)
|
|
return
|
|
}
|
|
|
|
// 成功返回
|
|
if (response.data?.list?.length > 0) {
|
|
const dataList = response.data.list
|
|
|
|
pageNum.value++
|
|
|
|
// 不满一页,说明已经无更多消息
|
|
if (dataList.length < pageSize.value) {
|
|
nolist.value = true
|
|
nolistTxt.value = '没有更多数据了'
|
|
}
|
|
|
|
// 生成 VIEW_ID,大写,避免污染源数据
|
|
dataList.forEach((val) => {
|
|
val.VIEW_ID = 'msg' + val.timestamp
|
|
})
|
|
|
|
// 获取当前最顶部的 VIEW_ID(注意写在 concat 前面)
|
|
let topMsg = historylist.value[0]
|
|
|
|
// 处理隐藏的数据
|
|
if (topMsg && !topMsg.msgText) {
|
|
topMsg = historylist.value[1]
|
|
if (topMsg && !topMsg.msgText) {
|
|
topMsg = historylist.value[2]
|
|
}
|
|
}
|
|
|
|
// 设置列表数据(历史消息也走同一套字段归一,保证 msgType=01 卡片能显示)
|
|
const normalizedList = store.normalizeHistoryMessages
|
|
? store.normalizeHistoryMessages(dataList)
|
|
: dataList
|
|
historylist.value = normalizedList.concat(historylist.value)
|
|
_.map(historylist.value, (item) => {
|
|
// 历史记录不展示数据互联
|
|
if (SJHLCmdList.includes(item.cmd) && !_.isEmpty(item.VIEW_ID)) {
|
|
item.msgText = ''
|
|
}
|
|
// 历史问句去掉 & 符号
|
|
if (item.fromFlag === 1 && item.msgText && item.msgText.indexOf('&') !== -1) {
|
|
item.msgText = item.msgText.replace(/&/g, '')
|
|
}
|
|
})
|
|
|
|
// 更新未读状态
|
|
updateReadStatus(dataList)
|
|
|
|
nextTick(() => {
|
|
if (pageNum.value <= 2) {
|
|
if (isLostMessage.value === 3) {
|
|
showToast('您有新消息未读,请及时查看历史消息')
|
|
}
|
|
setTimeout(() => {
|
|
scrollMainTo(99999)
|
|
}, 150)
|
|
} else if (topMsg) {
|
|
const view = document.querySelector('#' + topMsg.VIEW_ID)
|
|
console.log(topMsg.VIEW_ID + ' 节点离页面顶部的距离=' + view?.offsetTop)
|
|
|
|
setTimeout(() => {
|
|
if (view) scrollMainTo(view.offsetTop - 20)
|
|
}, 150)
|
|
}
|
|
})
|
|
} else {
|
|
nolist.value = true
|
|
nolistTxt.value = pageNum.value === 1 ? '当前还没有您的聊天记录' : '没有更多数据了'
|
|
}
|
|
})
|
|
.catch(() => {
|
|
historyLoading.value = false
|
|
pageNum.value--
|
|
nolist.value = true
|
|
})
|
|
}
|
|
onMounted(async () => {
|
|
await nextTick()
|
|
if (mainContainerRef.value) {
|
|
store?.setMescrollContent?.(mainContainerRef.value)
|
|
}
|
|
getHeaderUrl()
|
|
getChatHistory()
|
|
})
|
|
watch(
|
|
chatArr,
|
|
() => {
|
|
nextTick(() => {
|
|
const container = mainContainerRef.value
|
|
if (container) container.scrollTop = container.scrollHeight
|
|
})
|
|
},
|
|
{ deep: true },
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="chat-new">
|
|
<div class="main-container" ref="mainContainerRef">
|
|
<div class="history-loading" v-if="historyLoading">
|
|
<van-loading type="spinner" size="18px" />
|
|
</div>
|
|
<div class="historicRecords" v-if="historylist.length > 0">
|
|
<chatContent :historylist="historylist" :isWX="isWX" :tokenExist="tokenExist" :isHistory="true"
|
|
:lostChatInfoNum="lostChatInfoNum" />
|
|
</div>
|
|
<div class="this-news-box opacity" v-if="historylist.length > 0">
|
|
<div class="this-news-tips">以下是本次消息</div>
|
|
</div>
|
|
<welcomes />
|
|
<abstract />
|
|
<hotQuestionNew />
|
|
<chatContent :historylist="chatArr" :isWX="isWX" :tokenExist="tokenExist" :lostChatInfoNum="lostChatInfoNum" />
|
|
</div>
|
|
<div class="chat-footer">
|
|
<footer-bar v-if="tokenExist" :tokenExist="tokenExist" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<style lang="scss" scoped>
|
|
@import '@/assets/style/common.css';
|
|
.chat-new {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background-color: #ebf1ff;
|
|
background-image: url(@/assets/images/chat/chat-bg.png);
|
|
background-repeat: no-repeat;
|
|
background-size: 100% 100%;
|
|
background-position: center center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 12rem;
|
|
|
|
.chat-footer {
|
|
flex-shrink: 0;
|
|
margin: 12rem -12rem -12rem;
|
|
background: #ffffff;
|
|
padding-bottom: constant(safe-area-inset-bottom);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
.main-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
|
|
.history-loading {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.message-con {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12rem;
|
|
margin-top: 16rem;
|
|
|
|
.msg-item {
|
|
display: flex;
|
|
|
|
.bubble {
|
|
max-width: 80%;
|
|
padding: 10rem 14rem;
|
|
font-family: PingFang SC;
|
|
font-size: 14rem;
|
|
font-weight: normal;
|
|
line-height: 20rem;
|
|
letter-spacing: 0rem;
|
|
color: #17204d;
|
|
word-break: break-all;
|
|
|
|
.message-text {
|
|
white-space: normal;
|
|
|
|
:deep(p) {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
&.image-bubble {
|
|
// width: 130rem;
|
|
// height: 130rem;
|
|
min-width: 30rem;
|
|
min-height: 30rem;
|
|
max-width: 130rem;
|
|
max-height: 130rem;
|
|
position: relative;
|
|
background: #f5f5f5;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
|
|
.message-image {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-width: 30rem;
|
|
min-height: 30rem;
|
|
border-radius: inherit;
|
|
}
|
|
|
|
.upload-loading-icon {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #ffffff;
|
|
background: rgba(50, 50, 51, 0.88);
|
|
border-radius: inherit;
|
|
}
|
|
|
|
.upload-failed-icon {
|
|
font-size: 22rem;
|
|
}
|
|
}
|
|
|
|
.message-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.from-user {
|
|
justify-content: flex-end;
|
|
|
|
.bubble {
|
|
background: linear-gradient(266deg, #025cea 0%, #3f8cf8 100%);
|
|
color: #ffffff;
|
|
border-radius: 12rem 12rem 4rem 12rem;
|
|
|
|
&.image-bubble {
|
|
border-radius: 16rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.from-bot {
|
|
justify-content: flex-start;
|
|
|
|
.bubble {
|
|
background: #fcfdfd;
|
|
box-shadow: 0rem 4rem 10rem 0rem #e7effd;
|
|
border-radius: 12rem 12rem 12rem 4rem;
|
|
|
|
&.image-bubble {
|
|
border-radius: 16rem 16rem 16rem 2rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.typing {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5rem;
|
|
|
|
span {
|
|
width: 6rem;
|
|
height: 6rem;
|
|
border-radius: 50%;
|
|
background: #7e849f;
|
|
animation: typing-blink 1s infinite;
|
|
|
|
&:nth-child(2) {
|
|
animation-delay: 0.2s;
|
|
}
|
|
|
|
&:nth-child(3) {
|
|
animation-delay: 0.4s;
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes typing-blink {
|
|
|
|
0%,
|
|
80%,
|
|
100% {
|
|
opacity: 0.2;
|
|
}
|
|
|
|
40% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
|