From e7b4dbf658552ce7a66caa742bd55a75ac4d82e5 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 08 五月 2026 14:54:04 +0800
Subject: [PATCH] refactor(AIChatSidebar): 重构助理配置结构

---
 src/components/AIChatSidebar/index.vue |  118 +++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 106 insertions(+), 12 deletions(-)

diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index 09632d6..a5dcf0b 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -191,7 +191,8 @@
                   <div
                       v-for="(file, fileIndex) in message.localUploadFiles"
                       :key="`${file.previewId || file.name}-${fileIndex}`"
-                      class="message-local-file-item"
+                      :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]"
+                      @click="handleMessageFileClick(file)"
                   >
                     <el-image
                         v-if="file.isImage && file.previewUrl"
@@ -205,8 +206,14 @@
                     />
                     <el-icon v-else class="message-local-file-icon"><Document /></el-icon>
                     <div class="message-local-file-meta">
-                      <span class="message-local-file-name">{{ file.name }}</span>
-                      <small class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
+                      <span
+                          :class="['message-local-file-name', { clickable: !!file.accessUrl }]"
+                          :title="file.name"
+                          @click.stop="openMessageAttachment(file)"
+                      >
+                        {{ file.name }}
+                      </span>
+                      <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
                     </div>
                   </div>
                 </div>
@@ -773,6 +780,34 @@
 const loadingSessions = ref(false)
 
 const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/')
+const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i
+
+const getPathnameFromFilePath = (filePath = '') => {
+  const rawPath = String(filePath || '').trim()
+  if (!rawPath) return ''
+  try {
+    const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
+    return new URL(rawPath, baseOrigin).pathname || ''
+  } catch (err) {
+    return rawPath.split('?')[0]
+  }
+}
+
+const isImageFilePath = (filePath = '') => {
+  const pathname = getPathnameFromFilePath(filePath).toLowerCase()
+  return imageFilePathPattern.test(pathname)
+}
+
+const getHistoryFileName = (filePath = '', index = 0) => {
+  const pathname = getPathnameFromFilePath(filePath)
+  const fileName = pathname.split('/').filter(Boolean).pop()
+  if (!fileName) return `file-${index + 1}`
+  try {
+    return decodeURIComponent(fileName)
+  } catch (err) {
+    return fileName
+  }
+}
 
 const getImagePreviewList = (files = []) => {
   if (!Array.isArray(files)) return []
@@ -800,14 +835,34 @@
   const fileType = rawFile?.type || ''
   const isImage = isImageFileType(fileType)
   const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function'
+  const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : ''
   return {
     previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`,
     name: rawFile?.name || `file-${index + 1}`,
     size: rawFile?.size || 0,
     type: fileType,
     isImage,
-    previewUrl: isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : '',
-    rawFile
+    previewUrl,
+    accessUrl: '',
+    rawFile,
+    isObjectUrl: !!previewUrl
+  }
+}
+
+const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => {
+  const normalizedPath = String(filePath || '').trim()
+  if (!normalizedPath) return null
+  const isImage = isImageFilePath(normalizedPath)
+  return {
+    previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`,
+    name: getHistoryFileName(normalizedPath, fileIndex),
+    size: 0,
+    type: '',
+    isImage,
+    previewUrl: isImage ? normalizedPath : '',
+    accessUrl: normalizedPath,
+    rawFile: null,
+    isObjectUrl: false
   }
 }
 
@@ -816,10 +871,29 @@
   if (!canRevokeObjectURL) return
   if (!Array.isArray(snapshots)) return
   snapshots.forEach((snapshot) => {
-    if (snapshot?.previewUrl) {
+    if (snapshot?.isObjectUrl && snapshot?.previewUrl) {
       URL.revokeObjectURL(snapshot.previewUrl)
     }
   })
+}
+
+const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => {
+  if (!Array.isArray(filePaths)) return []
+  return filePaths
+    .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex))
+    .filter(Boolean)
+}
+
+const openMessageAttachment = (file) => {
+  const accessUrl = String(file?.accessUrl || '').trim()
+  if (!accessUrl) return
+  if (typeof window === 'undefined' || typeof window.open !== 'function') return
+  window.open(accessUrl, '_blank', 'noopener,noreferrer')
+}
+
+const handleMessageFileClick = (file) => {
+  if (!file?.accessUrl || file?.isImage) return
+  openMessageAttachment(file)
 }
 
 const revokeMessageLocalFileSnapshots = (messageList = []) => {
@@ -904,7 +978,7 @@
 
         const messageObj = {
           isUser,
-          content: msg.content,
+          content: msg.content || '',
           htmlContent: '',
           isTyping: false,
           chartOptions: null,
@@ -912,7 +986,8 @@
           type: '',
           tableData: null,
           payloadTreeData: null,
-          payloadHiddenData: null
+          payloadHiddenData: null,
+          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
         }
 
         messages.value.push(messageObj)
@@ -927,15 +1002,15 @@
           }
 
           // 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
-          const extracted = extractEmbeddedSuccessJson(msg.content)
+          const extracted = extractEmbeddedSuccessJson(msg.content || '')
           if (extracted) {
             applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
           }
 
-          updateOutputState(msg.content, botMsgIndex)
-          messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
+          updateOutputState(msg.content || '', botMsgIndex)
+          messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex)
         } else {
-          messageObj.htmlContent = convertTextToHtml(msg.content)
+          messageObj.htmlContent = convertTextToHtml(msg.content || '')
         }
       })
       scrollToBottom()
@@ -3451,6 +3526,16 @@
       border: 1px solid rgba(88, 117, 255, 0.2);
       background: rgba(255, 255, 255, 0.9);
       max-width: 100%;
+
+      &.clickable {
+        cursor: pointer;
+        transition: all 0.2s ease;
+
+        &:hover {
+          border-color: rgba(44, 109, 255, 0.38);
+          background: rgba(243, 247, 255, 0.96);
+        }
+      }
     }
 
     .message-local-file-thumb {
@@ -3489,6 +3574,15 @@
       white-space: nowrap;
       overflow: hidden;
       text-overflow: ellipsis;
+
+      &.clickable {
+        color: $primary-blue;
+        cursor: pointer;
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
     }
 
     .message-local-file-size {

--
Gitblit v1.9.3