|
|
|
<script setup lang="ts">
|
|
|
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
|
|
import { Image } from 'ant-design-vue'
|
|
|
|
import { MdPreview } from 'md-editor-v3'
|
|
|
|
import 'md-editor-v3/lib/style.css'
|
|
|
|
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'
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
list: {
|
|
|
|
type: Array as PropType<MessageItem[]>,
|
|
|
|
default: () => [],
|
|
|
|
},
|
|
|
|
aiWidthType: {
|
|
|
|
type: String as PropType<'auto' | 'full'>,
|
|
|
|
default: 'full',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
const emit = defineEmits(['onScrollTop', 'reloadMessage'])
|
|
|
|
|
|
|
|
defineExpose({ getScrollTopDistance, seamlessScrollToTop })
|
|
|
|
|
|
|
|
const messageStore = useMessageStore()
|
|
|
|
|
|
|
|
const messageRef = ref<Element | null>(null)
|
|
|
|
const elHeight = ref(0) // 旧列表的高度
|
|
|
|
const defaultScrollTop = ref(0) // 默认滚动距离
|
|
|
|
const isAutoScroll = ref(true) // 是否自动滚动
|
|
|
|
const conversationData = computed(() => messageStore.getConversationData)
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => props.list[props.list.length - 1],
|
|
|
|
() => {
|
|
|
|
if (isAutoScroll.value) {
|
|
|
|
scrollToBottom()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ immediate: true },
|
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
}
|
|
|
|
await getScrollTopDistance()
|
|
|
|
messageRef.value.scrollTo(0, elHeight.value)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 获取前10个元素的总高度,用于无缝滚动顶部的距离
|
|
|
|
* @param size 获取的元素个数,默认为10
|
|
|
|
*/
|
|
|
|
async function getScrollTopDistance(size = 10) {
|
|
|
|
if (!messageRef.value || messageRef.value.children.length < size) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
elHeight.value = 0
|
|
|
|
Array.from(messageRef.value.children).slice(0, size).forEach((item) => {
|
|
|
|
elHeight.value = elHeight.value + item.clientHeight
|
|
|
|
})
|
|
|
|
|
|
|
|
// +130 主要是做了个补充
|
|
|
|
elHeight.value += 130
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description: 重新回答
|
|
|
|
*/
|
|
|
|
function reloadMessage() {
|
|
|
|
emit('reloadMessage')
|
|
|
|
}
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
scrollToBottom()
|
|
|
|
})
|
|
|
|
</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">
|
|
|
|
{{ 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']">
|
|
|
|
<MdPreview
|
|
|
|
v-if="
|
|
|
|
conversationData?.type === MenuTypeEnum.TEXT_TO_TEXT
|
|
|
|
|| conversationData?.type === MenuTypeEnum.ROLE
|
|
|
|
|| conversationData?.type === MenuTypeEnum.REPOSITORY
|
|
|
|
"
|
|
|
|
editor-id="preview-only-ai"
|
|
|
|
:model-value="item.content"
|
|
|
|
/>
|
|
|
|
<div v-if="conversationData?.type === MenuTypeEnum.TEXT_TO_IMAGE">
|
|
|
|
<Image
|
|
|
|
:width="200"
|
|
|
|
:height="200"
|
|
|
|
:src="item.content"
|
|
|
|
fallback="data:image/png;base64,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"
|
|
|
|
></Image>
|
|
|
|
<p v-if="!item.content.includes('http')">
|
|
|
|
{{ item.content }}
|
|
|
|
</p>
|
|
|
|
</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" 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: calc(100% - 120px);
|
|
|
|
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: 10px 15px;
|
|
|
|
background: #edf3ff;
|
|
|
|
border-radius: 10px;
|
|
|
|
}
|
|
|
|
.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: 10px 15px;
|
|
|
|
background: #ffffff;
|
|
|
|
box-shadow: 0px 2px 10px 0px rgba(128, 135, 152, 0.4);
|
|
|
|
border-radius: 10px;
|
|
|
|
border: 1px solid #e3ebfc;
|
|
|
|
}
|
|
|
|
.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>
|