Просмотр исходного кода

[Feat 0000]智能问答文件预览 图片渲染功能优化

bobo04052021@163.com 4 месяцев назад
Родитель
Сommit
0190da98a9
2 измененных файлов с 288 добавлено и 100 удалено
  1. 8 1
      package-lock.json
  2. 280 99
      src/components/AIChat/MiniChat.vue

+ 8 - 1
package-lock.json

@@ -2665,7 +2665,9 @@
       }
     },
     "@liveqing/liveplayer-v3": {
-      "version": "3.7.19"
+      "version": "3.7.38",
+      "resolved": "https://registry.npmmirror.com/@liveqing/liveplayer-v3/-/liveplayer-v3-3.7.38.tgz",
+      "integrity": "sha512-lmqrZHeP1RaadQL9K++gFf/67EdPI11MUNB9dpIehFmMchy1D10GYUp34aR94EZMqIl4V36EE49DzEHysD9FSA=="
     },
     "@ljharb/through": {
       "version": "2.3.14",
@@ -9609,6 +9611,11 @@
       "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
       "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="
     },
+    "hls.js": {
+      "version": "1.6.15",
+      "resolved": "https://registry.npmmirror.com/hls.js/-/hls.js-1.6.15.tgz",
+      "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA=="
+    },
     "homedir-polyfill": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",

+ 280 - 99
src/components/AIChat/MiniChat.vue

@@ -79,7 +79,37 @@
             <template v-if="message.type === 'user'">
               <div class="flex-grow-1"></div>
               <div class="message-wrapper user-message-wrapper">
-                <div class="ask-message">{{ message.parsedContent }}</div>
+                <!-- <div class="ask-message" v-if="message.message_files && message.message_files.length > 0" @click="showPreview()">
+                  <div class="img-preview" v-if="askFileType == 'image'">
+                    <img :src="askFile.source_url" alt="" />
+                  </div>
+                  <div class="file-preview" v-else-if="askFileType !== 'image' && askFileType != ''">
+                    <div class="file-info"> 📄{{ askFile.name }} (大小:{{ (askFile.size / 1024).toFixed(2) }}KB) </div>
+                  </div>
+                  <div>
+                    {{ message.parsedContent }}
+                  </div>
+                </div>
+                <div class="ask-message" v-else @click="showPreview()">
+                  <div>
+                    {{ message.parsedContent }}
+                  </div>
+                </div> -->
+                <div class="ask-message">
+                  <!-- 渲染当前消息的文件 -->
+                  <div v-if="message.message_files && message.message_files.length > 0" @click="showPreview(message.message_files)">
+                    <div v-for="(file, fIdx) in message.message_files" :key="fIdx">
+                      <div class="img-preview" v-if="file.mime_type?.toLowerCase().includes('image')">
+                        <img :src="file.source_url ? file.source_url : file.url" alt="" />
+                      </div>
+                      <div class="file-preview" v-else>
+                        <div class="file-info"> 📄{{ file.name }} (大小:{{ (file.size / 1024).toFixed(2) }}KB) </div></div
+                      >
+                    </div>
+                  </div>
+                  <!-- 消息内容 -->
+                  <div>{{ message.parsedContent }}</div>
+                </div>
                 <div class="copy-icon-container">
                   <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
                   <EditOutlined class="copy-icon" @click="editAsk(message.parsedContent)" title="重新编辑" />
@@ -99,6 +129,12 @@
                     <div v-show="message.isShowThink" class="color-gray font-size-12px" v-html="formatMessage(message.parsedContentR1)"></div>
                   </div>
                   <div v-if="message.parsedContent" v-html="formatMessage(message.parsedContent)"> </div>
+                  <div
+                    id="chart-target"
+                    class="echarts-container"
+                    style="width: 400px; height: 400px"
+                    v-if="message.echartsOption && Object.keys(echartsOption).length > 0"
+                  ></div>
                 </div>
                 <div class="copy-icon-container">
                   <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
@@ -115,13 +151,22 @@
         </div>
         <!-- 底部输入区 -->
         <div class="input-area">
-          <!-- <div class="file-preview" v-if="currentFile">
+          <div class="img-preview" v-if="uploadfileType == 'image'">
+            <img :src="uploadedFiles.source_url" alt="" />
+          </div>
+          <div class="file-preview" v-else-if="uploadedFiles.name && uploadedFiles.size && uploadfileType !== 'image'">
             <div class="file-info">
-              📄 已选择文件:{{ currentFile.name }} (大小:{{ (currentFile.size / 1024).toFixed(2) }}KB)
-              <button @click="clearFile" class="clear-btn">×</button>
+              📄{{ uploadedFiles.name }} (大小:{{ (uploadedFiles.size / 1024).toFixed(2) }}KB)
+              <DeleteOutlined @click="deleteFile()"></DeleteOutlined>
             </div>
-          </div> -->
-          <a-textarea v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" auto-size />
+          </div>
+          <a-textarea
+            auto-size
+            v-model:value="inputText"
+            placeholder="请输入你的问题"
+            @keyup.enter="handleSend(inputText, uploadedFiles)"
+            class="ant-input"
+          />
           <div class="ctrl-btn">
             <div class="input-controls">
               <button class="control-btn" :class="{ active: isThinking }" @click="toggleThinking">深度思考</button>
@@ -134,7 +179,7 @@
                     <SvgIcon name="send-file" />
                   </template>
                 </Button>
-                <Button class="control-btn2" size="small" @click="handleSend(inputText)">
+                <Button class="control-btn2" size="small" @click="handleSend(inputText, uploadedFiles)">
                   <template #icon>
                     <SvgIcon name="send" />
                   </template>
@@ -152,7 +197,6 @@
               :before-upload="handleBeforeUpload"
               :file-list="fileList"
               :remove="handleRemove"
-              accept=".pdf,.docx,.xlsx,.xls"
             >
               <a-button class="upload-btn">
                 <UploadOutlined></UploadOutlined>
@@ -165,18 +209,6 @@
     </div>
     <div class="doc" v-if="isShowDoc">
       <div class="close"> <button class="closeBtn" @click="close">关闭</button></div>
-      <!-- 已上传文件列表 -->
-      <div class="file-list" v-if="uploadedFiles.length">
-        <div class="file-item" v-for="file in uploadedFiles" :key="file.id">
-          <div class="file-info">
-            <div class="file-name" :title="file.name">{{ file.name }}</div>
-          </div>
-          <div class="file-actions">
-            <button class="btn btn-preview" @click="previewFile(file)"> <i class="fas fa-eye"></i> 预览 </button>
-            <button class="btn btn-delete" @click="deleteFile(file.id)"> <i class="fas fa-trash"></i> 删除 </button>
-          </div>
-        </div>
-      </div>
       <!-- 预览内容 -->
       <div class="pre-container">
         <!-- PDF预览 -->
@@ -191,6 +223,14 @@
           @rendered="handleRendered"
           @error="handleError"
         />
+        <img
+          class="img-container"
+          v-else-if="
+            fileType === 'jpg' || fileType === 'jpeg' || fileType === 'png' || fileType === 'gjf' || fileType === 'webp' || fileType === 'svg'
+          "
+          :src="fileUrl"
+          alt=""
+        />
         <!-- 不支持的文件类型 -->
         <div v-else class="unsupported">
           <p>不支持预览该文件类型: {{ fileType }}</p>
@@ -208,7 +248,7 @@
 import VueOfficePdf from '@vue-office/pdf/lib/v3/vue-office-pdf.mjs';
 import VueOfficeDocx from '@vue-office/docx/lib/v3/vue-office-docx.mjs';
 import VueOfficeExcel from '@vue-office/excel/lib/v3/vue-office-excel.mjs';
-import { ref, onMounted, nextTick } from 'vue';
+import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue';
 import { SvgIcon } from '../Icon';
 import { Space, Button, Modal, Input, message } from 'ant-design-vue';
 // import AIChat from './index.vue';
@@ -227,6 +267,7 @@ import { createVNode } from 'vue';
 import { marked } from 'marked';
 import katex from 'katex';
 import 'katex/dist/katex.min.css';
+import * as echarts from 'echarts';
 const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
 const inputText = ref(''); // 输入框内容
 const refreshText = ref(''); //重新生成文本
@@ -257,17 +298,69 @@ interface Message {
   parsedContentR1: String; // 解析后的思考过程 HTML
   timestamp: number; // 排序依据
   isShowThink: boolean; //深度思考展示
+  message_files?: Array<[]>; //文件信息
+  echartsOption?: Object; //echart图表
 }
 // 定义消息历史数组类型
 const messageHistory = ref<Message[]>([]);
 const isFold = ref(true); // 是否折叠
 const userid = useUserStore().getUserInfo.id as string;
-const filePath = ref(''); // 绑定输入框值
-const uploadedFiles = ref([]);
-const showConfirmBtn = ref(false); // 控制确认按钮显示状态
+const uploadedFiles = ref({}); //接口获取文件信息
+const askFile = ref({});
+const uploadfileType = ref(''); //提问框文件展示
+const askFileType = ref(''); //回答区域文件展示
+const message_files = ref([]);
 const fileList = ref([]);
 const suggestList = ref([]); //建议列表
 const loading = ref(false);
+// const chartRef = ref(null);
+const validEchartsOption = ref(null);
+const optionTextRef = ref(null);
+const echartsOption = ref({});
+let myChart: echarts.EChartsType | null = null;
+const chartDom = ref<HTMLDivElement | null>(null);
+// 初始化图表
+const initChart = () => {
+  const targetNode = document.getElementById('chart-target');
+  if (!targetNode) {
+    console.error('目标插入节点不存在!');
+    return;
+  }
+
+  // 2. 创建 ECharts 容器(带唯一 id,方便后续清理)
+  const chartContainer = document.createElement('div');
+  chartContainer.id = 'dynamic-echarts-container';
+  // 必须设置宽高(继承目标节点宽高,或自定义)
+  chartContainer.style.width = '100%';
+  chartContainer.style.height = '100%';
+  chartContainer.style.border = '1px solid #eee';
+
+  // 3. 初始化 ECharts 实例到新建容器
+  myChart = echarts.init(chartContainer);
+  myChart.setOption(echartsOption.value);
+
+  // 4. 将容器插入目标节点(清空原有内容,可选)
+  targetNode.innerHTML = ''; // 清空目标节点原有内容(如需保留可注释)
+  targetNode.appendChild(chartContainer);
+
+  // 5. 监听窗口resize,自适应图表
+  window.addEventListener('resize', handleResize);
+};
+// 图表自适应方法
+const handleResize = () => {
+  myChart?.resize();
+};
+// 监听配置变化,确保DOM更新后初始化
+watch(
+  () => echartsOption.value,
+  async (newOption) => {
+    if (!newOption || Object.keys(newOption).length === 0) return;
+    await nextTick(); // 等待 DOM 渲染
+    initChart();
+  },
+  { deep: true, immediate: true }
+);
+
 // 文件预览
 const handleRendered = () => {
   loading.value = false;
@@ -277,21 +370,15 @@ const handleError = (error) => {
   loading.value = false;
   console.error('文件预览错误:', error);
 };
-function previewFile(data) {
-  fileType.value = data.name.split('.').pop().toLowerCase();
-  fileUrl.value = data.source_url;
-}
-function deleteFile(fileId) {
-  // 确认删除
-  if (confirm('确定要删除这个文件吗?')) {
-    uploadedFiles.value = uploadedFiles.value.filter((file) => file.id !== fileId);
-  }
-}
+// function previewFile(data) {
+//   fileType.value = data.extension;
+//   fileUrl.value = data.source_url;
+// }
 //启用深度思考
 const toggleThinking = () => {
   isThinking.value = !isThinking.value;
   if (isThinking.value) {
-    APIKEY.value = 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN';
+    APIKEY.value = 'Bearer app-7iLQR9T77YtwDYUYGPk1PFbi';
   } else {
     APIKEY.value = 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd';
   }
@@ -360,7 +447,7 @@ const initMarked = () => {
     breaks: false, // 禁用换行符转换
   });
 };
-// LaTeX 公式渲染(内部使用)
+// LaTeX 公式渲染
 const renderLatexInHtml = (html) => {
   // 匹配块级公式($$...$$)
   const blockRegex = /\$\$(.*?)\$\$/gs;
@@ -397,6 +484,21 @@ const parseMarkdownWithLatex = (mdStr) => {
     return mdStr; // 降级显示原始字符串
   }
 };
+// echart 数据解析
+const parseEchart = (data) => {
+  const reg = /```[\s\S]*?\n([\s\S]*?)```/;
+  const match = data.match(reg);
+
+  if (match && match[1]) {
+    // 提取匹配到的JSON部分并解析
+    echartsOption.value = JSON.parse(match[1].trim());
+  }
+  return match;
+};
+const triggerChart = async () => {
+  await nextTick();
+  initChart();
+};
 //重新生成
 const refresh = () => {
   handleSend(refreshText.value);
@@ -406,11 +508,17 @@ const editAsk = (data) => {
   inputText.value = data;
 };
 //获取消息列表
-async function handleSend(data) {
+async function handleSend(data, files?) {
   refreshText.value = data;
   inputText.value = '';
+  if (files) {
+    askFileType.value = files?.mime_type?.startsWith('image') ? 'image' : 'document';
+  }
+  // message_files.value.push(files);
+  const fileList = files ? [files] : [];
+  uploadfileType.value = '';
   if (isThinking) {
-    messageHistory.value.push({
+    const newMessage = {
       id: `user_${Date.now()}`,
       type: 'user',
       content: '',
@@ -419,9 +527,12 @@ async function handleSend(data) {
       parsedContentR1: '',
       timestamp: Date.now(),
       isShowThink: true,
-    });
+      message_files: fileList,
+    };
+    messageHistory.value.push(newMessage);
+    message_files.value.push(files);
   } else {
-    messageHistory.value.push({
+    const newMessage = {
       id: `user_${Date.now()}`,
       type: 'user',
       content: '',
@@ -430,7 +541,10 @@ async function handleSend(data) {
       parsedContentR1: '',
       timestamp: Date.now(),
       isShowThink: false,
-    });
+      message_files: fileList,
+    };
+    messageHistory.value.push(newMessage);
+    message_files.value.push(files);
   }
   try {
     const response = await fetch('http://39.97.59.228:8000/v1/chat-messages', {
@@ -442,16 +556,27 @@ async function handleSend(data) {
       body: JSON.stringify({
         conversation_id: currentSessionID.value ? currentSessionID.value : '',
         query: data,
+        ...(askFile.value?.id
+          ? {
+              files: [
+                {
+                  type: askFileType.value == 'image' ? 'image' : 'document',
+                  transfer_method: 'local_file',
+                  upload_file_id: askFile.value.id,
+                  url: '',
+                },
+              ],
+            }
+          : {}),
         response_mode: 'streaming',
         user: userid,
         inputs: {},
       }),
     });
-
     if (!response.ok) {
       throw new Error(`HTTP error! status: ${response.status}`);
     }
-
+    uploadedFiles.value = {};
     const decoder = new TextDecoder('utf-8');
     const reader = response.body.getReader();
     let textBuffer = ''; // 使用字符串缓冲区来累积数据
@@ -513,7 +638,6 @@ async function handleSend(data) {
             case 'message':
               if (data.answer) {
                 const targetMessage = messageHistory.value.find((msg) => msg.id === currentProcessingMessage.value.id);
-
                 if (!targetMessage) break;
                 let currentChunk = data.answer;
 
@@ -603,6 +727,15 @@ const showModal = (data) => {
 const close = () => {
   isShowDoc.value = false;
 };
+const showPreview = (data) => {
+  fileType.value = data[0].extension ? data[0].extension : data[0].filename.split('.').pop();
+  fileUrl.value = data[0].source_url ? data[0].source_url : data[0].url;
+  isShowDoc.value = true;
+};
+//删除文件
+const deleteFile = () => {
+  uploadedFiles.value = {};
+};
 // 上传文件
 const handleBeforeUpload = async (file) => {
   const formData = new FormData();
@@ -620,11 +753,10 @@ const handleBeforeUpload = async (file) => {
 
     const result = await response.json();
     if (response.ok) {
-      const linkText = `[${result.name}](${result.source_url})`;
-      // inputText.value += `\n${linkText}`;
-      uploadedFiles.value.push(result);
-      isShowDoc.value = true;
-      previewFile(result);
+      uploadedFiles.value = result;
+      askFile.value = result;
+      uploadfileType.value = result.mime_type.startsWith('image') ? 'image' : 'document';
+      // previewFile(result);
     } else {
       message.error(`上传失败: ${result.message || '网络错误'}`);
     }
@@ -663,7 +795,7 @@ async function stopReq() {
 }
 //获取具体会话记录
 async function sessionsHistory(id: string, retryCount = 0) {
-  const API_KEYS = ['Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd', 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN'];
+  const API_KEYS = ['Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd', 'Bearer app-7iLQR9T77YtwDYUYGPk1PFbi'];
   // 最大重试次数
   const maxRetries = API_KEYS.length - 1;
   try {
@@ -689,6 +821,7 @@ async function sessionsHistory(id: string, retryCount = 0) {
           parsedContent: item.query,
           contentR1: '',
           parsedContentR1: '',
+          message_files: item.message_files,
           timestamp: Date.now(),
           isShowThink: false,
         });
@@ -696,6 +829,7 @@ async function sessionsHistory(id: string, retryCount = 0) {
         // 查找<think>
         const startIndex = answer.indexOf('<think>');
         const endIndex = answer.indexOf('</think>');
+        const isChart = answer.indexOf('echarts');
         let content = '';
         let contentR1 = '';
         // 根据标签判断
@@ -714,17 +848,33 @@ async function sessionsHistory(id: string, retryCount = 0) {
           // 没有标签
           content = answer;
         }
-        // 添加到消息历史
-        messageHistory.value.push({
-          id: `system_${Date.now()}`,
-          type: 'system',
-          content: '',
-          parsedContent: parseMarkdownWithLatex(content.trim()),
-          contentR1: '',
-          parsedContentR1: parseMarkdownWithLatex(contentR1.trim()),
-          timestamp: Date.now(),
-          isShowThink: contentR1.length > 0, // 如果有思考内容则显示
-        });
+        if (isChart != -1) {
+          // 添加到消息历史
+          messageHistory.value.push({
+            id: `system_${Date.now()}`,
+            type: 'system',
+            content: '',
+            parsedContent: '',
+            contentR1: '',
+            parsedContentR1: parseMarkdownWithLatex(contentR1.trim()),
+            timestamp: Date.now(),
+            isShowThink: contentR1.length > 0, // 如果有思考内容则显示
+            echartsOption: parseEchart(content.trim()),
+          });
+          triggerChart();
+        } else {
+          // 添加到消息历史
+          messageHistory.value.push({
+            id: `system_${Date.now()}`,
+            type: 'system',
+            content: '',
+            parsedContent: parseMarkdownWithLatex(content.trim()),
+            contentR1: '',
+            parsedContentR1: parseMarkdownWithLatex(contentR1.trim()),
+            timestamp: Date.now(),
+            isShowThink: contentR1.length > 0, // 如果有思考内容则显示
+          });
+        }
       });
     }
   } catch (error) {
@@ -742,7 +892,6 @@ async function sessionsHistory(id: string, retryCount = 0) {
 //获取下一轮建议问题列表
 //停止响应
 async function getNextSuggest() {
-  console.log(messageID.value, '123');
   try {
     let response = await fetch(`http://39.97.59.228:8000/v1/messages/${messageID.value}/suggested?user=${userid}`, {
       method: 'GET',
@@ -832,45 +981,47 @@ const fold = () => {
 async function getHistoryList() {
   sessionHistory.value = [];
   try {
-    // 并行请求两个接口,提升效率
-    const [response1, response2] = await Promise.all([
-      // 第一个请求
-      fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
-        method: 'get',
-        headers: {
-          'Content-Type': 'application/json',
-          Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
-        },
-      }),
-      // 第二个请求
-      fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
-        method: 'get',
-        headers: {
-          'Content-Type': 'application/json',
-          Authorization: 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN',
-        },
-      }),
-    ]);
-
-    // 检查响应是否成功
-    if (!response1.ok || !response2.ok) {
-      throw new Error('接口请求失败');
-    }
+    const fetch1 = fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
+      method: 'get',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      },
+    })
+      .then((res) => {
+        return res.json();
+      })
+      .catch((err) => {
+        console.error('第一个请求错误:', err);
+        return { data: [] }; // 失败时返回空数组
+      });
 
-    // 解析两个响应的JSON数据
-    const [data1, data2] = await Promise.all([response1.json(), response2.json()]);
+    const fetch2 = fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
+      method: 'get',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: 'Bearer app-7iLQR9T77YtwDYUYGPk1PFbi',
+      },
+    })
+      .then((res) => {
+        return res.json();
+      })
+      .catch((err) => {
+        console.error('第二个请求错误:', err);
+        return { data: [] }; // 失败时返回空数组
+      });
 
-    // 合并两个数组(核心修改:用扩展运算符合并)
-    // 确保 data1.data 和 data2.data 都是数组(防止接口返回异常)
+    // 等待两个请求完成
+    const [data1, data2] = await Promise.all([fetch1, fetch2]);
+    // 合并两个数组(确保是数组)
     const list1 = Array.isArray(data1.data) ? data1.data : [];
     const list2 = Array.isArray(data2.data) ? data2.data : [];
-
     // 合并数组并赋值给 sessionHistory
     sessionHistory.value = [...list1, ...list2];
-    sessionHistory.value.forEach((item) => {
-      currentSessionID.value = item.id;
-    });
-    console.log(sessionHistory.value, '历史数据');
+    // 设置当前会话ID(如果有数据)
+    if (sessionHistory.value.length > 0) {
+      currentSessionID.value = sessionHistory.value[0].id;
+    }
   } catch (error) {
     console.error('获取历史数据失败:', error);
     // 错误处理(比如提示用户)
@@ -899,6 +1050,19 @@ onMounted(() => {
   getHistoryList();
   // 初始化(仅执行一次)
   initMarked();
+  triggerChart();
+});
+onUnmounted(() => {
+  // 卸载时销毁实例,清理 DOM
+  if (myChart) {
+    myChart.dispose();
+    myChart = null;
+  }
+  const chartContainer = document.getElementById('dynamic-echarts-container');
+  if (chartContainer && chartContainer.parentNode) {
+    chartContainer.parentNode.removeChild(chartContainer);
+  }
+  window.removeEventListener('resize', handleResize);
 });
 </script>
 
@@ -1141,24 +1305,39 @@ onMounted(() => {
   }
 
   .input-area {
-    margin: 10px 10px 20px 10px;
-    padding: 10px;
+    padding: 5px;
     background-color: #043256;
     border: 1px solid #2cb6ff;
     border-radius: 5px;
     display: flex;
     flex-direction: column;
-    justify-content: space-between;
+    justify-content: space-around;
     gap: 5px;
-    height: 25%;
+    min-height: 170px;
+    height: auto;
+    margin: 5px;
+    position: relative;
   }
   /* 文件展示区域 */
+  .img-preview {
+    position: relative;
+    width: 50px;
+    height: 50px;
+  }
+  .img-preview img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    display: block;
+  }
   .file-preview {
+    position: relative;
     margin-bottom: 10px;
     padding: 8px;
     border: 1px solid #e5e7eb;
     border-radius: 4px;
     background: #f9fafb;
+    box-sizing: border-box;
   }
   .file-info {
     display: flex;
@@ -1481,6 +1660,8 @@ onMounted(() => {
   height: 100%;
   overflow: auto;
 }
+.img-container {
+}
 .vue-office-excel {
   background: #fff !important;
 }