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.
230 lines
5.6 KiB
230 lines
5.6 KiB
<script setup lang="ts"> |
|
import { nextTick, onMounted, ref, watch } from '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 { copyText } from '@/utils/copyTextToClipboard' |
|
|
|
const props = defineProps({ |
|
list: { |
|
type: Array as PropType<MessageItem[]>, |
|
default: () => [], |
|
}, |
|
}) |
|
|
|
const emit = defineEmits(['onScrollTop', 'reloadMessage']) |
|
|
|
defineExpose({ getScrollTopDistance, seamlessScrollToTop }) |
|
|
|
const messageRef = ref<Element | null>(null) |
|
const elHeight = ref(0) // 旧列表的高度 |
|
const defaultScrollTop = ref(0) // 默认滚动距离 |
|
const isAutoScroll = ref(true) // 是否自动滚动 |
|
|
|
watch( |
|
() => props.list[props.list.length - 1], |
|
() => { |
|
isAutoScroll.value && scrollToBottom() |
|
}, |
|
{ immediate: true }, |
|
) |
|
|
|
/** |
|
* @description: 滚动到底部 |
|
*/ |
|
async function scrollToBottom() { |
|
if (!messageRef.value) { |
|
return |
|
} |
|
await nextTick() |
|
defaultScrollTop.value = messageRef.value.scrollTop |
|
messageRef.value.scrollTo(0, messageRef.value.scrollHeight) |
|
} |
|
|
|
/** |
|
* @description: 监听滚动 |
|
*/ |
|
function onScroll() { |
|
if (!messageRef.value) { |
|
return |
|
} |
|
|
|
// 滚动距离 - 默认距离 > 20px,停止自动滚动 |
|
if (messageRef.value.scrollTop - defaultScrollTop.value > 20) { |
|
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" class="ai"> |
|
<img class="icon-ai" src="@/assets/images/conversation/logo.png" alt=""> |
|
<div class="content"> |
|
<MdPreview editor-id="preview-only-ai" :model-value="item.content" /> |
|
</div> |
|
<div v-if="item.messageStatus === MessageStatusEnum.ERROR" class="error"> |
|
哎呀,出问题了... |
|
</div> |
|
<div |
|
v-if="item.messageStatus === MessageStatusEnum.ACTICON" |
|
class="stop" |
|
> |
|
<SvgIcon class="icon" name="stop"></SvgIcon> |
|
停止回答 |
|
</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> |
|
</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; |
|
} |
|
.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 { |
|
width: calc(100% - 110px); |
|
padding: 10px 15px; |
|
background: #ffffff; |
|
box-shadow: 0px 2px 10px 0px rgba(128, 135, 152, 0.4); |
|
border-radius: 10px; |
|
border: 1px solid #e3ebfc; |
|
} |
|
.error { |
|
color: red; |
|
height: 30px; |
|
padding-left: 60px; |
|
margin-top: 10px; |
|
} |
|
.stop { |
|
height: 30px; |
|
color: #4670e3; |
|
padding-left: 60px; |
|
margin-top: 10px; |
|
cursor: pointer; |
|
} |
|
.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>
|
|
|