|
|
|
@ -1,22 +1,98 @@ |
|
|
|
<script setup> |
|
|
|
<script setup> |
|
|
|
import { ref, computed, inject, watch, nextTick, onMounted } from 'vue' |
|
|
|
import { ref, computed, inject, watch, nextTick } from 'vue' |
|
|
|
import { showToast } from 'vant'; |
|
|
|
import { showImagePreview } from 'vant' |
|
|
|
|
|
|
|
import xss from 'xss' |
|
|
|
import footerBarInput from '@/components/footer-bar-input.vue' |
|
|
|
import footerBarInput from '@/components/footer-bar-input.vue' |
|
|
|
import hotQuestionNew from '@/components/hot-question-new.vue' |
|
|
|
import hotQuestionNew from '@/components/hot-question-new.vue' |
|
|
|
import welcomes from '@/components/welcomes.vue' |
|
|
|
import welcomes from '@/components/welcomes.vue' |
|
|
|
import abstract from '@/components/abstract.vue' |
|
|
|
import abstract from '@/components/abstract.vue' |
|
|
|
|
|
|
|
import DynamicImage from '@/components/common/DynamicImage.vue' |
|
|
|
|
|
|
|
import imageService from '@/utils/imageService' |
|
|
|
import { messageSend } from '@/utils/api' |
|
|
|
import { messageSend } from '@/utils/api' |
|
|
|
defineOptions({ |
|
|
|
defineOptions({ |
|
|
|
name: 'chat-new', |
|
|
|
name: 'chat-new', |
|
|
|
}) |
|
|
|
}) |
|
|
|
import { Post } from '@/utils/https' |
|
|
|
|
|
|
|
const store = inject('appStore', null) |
|
|
|
const store = inject('appStore', null) |
|
|
|
const chatArr = inject('chatArr', ref([])) |
|
|
|
const chatArr = inject('chatArr', ref([])) |
|
|
|
const isChatLoading = inject('isChatLoading', ref(false)) |
|
|
|
const isChatLoading = inject('isChatLoading', ref(false)) |
|
|
|
const mainContainerRef = ref(null) |
|
|
|
const mainContainerRef = ref(null) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 === '02')) || |
|
|
|
|
|
|
|
(reqAndResType === '08' && msgType === '02') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return 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 === '02')) || |
|
|
|
|
|
|
|
(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) { |
|
|
|
function getItemText(item) { |
|
|
|
return item?.message?.[0]?.options?.label || '' |
|
|
|
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( |
|
|
|
const showTyping = computed( |
|
|
|
@ -38,8 +114,17 @@ function sendText(text) { |
|
|
|
function handleAskClick(item) { |
|
|
|
function handleAskClick(item) { |
|
|
|
sendText(item) |
|
|
|
sendText(item) |
|
|
|
} |
|
|
|
} |
|
|
|
onMounted(async () => { |
|
|
|
|
|
|
|
|
|
|
|
watch( |
|
|
|
|
|
|
|
chatArr, |
|
|
|
|
|
|
|
() => { |
|
|
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
|
|
const container = mainContainerRef.value |
|
|
|
|
|
|
|
if (container) container.scrollTop = container.scrollHeight |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
{ deep: true }, |
|
|
|
|
|
|
|
) |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
<template> |
|
|
|
<template> |
|
|
|
@ -51,7 +136,25 @@ onMounted(async () => { |
|
|
|
<div class="message-con" v-if="chatArr.length || showTyping"> |
|
|
|
<div class="message-con" v-if="chatArr.length || showTyping"> |
|
|
|
<div v-for="(item, index) in chatArr" :key="index" |
|
|
|
<div v-for="(item, index) in chatArr" :key="index" |
|
|
|
:class="['msg-item', item.type === 'user' ? 'from-user' : 'from-bot']"> |
|
|
|
:class="['msg-item', item.type === 'user' ? 'from-user' : 'from-bot']"> |
|
|
|
<div class="bubble">{{ getItemText(item) }}</div> |
|
|
|
<div class="bubble" :class="{ 'image-bubble': isImageMessage(item) }"> |
|
|
|
|
|
|
|
<template v-if="isImageMessage(item)"> |
|
|
|
|
|
|
|
<DynamicImage |
|
|
|
|
|
|
|
class="message-image" |
|
|
|
|
|
|
|
:image-key="getItemImage(item)" |
|
|
|
|
|
|
|
alt="聊天图片" |
|
|
|
|
|
|
|
:clickable="true" |
|
|
|
|
|
|
|
image-style="display: block; width: 100%; height: 100%; object-fit: cover;" |
|
|
|
|
|
|
|
@click="previewImage(item)" |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
<div v-if="isImageUploading(item)" class="upload-loading-icon"> |
|
|
|
|
|
|
|
<van-loading type="spinner" color="#ffffff" size="16px" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-else-if="isImageFailed(item)" class="upload-loading-icon upload-failed-icon"> |
|
|
|
|
|
|
|
<van-icon name="warning-o" size="22px" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
<span v-else class="message-text" v-html="getItemHtml(item)"></span> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="msg-item from-bot" v-if="showTyping"> |
|
|
|
<div class="msg-item from-bot" v-if="showTyping"> |
|
|
|
<div class="bubble typing"><span></span><span></span><span></span></div> |
|
|
|
<div class="bubble typing"><span></span><span></span><span></span></div> |
|
|
|
@ -108,6 +211,57 @@ onMounted(async () => { |
|
|
|
letter-spacing: 0rem; |
|
|
|
letter-spacing: 0rem; |
|
|
|
color: #17204d; |
|
|
|
color: #17204d; |
|
|
|
word-break: break-all; |
|
|
|
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%; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -118,6 +272,10 @@ onMounted(async () => { |
|
|
|
background: linear-gradient(266deg, #025cea 0%, #3f8cf8 100%); |
|
|
|
background: linear-gradient(266deg, #025cea 0%, #3f8cf8 100%); |
|
|
|
color: #ffffff; |
|
|
|
color: #ffffff; |
|
|
|
border-radius: 12rem 12rem 4rem 12rem; |
|
|
|
border-radius: 12rem 12rem 4rem 12rem; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.image-bubble { |
|
|
|
|
|
|
|
border-radius: 16rem; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -128,6 +286,10 @@ onMounted(async () => { |
|
|
|
background: #fcfdfd; |
|
|
|
background: #fcfdfd; |
|
|
|
box-shadow: 0rem 4rem 10rem 0rem #e7effd; |
|
|
|
box-shadow: 0rem 4rem 10rem 0rem #e7effd; |
|
|
|
border-radius: 12rem 12rem 12rem 4rem; |
|
|
|
border-radius: 12rem 12rem 12rem 4rem; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.image-bubble { |
|
|
|
|
|
|
|
border-radius: 16rem 16rem 16rem 2rem; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|