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