From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目
---
src/views/ai/chat/index/components/message/MessageList.vue | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 226 insertions(+), 0 deletions(-)
diff --git a/src/views/ai/chat/index/components/message/MessageList.vue b/src/views/ai/chat/index/components/message/MessageList.vue
new file mode 100644
index 0000000..da5c338
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageList.vue
@@ -0,0 +1,226 @@
+<template>
+ <div ref="messageContainer" class="h-100% overflow-y-auto relative">
+ <div class="flex flex-col overflow-y-hidden px-20px" v-for="(item, index) in list" :key="index">
+ <!-- 闈犲乏 message锛歴ystem銆乤ssistant 绫诲瀷 -->
+ <div class="flex flex-row mt-50px" v-if="item.type !== 'user'">
+ <div class="avatar">
+ <el-avatar :src="roleAvatar" />
+ </div>
+ <div class="flex flex-col text-left mx-15px">
+ <div>
+ <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
+ </div>
+ <div
+ class="relative flex flex-col break-words bg-[var(--el-fill-color-light)] shadow-[0_0_0_1px_var(--el-border-color-light)] rounded-10px pt-10px px-10px pb-5px"
+ ref="markdownViewRef"
+ >
+ <MessageReasoning
+ :reasoning-content="item.reasoningContent || ''"
+ :content="item.content || ''"
+ />
+ <MarkdownView
+ class="text-[var(--el-text-color-primary)] text-[0.95rem]"
+ :content="item.content"
+ />
+ <MessageFiles :attachment-urls="item.attachmentUrls" />
+ <MessageKnowledge v-if="item.segments" :segments="item.segments" />
+ <MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" />
+ </div>
+ <div class="flex flex-row mt-8px">
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="copyContent(item.content)"
+ >
+ <img class="h-20px" src="@/assets/ai/copy.svg" />
+ </el-button>
+ <el-button
+ v-if="item.id > 0"
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onDelete(item.id)"
+ >
+ <img class="h-17px" src="@/assets/ai/delete.svg" />
+ </el-button>
+ </div>
+ </div>
+ </div>
+ <!-- 闈犲彸 message锛歶ser 绫诲瀷 -->
+ <div class="flex flex-row-reverse justify-start mt-50px" v-if="item.type === 'user'">
+ <div class="avatar">
+ <el-avatar :src="userAvatar" />
+ </div>
+ <div class="flex flex-col text-left mx-15px">
+ <div>
+ <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
+ </div>
+ <!-- 闄勪欢鏄剧ず琛� -->
+ <div
+ v-if="item.attachmentUrls && item.attachmentUrls.length > 0"
+ class="flex flex-row-reverse mb-8px"
+ >
+ <MessageFiles :attachment-urls="item.attachmentUrls" />
+ </div>
+ <!-- 鏂囨湰鍐呭琛� -->
+ <div class="flex flex-row-reverse">
+ <div
+ v-if="item.content && item.content.trim()"
+ class="text-[0.95rem] text-[var(--el-color-white)] inline bg-[var(--el-color-primary)] shadow-[0_0_0_1px_var(--el-color-primary)] rounded-10px p-10px w-auto break-words whitespace-pre-wrap"
+ >
+ {{ item.content }}
+ </div>
+ </div>
+ <div class="flex flex-row-reverse mt-8px">
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="copyContent(item.content)"
+ >
+ <img class="h-20px" src="@/assets/ai/copy.svg" />
+ </el-button>
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onDelete(item.id)"
+ >
+ <img class="h-17px mr-12px" src="@/assets/ai/delete.svg" />
+ </el-button>
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onRefresh(item)"
+ >
+ <el-icon size="17"><RefreshRight /></el-icon>
+ </el-button>
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onEdit(item)"
+ >
+ <el-icon size="17"><Edit /></el-icon>
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- 鍥炲埌搴曢儴 -->
+ <div v-if="isScrolling" class="absolute z-1000 bottom-0 right-50%" @click="handleGoBottom">
+ <el-button :icon="ArrowDownBold" circle />
+ </div>
+</template>
+<script setup lang="ts">
+import { PropType } from 'vue'
+import { formatDate } from '@/utils/formatTime'
+import MarkdownView from '@/components/MarkdownView/index.vue'
+import MessageKnowledge from './MessageKnowledge.vue'
+import MessageReasoning from './MessageReasoning.vue'
+import MessageFiles from './MessageFiles.vue'
+import MessageWebSearch from './MessageWebSearch.vue'
+import { useClipboard } from '@vueuse/core'
+import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
+import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
+import { ChatConversationVO } from '@/api/ai/chat/conversation'
+import { useUserStore } from '@/store/modules/user'
+import userAvatarDefaultImg from '@/assets/imgs/avatar.gif'
+import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { copy } = useClipboard({ legacy: true }) // 鍒濆鍖� copy 鍒扮矘璐存澘
+const userStore = useUserStore()
+
+// 鍒ゆ柇鈥滄秷鎭垪琛ㄢ�濇粴鍔ㄧ殑浣嶇疆(鐢ㄤ簬鍒ゆ柇鏄惁闇�瑕佹粴鍔ㄥ埌娑堟伅鏈�涓嬫柟)
+const messageContainer: any = ref(null)
+const isScrolling = ref(false) //鐢ㄤ簬鍒ゆ柇鐢ㄦ埛鏄惁鍦ㄦ粴鍔�
+
+const userAvatar = computed(() => userStore.user.avatar || userAvatarDefaultImg)
+const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
+
+// 瀹氫箟 props
+const props = defineProps({
+ conversation: {
+ type: Object as PropType<ChatConversationVO>,
+ required: true
+ },
+ list: {
+ type: Array as PropType<ChatMessageVO[]>,
+ required: true
+ }
+})
+
+const { list } = toRefs(props) // 娑堟伅鍒楄〃
+
+const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 瀹氫箟 emits
+
+// ============ 澶勭悊瀵硅瘽婊氬姩 ==============
+
+/** 婊氬姩鍒板簳閮� */
+const scrollToBottom = async (isIgnore?: boolean) => {
+ // 娉ㄦ剰瑕佷娇鐢� nextTick 浠ュ厤鑾峰彇涓嶅埌 dom
+ await nextTick()
+ if (isIgnore || !isScrolling.value) {
+ messageContainer.value.scrollTop =
+ messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
+ }
+}
+
+function handleScroll() {
+ const scrollContainer = messageContainer.value
+ const scrollTop = scrollContainer.scrollTop
+ const scrollHeight = scrollContainer.scrollHeight
+ const offsetHeight = scrollContainer.offsetHeight
+ if (scrollTop + offsetHeight < scrollHeight - 100) {
+ // 鐢ㄦ埛寮�濮嬫粴鍔ㄥ苟鍦ㄦ渶搴曢儴涔嬩笂锛屽彇娑堜繚鎸佸湪鏈�搴曢儴鐨勬晥鏋�
+ isScrolling.value = true
+ } else {
+ // 鐢ㄦ埛鍋滄婊氬姩骞舵粴鍔ㄥ埌鏈�搴曢儴锛屽紑鍚繚鎸佸埌鏈�搴曢儴鐨勬晥鏋�
+ isScrolling.value = false
+ }
+}
+
+/** 鍥炲埌搴曢儴 */
+const handleGoBottom = async () => {
+ const scrollContainer = messageContainer.value
+ scrollContainer.scrollTop = scrollContainer.scrollHeight
+}
+
+/** 鍥炲埌椤堕儴 */
+const handlerGoTop = async () => {
+ const scrollContainer = messageContainer.value
+ scrollContainer.scrollTop = 0
+}
+
+defineExpose({ scrollToBottom, handlerGoTop }) // 鎻愪緵鏂规硶缁� parent 璋冪敤
+
+// ============ 澶勭悊娑堟伅鎿嶄綔 ==============
+
+/** 澶嶅埗 */
+const copyContent = async (content: string) => {
+ await copy(content)
+ message.success('澶嶅埗鎴愬姛锛�')
+}
+
+/** 鍒犻櫎 */
+const onDelete = async (id) => {
+ // 鍒犻櫎 message
+ await ChatMessageApi.deleteChatMessage(id)
+ message.success('鍒犻櫎鎴愬姛锛�')
+ // 鍥炶皟
+ emits('onDeleteSuccess')
+}
+
+/** 鍒锋柊 */
+const onRefresh = async (message: ChatMessageVO) => {
+ emits('onRefresh', message)
+}
+
+/** 缂栬緫 */
+const onEdit = async (message: ChatMessageVO) => {
+ emits('onEdit', message)
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ messageContainer.value.addEventListener('scroll', handleScroll)
+})
+</script>
--
Gitblit v1.8.0