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.
 
 
 
customAgent/src/components/chat-new.vue

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>