|
|
|
<script setup lang="ts">
|
|
|
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
|
|
import { Image, Spin } from 'ant-design-vue'
|
|
|
|
import { MdPreview } from 'md-editor-v3'
|
|
|
|
import 'md-editor-v3/lib/style.css'
|
|
|
|
import { throttle } from 'lodash-es'
|
|
|
|
import type { MessageItem } from './index.d'
|
|
|
|
import { SvgIcon } from '@/components/SvgIcon'
|
|
|
|
import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum'
|
|
|
|
import { MenuTypeEnum } from '@/enums/menuEnum'
|
|
|
|
import { copyText } from '@/utils/copyTextToClipboard'
|
|
|
|
import { useMessageStore } from '@/store/moules/messageStore/index'
|
|
|
|
import { MqttContentErrorEnum } from '@/enums/mqttEnum'
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
elIndex: {
|
|
|
|
type: Number,
|
|
|
|
default: 0,
|
|
|
|
},
|
|
|
|
height: {
|
|
|
|
type: String,
|
|
|
|
default: 'calc(100% - 120px)',
|
|
|
|
},
|
|
|
|
list: {
|
|
|
|
type: Array as PropType<MessageItem[]>,
|
|
|
|
default: () => [],
|
|
|
|
},
|
|
|
|
aiWidthType: {
|
|
|
|
type: String as PropType<'auto' | 'full'>,
|
|
|
|
default: 'full',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
const emit = defineEmits(['onScrollTop', 'reloadMessage'])
|
|
|
|
|
|
|
|
defineExpose({ seamlessScrollToTop, getElementOffsetTop })
|
|
|
|
|
|
|
|
const messageStore = useMessageStore()
|
|
|
|
|
|
|
|
const messageRef = ref<any>(null)
|
|
|
|
const filstDivTop = ref(0)
|
|
|
|
|
|
|
|
const defaultScrollTop = ref(0) // 默认滚动距离
|
|
|
|
const isAutoScroll = ref(true) // 是否自动滚动
|
|
|
|
const conversationData = computed(() => messageStore.getConversationData)
|
|
|
|
const throttleSetHrefTarget = throttle(setHrefTarget, 500)
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => props.list[props.list.length - 1],
|
|
|
|
() => {
|
|
|
|
throttleSetHrefTarget()
|
|
|
|
if (isAutoScroll.value) {
|
|
|
|
scrollToBottom()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ immediate: true },
|
|
|
|
)
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => props.list.length,
|
|
|
|
() => {
|
|
|
|
throttleSetHrefTarget()
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 滚动到底部
|
|
|
|
*/
|
|
|
|
async function scrollToBottom() {
|
|
|
|
if (!messageRef.value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
if (messageRef.value) {
|
|
|
|
defaultScrollTop.value = messageRef.value.scrollTop
|
|
|
|
messageRef.value.scrollTo(0, messageRef.value.scrollHeight)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 监听滚动
|
|
|
|
*/
|
|
|
|
function onScroll() {
|
|
|
|
if (!messageRef.value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// 滚动距离 - 默认距离 > -1px,停止自动滚动
|
|
|
|
if (messageRef.value.scrollTop - defaultScrollTop.value > -1) {
|
|
|
|
isAutoScroll.value = true
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
isAutoScroll.value = false
|
|
|
|
}
|
|
|
|
emit('onScrollTop', messageRef.value.scrollTop)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 上滑加载无缝滚动
|
|
|
|
*/
|
|
|
|
async function seamlessScrollToTop() {
|
|
|
|
if (!messageRef.value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
messageRef.value.scrollTo(0, filstDivTop.value)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 获取某个元素的滚动条位置
|
|
|
|
*/
|
|
|
|
async function getElementOffsetTop() {
|
|
|
|
if (messageRef.value) {
|
|
|
|
filstDivTop.value = messageRef.value.children[props.elIndex].offsetTop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 重新回答
|
|
|
|
*/
|
|
|
|
function reloadMessage() {
|
|
|
|
emit('reloadMessage')
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 给链接添加target="_blank"
|
|
|
|
*/
|
|
|
|
async function setHrefTarget() {
|
|
|
|
if (messageRef.value && props.list.length) {
|
|
|
|
await nextTick()
|
|
|
|
const links = messageRef.value.querySelectorAll('a')
|
|
|
|
links.forEach((item: { target: string }) => {
|
|
|
|
item.target = '_blank'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
scrollToBottom()
|
|
|
|
throttleSetHrefTarget()
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<div ref="messageRef" class="app-message" @scroll="onScroll">
|
|
|
|
<div v-for="(item, index) in list" :key="item.time + index" class="item">
|
|
|
|
<div v-if="item.messageType === MessageTypeEnum.USER" class="user">
|
|
|
|
<div class="content">
|
|
|
|
<MdPreview
|
|
|
|
:editor-id="`preview-only-user${index}${Date.now()}`"
|
|
|
|
:model-value="item.content"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<img class="icon-user" src="@/assets/images/conversation/user.png" alt="">
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
v-if="item.messageType === MessageTypeEnum.AI || item.messageType === MessageTypeEnum.DESCRIBE"
|
|
|
|
class="ai"
|
|
|
|
>
|
|
|
|
<img class="icon-ai" src="@/assets/images/conversation/logo.png" alt="">
|
|
|
|
<div class="content" :class="[aiWidthType === 'auto' ? 'width-auto' : 'width-full']">
|
|
|
|
<div v-if="conversationData?.type === MenuTypeEnum.TEXT_TO_IMAGE" class="pt-2 pb-2">
|
|
|
|
<Spin tip="正在生成中..." :spinning="item.messageStatus !== MessageStatusEnum.END">
|
|
|
|
<Image
|
|
|
|
v-if="!item.content.includes(MqttContentErrorEnum.SENSITIVE_WORD)"
|
|
|
|
class="mb-2"
|
|
|
|
:width="400"
|
|
|
|
:height="400"
|
|
|
|
:src="item.content"
|
|
|
|
fallback="data:image/png;base64,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
|
|
|
|
></Image>
|
|
|
|
</Spin>
|
|
|
|
<!-- <p v-if="!item.content.includes('http')" class="mb-0">
|
|
|
|
{{ item.content }}
|
|
|
|
</p> -->
|
|
|
|
</div>
|
|
|
|
<div v-else>
|
|
|
|
<MdPreview
|
|
|
|
:editor-id="`preview-only-ai${index}${Date.now()}`"
|
|
|
|
:model-value="item.content"
|
|
|
|
/>
|
|
|
|
<div v-if="item.messageStatus !== MessageStatusEnum.END" class="flex items-center pt-1 text-stone-600">
|
|
|
|
<Spin></Spin>
|
|
|
|
<span class="ml-2 mb-1">正在思考中 . . .</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div v-if="item.messageType !== MessageTypeEnum.DESCRIBE" class="w-full">
|
|
|
|
<div v-if="item.messageStatus === MessageStatusEnum.ERROR" class="error">
|
|
|
|
哎呀,出问题了...
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="item.messageStatus === MessageStatusEnum.END" class="btns">
|
|
|
|
<div class="copy" @click="copyText(item.content)">
|
|
|
|
<SvgIcon class="icon" name="copy"></SvgIcon>
|
|
|
|
复制
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
v-if="
|
|
|
|
index === list.length - 1
|
|
|
|
&& conversationData?.type !== MenuTypeEnum.VISUAL_ANALYSIS"
|
|
|
|
class="reload"
|
|
|
|
@click="reloadMessage"
|
|
|
|
>
|
|
|
|
<SvgIcon class="icon" name="again"></SvgIcon>
|
|
|
|
重新回答
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
@include app('message') {
|
|
|
|
height: v-bind(height);
|
|
|
|
overflow: auto;
|
|
|
|
box-sizing: border-box;
|
|
|
|
:deep(.md-editor-preview) {
|
|
|
|
font-size: 14px;
|
|
|
|
}
|
|
|
|
:deep(.md-editor-preview-wrapper) {
|
|
|
|
padding: 0;
|
|
|
|
.text-to-image {
|
|
|
|
width: 30%;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.item {
|
|
|
|
padding: 0 15px;
|
|
|
|
margin-top: 20px;
|
|
|
|
.user,
|
|
|
|
.ai {
|
|
|
|
display: flex;
|
|
|
|
}
|
|
|
|
.user {
|
|
|
|
justify-content: flex-end;
|
|
|
|
.content {
|
|
|
|
max-width: calc(100% - 110px);
|
|
|
|
padding: 5px 15px;
|
|
|
|
background: #edf3ff;
|
|
|
|
border-radius: 10px;
|
|
|
|
:deep(.md-editor-preview-wrapper) {
|
|
|
|
img {
|
|
|
|
width: 400px;
|
|
|
|
display: block;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
div {
|
|
|
|
background: #edf3ff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.icon-user {
|
|
|
|
width: 40px;
|
|
|
|
height: 40px;
|
|
|
|
margin-left: 15px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.ai {
|
|
|
|
position: relative;
|
|
|
|
margin-bottom: 15px;
|
|
|
|
display: flex;
|
|
|
|
flex-wrap: wrap;
|
|
|
|
.icon-ai {
|
|
|
|
width: 40px;
|
|
|
|
height: 40px;
|
|
|
|
margin-right: 15px;
|
|
|
|
}
|
|
|
|
.icon {
|
|
|
|
margin-right: 5px;
|
|
|
|
}
|
|
|
|
.content {
|
|
|
|
padding: 5px 15px;
|
|
|
|
background: #ffffff;
|
|
|
|
// box-shadow: 0px 2px 10px 0px rgba(128, 135, 152, 0.4);
|
|
|
|
border-radius: 10px;
|
|
|
|
border: 1px solid #d8dde7;
|
|
|
|
}
|
|
|
|
.width-auto {
|
|
|
|
max-width: calc(100% - 110px);
|
|
|
|
}
|
|
|
|
.width-full {
|
|
|
|
width: calc(100% - 110px);
|
|
|
|
}
|
|
|
|
.error {
|
|
|
|
width: 100%;
|
|
|
|
color: red;
|
|
|
|
height: 30px;
|
|
|
|
padding-left: 60px;
|
|
|
|
margin-top: 10px;
|
|
|
|
}
|
|
|
|
.btns {
|
|
|
|
width: 100%;
|
|
|
|
height: 30px;
|
|
|
|
padding-left: 60px;
|
|
|
|
margin: 10px auto 0 auto;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
.copy {
|
|
|
|
color: #4670e3;
|
|
|
|
margin-right: 20px;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
.reload {
|
|
|
|
color: #4670e3;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|