Quellcode durchsuchen

feat: 文件管理预览增强和AI聊天代码高亮

ys vor 5 Tagen
Ursprung
Commit
48e19cb154

+ 48 - 0
yushu-backend/yushu-admin/src/main/java/com/yushu/web/controller/system/SysFileController.java

@@ -2,8 +2,10 @@ package com.yushu.web.controller.system;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.net.URLEncoder;
 import java.util.List;
 import jakarta.servlet.http.HttpServletResponse;
@@ -247,4 +249,50 @@ public class SysFileController extends BaseController
     {
         return toAjax(sysFileService.copyFile(fileId, targetFolderId));
     }
+    
+    /**
+     * 更新文件内容(文本文件)
+     */
+    @PreAuthorize("@ss.hasPermi('system:file:edit')")
+    @Log(title = "更新文件内容", businessType = BusinessType.UPDATE)
+    @PutMapping("/content/{fileId}")
+    public AjaxResult updateContent(@PathVariable Long fileId, @RequestBody String content)
+    {
+        try
+        {
+            SysFileInfo sysFile = sysFileService.selectSysFileByFileId(fileId);
+            if (sysFile == null)
+            {
+                return AjaxResult.error("文件不存在");
+            }
+            
+            // 获取文件完整路径
+            String basePath = fileStorageConfig.getLocal().getNormalizedBasePath();
+            String fullPath = basePath + File.separator + sysFile.getFilePath();
+            File file = new File(fullPath);
+            
+            if (!file.exists())
+            {
+                return AjaxResult.error("文件不存在");
+            }
+            
+            // 写入新内容
+            try (FileOutputStream fos = new FileOutputStream(file);
+                 OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"))
+            {
+                osw.write(content);
+            }
+            
+            // 更新文件大小
+            sysFile.setFileSize(file.length());
+            sysFileService.updateSysFile(sysFile);
+            
+            return AjaxResult.success("保存成功");
+        }
+        catch (Exception e)
+        {
+            logger.error("更新文件内容失败", e);
+            return AjaxResult.error("保存失败: " + e.getMessage());
+        }
+    }
 }

+ 17 - 1
yushu-uivue3/package.json

@@ -16,15 +16,29 @@
     "url": "https://gitee.com/y_project/yushu-Vue.git"
   },
   "dependencies": {
+    "@codemirror/lang-cpp": "^6.0.3",
+    "@codemirror/lang-css": "^6.3.1",
+    "@codemirror/lang-html": "^6.4.11",
+    "@codemirror/lang-java": "^6.0.2",
+    "@codemirror/lang-javascript": "^6.2.4",
+    "@codemirror/lang-json": "^6.0.2",
+    "@codemirror/lang-markdown": "^6.5.0",
+    "@codemirror/lang-php": "^6.0.2",
+    "@codemirror/lang-python": "^6.2.1",
+    "@codemirror/lang-sql": "^6.10.0",
+    "@codemirror/lang-xml": "^6.1.0",
+    "@codemirror/theme-one-dark": "^6.1.3",
     "@element-plus/icons-vue": "2.3.1",
     "@vueup/vue-quill": "1.2.0",
     "@vueuse/core": "13.3.0",
     "axios": "1.9.0",
     "clipboard": "2.0.11",
+    "codemirror": "^6.0.2",
     "echarts": "5.6.0",
     "element-plus": "2.10.7",
     "file-saver": "2.0.5",
     "fuse.js": "6.6.2",
+    "highlight.js": "^11.11.1",
     "js-beautify": "1.14.11",
     "js-cookie": "3.0.5",
     "jsencrypt": "3.3.2",
@@ -33,10 +47,12 @@
     "pinia": "3.0.2",
     "splitpanes": "4.0.4",
     "vue": "3.5.16",
+    "vue-codemirror": "^6.1.1",
     "vue-count-to": "^1.0.13",
     "vue-cropper": "1.1.1",
     "vue-router": "4.5.1",
-    "vuedraggable": "4.1.0"
+    "vuedraggable": "4.1.0",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "5.2.4",

+ 12 - 0
yushu-uivue3/src/api/system/file.js

@@ -108,6 +108,18 @@ export function downloadFile(fileId) {
   })
 }
 
+// 更新文件内容(文本文件)
+export function updateFileContent(fileId, content) {
+  return request({
+    url: '/system/file/content/' + fileId,
+    method: 'put',
+    data: content,
+    headers: {
+      'Content-Type': 'text/plain'
+    }
+  })
+}
+
 // 预览文件
 export function previewFile(fileId) {
   return import.meta.env.VITE_APP_BASE_API + '/system/file/preview/' + fileId

+ 9 - 0
yushu-uivue3/src/assets/styles/element-ui.scss

@@ -94,6 +94,15 @@
     background: transparent !important;
     padding: 24px !important;
     color: #334155;
+    
+    // 确保树形组件文字可见
+    .el-tree {
+      color: #303133;
+      
+      .el-tree-node__label {
+        color: #303133;
+      }
+    }
   }
 
   .el-dialog__footer {

+ 56 - 29
yushu-uivue3/src/views/system/ai/chat/index.vue

@@ -286,6 +286,8 @@ import { listEnabledService } from '@/api/system/ai/service'
 import { getToken } from '@/utils/auth'
 import request from '@/utils/request'
 import MarkdownIt from 'markdown-it'
+import hljs from 'highlight.js'
+import 'highlight.js/styles/atom-one-dark.css'
 
 const { proxy } = getCurrentInstance()
 
@@ -342,21 +344,33 @@ watch(selectedServiceId, () => {
 const md = new MarkdownIt({
   html: true,
   linkify: true,
-  breaks: true,
-  highlight: function (str, lang) {
-    return '<pre class="hljs"><code>' +
-           md.utils.escapeHtml(str) +
-           '</code></pre>';
-  }
+  breaks: true
 })
 
-// 自定义代码块渲染
+// 自定义代码块渲染(带语法高亮)
 md.renderer.rules.fence = function (tokens, idx, options, env, self) {
   const token = tokens[idx]
   const code = token.content.trim()
-  const lang = token.info ? md.utils.escapeHtml(token.info) : ''
+  const lang = token.info ? token.info.trim() : ''
+  
+  let highlighted
+  if (lang && hljs.getLanguage(lang)) {
+    try {
+      highlighted = hljs.highlight(code, { language: lang }).value
+    } catch {
+      highlighted = md.utils.escapeHtml(code)
+    }
+  } else {
+    highlighted = hljs.highlightAuto(code).value
+  }
   
-  return `<div class="code-block"><div class="code-block-header"><span class="lang-name">${lang}</span><button class="copy-code-btn" data-code="${encodeURIComponent(code)}">复制</button></div><pre class="hljs language-${lang}"><code>${md.utils.escapeHtml(code)}</code></pre></div>`
+  return `<div class="code-block">
+    <div class="code-block-header">
+      <span class="lang-name">${md.utils.escapeHtml(lang) || 'code'}</span>
+      <button class="copy-code-btn" data-code="${encodeURIComponent(code)}">复制</button>
+    </div>
+    <pre class="hljs"><code>${highlighted}</code></pre>
+  </div>`
 }
 
 /** 加载AI服务列表 */
@@ -1152,53 +1166,66 @@ loadConversations()
           }
           
           :deep(.code-block) {
-            margin: 8px 0;
+            margin: 12px 0;
             margin-left: 0 !important;
-            border-radius: 6px;
+            border-radius: 8px;
             overflow: hidden;
-            background: #282c34;
+            background: #1e1e1e;
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+            
             .code-block-header {
               display: flex;
               justify-content: space-between;
               align-items: center;
-              padding: 8px 12px;
-              background: #21252b;
-              border-bottom: 1px solid #181a1f;
+              padding: 10px 16px;
+              background: #2d2d2d;
+              border-bottom: 1px solid #404040;
               
               .lang-name {
                 font-size: 12px;
-                color: #abb2bf;
-                text-transform: lowercase;
+                color: #9cdcfe;
+                font-weight: 500;
+                text-transform: uppercase;
+                letter-spacing: 0.5px;
               }
               
               .copy-code-btn {
                 font-size: 12px;
-                color: #abb2bf;
-                background: transparent;
+                color: #cccccc;
+                background: rgba(255, 255, 255, 0.08);
                 border: none;
                 cursor: pointer;
-                opacity: 0.7;
-                padding: 4px 8px;
+                padding: 5px 12px;
                 border-radius: 4px;
+                transition: all 0.2s;
                 
                 &:hover {
-                  background: rgba(255, 255, 255, 0.1);
-                  opacity: 1;
+                  background: rgba(255, 255, 255, 0.15);
+                  color: #ffffff;
                 }
               }
             }
             
-            pre {
+            pre.hljs {
               margin: 0;
-              padding: 12px;
-              background: #282c34;
-              color: #abb2bf;
+              padding: 16px;
+              background: #1e1e1e !important;
               overflow-x: auto;
               
+              &::-webkit-scrollbar {
+                height: 6px;
+              }
+              
+              &::-webkit-scrollbar-thumb {
+                background: #555;
+                border-radius: 3px;
+              }
+              
               code {
-                font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+                font-family: 'Fira Code', 'JetBrains Mono', Consolas, Monaco, 'Courier New', monospace;
                 font-size: 13px;
-                line-height: 1.5;
+                line-height: 1.6;
+                color: #d4d4d4;
               }
             }
           }

+ 277 - 185
yushu-uivue3/src/views/system/file/index.vue

@@ -114,7 +114,7 @@
       <!-- 网格视图 -->
       <div v-if="viewMode === 'grid'" class="grid-view" ref="gridViewRef">
         <div
-          v-for="folder in folderList"
+          v-for="folder in filteredFolderList"
           :key="'folder-' + folder.folderId"
           class="file-item folder-type"
           :class="{ selected: isSelected('folder', folder.folderId), 'drag-over': dragOverFolderId === folder.folderId }"
@@ -137,7 +137,7 @@
         </div>
 
         <div
-          v-for="file in fileList"
+          v-for="file in filteredFileList"
           :key="'file-' + file.fileId"
           class="file-item file-type"
           :class="{ selected: isSelected('file', file.fileId) }"
@@ -159,7 +159,7 @@
       </div>
 
       <!-- 列表视图 -->
-      <el-table v-else :data="[...folderList, ...fileList]" @selection-change="handleSelectionChange">
+      <el-table v-else :data="[...filteredFolderList, ...filteredFileList]" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" />
         <el-table-column label="名称" min-width="300">
           <template #default="scope">
@@ -192,9 +192,12 @@
         </el-table-column>
       </el-table>
 
-      <el-empty v-if="!loading && folderList.length === 0 && fileList.length === 0" description="此文件夹为空">
-        <el-button type="primary" @click="handleUpload">上传文件</el-button>
-        <el-button @click="handleAddFolder">新建文件夹</el-button>
+      <el-empty v-if="!loading && filteredFolderList.length === 0 && filteredFileList.length === 0" 
+        :description="searchKeyword ? '未找到匹配项' : '此文件夹为空'">
+        <template v-if="!searchKeyword">
+          <el-button type="primary" @click="handleUpload">上传文件</el-button>
+          <el-button @click="handleAddFolder">新建文件夹</el-button>
+        </template>
       </el-empty>
     </div>
 
@@ -242,9 +245,10 @@
     <el-dialog 
       v-model="previewDialogVisible" 
       :title="previewFile?.fileName || '文件预览'" 
-      width="80%" 
+      width="85%" 
       :close-on-click-modal="true"
       destroy-on-close
+      @close="handlePreviewClose"
     >
       <div class="preview-container">
         <!-- 图片预览 -->
@@ -263,13 +267,51 @@
         <div v-else-if="previewType === 'pdf'" class="preview-pdf">
           <iframe :src="previewUrl" style="width: 100%; height: 70vh; border: none;" />
         </div>
-        <!-- 文本预览 -->
-        <div v-else-if="previewType === 'text'" class="preview-text">
-          <pre style="max-height: 70vh; overflow: auto; background: #f5f7fa; padding: 16px; border-radius: 4px;">{{ previewContent }}</pre>
+        <!-- 代码/文本预览 -->
+        <div v-else-if="previewType === 'text' || previewType === 'code'" class="preview-code">
+          <div class="code-toolbar">
+            <el-button v-if="!previewEditing" size="small" @click="previewEditing = true">
+              <el-icon><Edit /></el-icon> 编辑
+            </el-button>
+            <template v-else>
+              <el-button size="small" type="primary" @click="saveTextContent">保存</el-button>
+              <el-button size="small" @click="previewEditing = false">取消</el-button>
+            </template>
+          </div>
+          <textarea 
+            v-if="previewEditing" 
+            v-model="previewContent" 
+            class="code-editor"
+          ></textarea>
+          <pre v-else class="code-preview"><code v-html="highlightedCode"></code></pre>
+        </div>
+        <!-- Excel预览 -->
+        <div v-else-if="previewType === 'excel'" class="preview-excel">
+          <div class="excel-tabs" v-if="excelSheets.length > 1">
+            <el-radio-group v-model="currentSheet" size="small">
+              <el-radio-button v-for="(sheet, idx) in excelSheets" :key="idx" :value="idx">
+                {{ sheet }}
+              </el-radio-button>
+            </el-radio-group>
+          </div>
+          <div class="excel-table-wrapper">
+            <table class="excel-table" v-if="excelData[currentSheet]">
+              <thead>
+                <tr>
+                  <th v-for="(cell, idx) in excelData[currentSheet][0]" :key="idx">{{ cell }}</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(row, rowIdx) in excelData[currentSheet].slice(1)" :key="rowIdx">
+                  <td v-for="(cell, cellIdx) in row" :key="cellIdx">{{ cell }}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
         </div>
         <!-- 不支持预览 -->
         <div v-else class="preview-unsupported">
-          <el-empty description="该文件类型暂不支持预览">
+          <el-empty :description="previewType === 'office' ? 'Word/PPT请下载后查看' : '该文件类型暂不支持预览'">
             <el-button type="primary" @click="handleDownloadFile(previewFile)">下载文件</el-button>
           </el-empty>
         </div>
@@ -301,41 +343,11 @@
       <div class="menu-item" @click="handleShare(contextMenuItem)">
         <el-icon><Share /></el-icon> 分享
       </div>
-      <div class="menu-item" @click="handleCopy(contextMenuItem)">
-        <el-icon><Folder /></el-icon> 复制到...
-      </div>
-      <div class="menu-item" @click="handleMove(contextMenuItem)">
-        <el-icon><FolderRemove /></el-icon> 移动到...
-      </div>
       <div class="menu-item danger" @click="handleDeleteItem(contextMenuItem)">
         <el-icon><Delete /></el-icon> 删除
       </div>
     </div>
 
-    <!-- 移动/复制目标文件夹选择对话框 -->
-    <el-dialog :title="folderSelectTitle" v-model="folderSelectDialogVisible" width="500px">
-      <el-tree
-        ref="folderTreeRef"
-        :data="folderTree"
-        :props="{ label: 'label', children: 'children' }"
-        node-key="id"
-        default-expand-all
-        highlight-current
-        @node-click="handleFolderTreeClick"
-      >
-        <template #default="{ node, data }">
-          <span class="folder-tree-node">
-            <el-icon color="#f9a825"><Folder /></el-icon>
-            <span>{{ data.label }}</span>
-          </span>
-        </template>
-      </el-tree>
-      <template #footer>
-        <el-button @click="folderSelectDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitFolderSelect" :disabled="!selectedTargetFolderId">确定</el-button>
-      </template>
-    </el-dialog>
-
     <!-- 分享对话框 -->
     <el-dialog 
       v-model="shareDialogVisible" 
@@ -467,16 +479,18 @@
 import { 
   HomeFilled, Folder, Document, FolderAdd, Upload, UploadFilled, 
   ArrowDown, ArrowLeft, ArrowRight, Top, Menu, List, Edit, Download, Delete, Search,
-  Share, CopyDocument, FolderRemove, Scissor, DocumentAdd, Refresh, CircleCheck
+  Share, CopyDocument, Scissor, DocumentAdd, Refresh, CircleCheck
 } from '@element-plus/icons-vue'
 import { 
-  listFolder, addFolder, updateFolder, delFolder, getFolderTree,
-  listFile, downloadFile, delFile, updateFile,
-  moveFile, copyFile, copyFolder,
+  listFolder, addFolder, updateFolder, delFolder,
+  listFile, downloadFile, delFile, updateFile, updateFileContent,
   createFileShare, createFolderShare, cancelFileShare,
   getFileShare, getFolderShare
 } from '@/api/system/file'
 import { getToken } from '@/utils/auth'
+import hljs from 'highlight.js'
+import 'highlight.js/styles/github.css'
+import * as XLSX from 'xlsx'
 
 const { proxy } = getCurrentInstance()
 
@@ -496,6 +510,19 @@ const isIndeterminate = computed(() => {
   return selectedItems.value.length > 0 && selectedItems.value.length < totalItems
 })
 
+// 过滤后的列表(搜索)
+const filteredFolderList = computed(() => {
+  if (!searchKeyword.value.trim()) return folderList.value
+  const keyword = searchKeyword.value.toLowerCase()
+  return folderList.value.filter(f => f.folderName?.toLowerCase().includes(keyword))
+})
+
+const filteredFileList = computed(() => {
+  if (!searchKeyword.value.trim()) return fileList.value
+  const keyword = searchKeyword.value.toLowerCase()
+  return fileList.value.filter(f => f.fileName?.toLowerCase().includes(keyword))
+})
+
 // 剪贴板
 const clipboard = ref([])
 const clipboardAction = ref('') // 'copy' or 'cut'
@@ -541,15 +568,10 @@ const previewFile = ref(null)
 const previewType = ref('')
 const previewUrl = ref('')
 const previewContent = ref('')
-
-// 移动/复制相关
-const folderSelectDialogVisible = ref(false)
-const folderSelectTitle = ref('')
-const folderSelectMode = ref('') // 'move' or 'copy'
-const folderTree = ref([])
-const folderTreeRef = ref(null)
-const selectedTargetFolderId = ref(null)
-const operatingItem = ref(null)
+const previewEditing = ref(false)
+const excelData = ref([])
+const excelSheets = ref([])
+const currentSheet = ref(0)
 
 // 分享相关
 const shareDialogVisible = ref(false)
@@ -844,14 +866,22 @@ function handleFilePreview(file) {
   if (!file) return
   
   previewFile.value = file
+  previewEditing.value = false
+  excelData.value = []
+  excelSheets.value = []
+  currentSheet.value = 0
+  
   const fileName = file.fileName || ''
   const ext = fileName.split('.').pop()?.toLowerCase() || ''
   
   // 判断文件类型
-  const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
-  const videoExts = ['mp4', 'webm', 'ogg', 'mov', 'avi']
-  const audioExts = ['mp3', 'wav', 'ogg', 'aac', 'flac']
-  const textExts = ['txt', 'json', 'xml', 'html', 'css', 'js', 'ts', 'vue', 'md', 'yaml', 'yml', 'ini', 'conf', 'log']
+  const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico']
+  const videoExts = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv', 'flv', 'm4v']
+  const audioExts = ['mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a', 'wma']
+  const codeExts = ['js', 'ts', 'jsx', 'tsx', 'vue', 'json', 'html', 'htm', 'css', 'scss', 'less', 'xml', 'sql', 'java', 'py', 'php', 'c', 'cpp', 'h', 'hpp', 'go', 'rs', 'sh', 'bat', 'ps1', 'dockerfile', 'makefile']
+  const textExts = ['txt', 'md', 'markdown', 'yaml', 'yml', 'ini', 'conf', 'log', 'csv', 'env', 'gitignore']
+  const excelExts = ['xls', 'xlsx']
+  const officeExts = ['doc', 'docx', 'ppt', 'pptx']
   
   if (imageExts.includes(ext)) {
     previewType.value = 'image'
@@ -861,19 +891,47 @@ function handleFilePreview(file) {
     previewType.value = 'audio'
   } else if (ext === 'pdf') {
     previewType.value = 'pdf'
+  } else if (codeExts.includes(ext)) {
+    previewType.value = 'code'
+    loadTextContent(file)
   } else if (textExts.includes(ext)) {
     previewType.value = 'text'
-    // 加载文本内容
     loadTextContent(file)
+  } else if (excelExts.includes(ext)) {
+    previewType.value = 'excel'
+    loadExcelContent(file)
+  } else if (officeExts.includes(ext)) {
+    previewType.value = 'office'
   } else {
     previewType.value = 'unsupported'
   }
   
-  // 设置预览URL
-  previewUrl.value = import.meta.env.VITE_APP_BASE_API + '/system/file/preview/' + file.fileId
+  // 设置预览URL(带token)
+  previewUrl.value = import.meta.env.VITE_APP_BASE_API + '/system/file/preview/' + file.fileId + '?Authorization=' + getToken()
   previewDialogVisible.value = true
 }
 
+/** 获取代码高亮 */
+const highlightedCode = computed(() => {
+  if (!previewContent.value) return ''
+  const ext = previewFile.value?.fileName?.split('.').pop()?.toLowerCase() || ''
+  const langMap = {
+    'js': 'javascript', 'ts': 'typescript', 'jsx': 'javascript', 'tsx': 'typescript',
+    'vue': 'xml', 'json': 'json', 'html': 'xml', 'htm': 'xml', 'xml': 'xml',
+    'css': 'css', 'scss': 'scss', 'less': 'less', 'sql': 'sql',
+    'java': 'java', 'py': 'python', 'php': 'php', 'c': 'c', 'cpp': 'cpp',
+    'h': 'c', 'hpp': 'cpp', 'go': 'go', 'rs': 'rust', 'sh': 'bash',
+    'bat': 'dos', 'ps1': 'powershell', 'dockerfile': 'dockerfile', 'makefile': 'makefile',
+    'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml'
+  }
+  const lang = langMap[ext] || 'plaintext'
+  try {
+    return hljs.highlight(previewContent.value, { language: lang }).value
+  } catch {
+    return hljs.highlightAuto(previewContent.value).value
+  }
+})
+
 /** 加载文本内容 */
 async function loadTextContent(file) {
   try {
@@ -885,6 +943,44 @@ async function loadTextContent(file) {
   }
 }
 
+/** 加载Excel内容 */
+async function loadExcelContent(file) {
+  try {
+    const response = await downloadFile(file.fileId)
+    const arrayBuffer = await response.arrayBuffer()
+    const workbook = XLSX.read(arrayBuffer, { type: 'array' })
+    
+    excelSheets.value = workbook.SheetNames
+    excelData.value = workbook.SheetNames.map(name => {
+      const sheet = workbook.Sheets[name]
+      return XLSX.utils.sheet_to_json(sheet, { header: 1 })
+    })
+  } catch (error) {
+    proxy.$modal.msgError('Excel加载失败')
+  }
+}
+
+/** 保存文本内容 */
+async function saveTextContent() {
+  if (!previewFile.value) return
+  
+  try {
+    await updateFileContent(previewFile.value.fileId, previewContent.value)
+    proxy.$modal.msgSuccess('保存成功')
+    previewEditing.value = false
+  } catch (error) {
+    proxy.$modal.msgError('保存失败')
+  }
+}
+
+/** 预览关闭 */
+function handlePreviewClose() {
+  previewEditing.value = false
+  previewContent.value = ''
+  excelData.value = []
+  excelSheets.value = []
+}
+
 /** 删除项目 */
 function handleDeleteItem(item) {
   closeContextMenu()
@@ -1246,78 +1342,6 @@ function copyShareAll() {
   proxy.$modal.msgSuccess('已复制到剪贴板')
 }
 
-/** 复制 */
-async function handleCopy(item) {
-  closeContextMenu()
-  if (!item) return
-  
-  operatingItem.value = item
-  folderSelectMode.value = 'copy'
-  folderSelectTitle.value = '复制到'
-  selectedTargetFolderId.value = null
-  
-  try {
-    const response = await getFolderTree()
-    folderTree.value = [{ id: 0, label: '根目录', children: response.data || [] }]
-    folderSelectDialogVisible.value = true
-  } catch (error) {
-    proxy.$modal.msgError('加载文件夹失败')
-  }
-}
-
-/** 移动 */
-async function handleMove(item) {
-  closeContextMenu()
-  if (!item) return
-  
-  operatingItem.value = item
-  folderSelectMode.value = 'move'
-  folderSelectTitle.value = '移动到'
-  selectedTargetFolderId.value = null
-  
-  try {
-    const response = await getFolderTree()
-    folderTree.value = [{ id: 0, label: '根目录', children: response.data || [] }]
-    folderSelectDialogVisible.value = true
-  } catch (error) {
-    proxy.$modal.msgError('加载文件夹失败')
-  }
-}
-
-function handleFolderTreeClick(data) {
-  selectedTargetFolderId.value = data.id
-}
-
-async function submitFolderSelect() {
-  if (selectedTargetFolderId.value === null || !operatingItem.value) return
-  
-  const isFolder = !!operatingItem.value.folderId
-  const targetId = selectedTargetFolderId.value
-  
-  try {
-    if (folderSelectMode.value === 'copy') {
-      if (isFolder) {
-        await copyFolder(operatingItem.value.folderId, targetId)
-      } else {
-        await copyFile(operatingItem.value.fileId, targetId)
-      }
-      proxy.$modal.msgSuccess('复制成功')
-    } else {
-      if (isFolder) {
-        proxy.$modal.msgError('暂不支持移动文件夹')
-        return
-      }
-      await moveFile(operatingItem.value.fileId, targetId)
-      proxy.$modal.msgSuccess('移动成功')
-    }
-    
-    folderSelectDialogVisible.value = false
-    loadData()
-  } catch (error) {
-    proxy.$modal.msgError(folderSelectMode.value === 'copy' ? '复制失败' : '移动失败')
-  }
-}
-
 /** 关闭右键菜单 */
 function closeContextMenu() {
   contextMenuVisible.value = false
@@ -1619,6 +1643,12 @@ onUnmounted(() => {
     gap: 12px;
     flex: 1;
     min-width: 0;
+    flex-wrap: nowrap;
+    
+    .el-button-group,
+    > .el-button {
+      flex-shrink: 0;
+    }
   }
   
   .toolbar-right {
@@ -1633,29 +1663,46 @@ onUnmounted(() => {
     background: #f5f7fa;
     border-radius: 8px;
     flex: 1;
-    min-width: 200px;
-    max-width: 500px;
+    min-width: 120px;
+    overflow: hidden;
     
-    .el-breadcrumb {
+    :deep(.el-breadcrumb) {
+      display: flex;
+      flex-wrap: nowrap;
+      white-space: nowrap;
+      overflow: hidden;
       line-height: 1;
       font-size: 14px;
-    }
-    
-    :deep(.el-breadcrumb__item) {
-      cursor: pointer;
       
-      .el-breadcrumb__inner {
-        transition: color 0.2s;
+      .el-breadcrumb__item {
+        cursor: pointer;
+        flex-shrink: 0;
+        max-width: 120px;
+        
+        .el-breadcrumb__inner {
+          display: inline-block;
+          max-width: 100px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          vertical-align: middle;
+          transition: color 0.2s;
+          
+          &:hover {
+            color: #409eff;
+          }
+        }
         
-        &:hover {
-          color: #409eff;
+        .el-breadcrumb__separator {
+          flex-shrink: 0;
         }
       }
     }
   }
   
   .search-input {
-    width: 220px;
+    width: 180px;
+    flex-shrink: 0;
     
     :deep(.el-input__wrapper) {
       border-radius: 8px;
@@ -1883,8 +1930,7 @@ onUnmounted(() => {
 
 .preview-container {
   display: flex;
-  justify-content: center;
-  align-items: center;
+  flex-direction: column;
   min-height: 400px;
   background: #fafafa;
   border-radius: 8px;
@@ -1893,8 +1939,7 @@ onUnmounted(() => {
   .preview-image,
   .preview-video,
   .preview-audio,
-  .preview-pdf,
-  .preview-text {
+  .preview-pdf {
     width: 100%;
     display: flex;
     justify-content: center;
@@ -1912,15 +1957,90 @@ onUnmounted(() => {
     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
   }
   
-  .preview-text pre {
+  .preview-code {
     width: 100%;
-    white-space: pre-wrap;
-    word-wrap: break-word;
-    background: #ffffff;
-    border-radius: 8px;
-    font-family: 'Monaco', 'Menlo', monospace;
-    font-size: 13px;
-    line-height: 1.6;
+    
+    .code-toolbar {
+      margin-bottom: 12px;
+      display: flex;
+      gap: 8px;
+    }
+    
+    .code-editor {
+      width: 100%;
+      height: 65vh;
+      padding: 16px;
+      font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+      font-size: 13px;
+      line-height: 1.6;
+      border: 1px solid #dcdfe6;
+      border-radius: 8px;
+      resize: none;
+      outline: none;
+      background: #fff;
+      
+      &:focus {
+        border-color: #409eff;
+      }
+    }
+    
+    .code-preview {
+      width: 100%;
+      max-height: 65vh;
+      overflow: auto;
+      margin: 0;
+      padding: 16px;
+      background: #fff;
+      border-radius: 8px;
+      border: 1px solid #eee;
+      
+      code {
+        font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+        font-size: 13px;
+        line-height: 1.6;
+      }
+    }
+  }
+  
+  .preview-excel {
+    width: 100%;
+    
+    .excel-tabs {
+      margin-bottom: 12px;
+    }
+    
+    .excel-table-wrapper {
+      max-height: 65vh;
+      overflow: auto;
+      border: 1px solid #eee;
+      border-radius: 8px;
+    }
+    
+    .excel-table {
+      width: 100%;
+      border-collapse: collapse;
+      background: #fff;
+      font-size: 13px;
+      
+      th, td {
+        padding: 8px 12px;
+        border: 1px solid #ebeef5;
+        text-align: left;
+        white-space: nowrap;
+      }
+      
+      th {
+        background: #f5f7fa;
+        font-weight: 600;
+        position: sticky;
+        top: 0;
+        z-index: 1;
+      }
+      
+      tr:hover td {
+        background: #f5f7fa;
+      }
+    }
   }
 }
 
@@ -1988,34 +2108,6 @@ onUnmounted(() => {
   }
 }
 
-// 文件夹树选择
-.folder-tree-node {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  
-  .el-icon {
-    font-size: 18px;
-  }
-}
-
-:deep(.el-tree) {
-  .el-tree-node__content {
-    height: 36px;
-    border-radius: 6px;
-    margin: 2px 0;
-    
-    &:hover {
-      background: #f5f7fa;
-    }
-  }
-  
-  .el-tree-node.is-current > .el-tree-node__content {
-    background: #ecf5ff;
-    color: #409eff;
-  }
-}
-
 // 分享结果
 .share-result {
   .share-tip {