# 文件操作增强:多选 / 复制粘贴剪切 / 移动 > 状态:方案设计完成,待实施 > 日期:2026-04-12 ## 一、现状 | 功能 | 前端 | 后端 | 状态 | |------|------|------|------| | **多选** (Ctrl+Click / Shift+Click) | 无,单选 `selectedFileItem: FileItem \| null` | N/A | **缺失** | | **全选** (Ctrl+A) | 无 | N/A | **缺失** | | **复制文件** (Ctrl+C) | TODO stub(弹"暂未实现") | 无 API | **缺失** | | **剪切文件** (Ctrl+X) | 无 | 无 API | **缺失** | | **粘贴文件** (Ctrl+V) | 仅支持图片粘贴 | 无 API | **部分** | | **移动文件** | TODO stub(弹"暂未实现") | 无 API | **缺失** | | **重命名** (F2) | 完整 | 完整 | 已完成 | | **删除** (Del) | 完整(含回收站) | 完整 | 已完成 | | **右键菜单 Copy/Cut/Paste** | 无菜单项 | N/A | 缺失 | ### 关键发现 1. `useFileOperations.ts` 中 `copy()` 和 `move()` 已有函数签名但都是 TODO stub 2. 回收站模块 (`recycle_bin.go`) 内部有 `copyRecursively`/`copyFile`/`copyDirectory` 私有方法可复用 3. 快捷键已有 F5/F2/Del/Ctrl+S 等 12 个,缺 Ctrl+C/V/X/A 四个 4. 右键菜单只有:新建文件、新建文件夹、系统打开、重命名、删除 — 缺复制/剪切/粘贴/移动 ## 二、方案架构 ``` ┌─────────────────────────────────────────────┐ │ 前端 (Vue 3) │ │ │ │ 1. 多选状态管理 (selectedFiles: FileItem[]) │ │ 2. 剪贴板状态 (clipboard: {op, files[]}) │ │ 3. FileListPanel: Ctrl+Click / Shift+Click │ │ 4. ContextMenu: +Copy / Cut / Paste 项 │ │ 5. 快捷键: Ctrl+C / V / X / A │ └──────────────────┬──────────────────────────┘ │ window.go.main.App.* ┌──────────────────▼──────────────────────────┐ │ 后端 (Go) │ │ │ │ 6. App.CopyPath(src, dst) → FileSystemService │ │ 7. App.MovePath(src, dst) → os.Rename │ │ 8. 复用 recycle_bin.go 的 copy 辅助函数 │ └─────────────────────────────────────────────┘ ``` ## 三、实施步骤 ### Step 1:后端 — 新增 Copy / Move API **文件**: `internal/filesystem/service.go` 新增两个公开方法(复用已有的私有 copy 函数): ```go // CopyPath 复制文件或目录 func (s *FileSystemService) CopyPath(src, dst string) error { return copyRecursively(src, dst) } // MovePath 移动文件或目录(跨盘需 copy+delete) func (s *FileSystemService) MovePath(src, dst string) error { // 同盘: os.Rename (原子操作) // 跨盘: copyRecursively + DeletePathWithContext } ``` **文件**: `app.go` 新增两个绑定方法 + 请求结构体: ```go type CopyMoveRequest struct { Src string `json:"src"` Dst string `json:"dst"` } func (a *App) CopyPath(req CopyMoveRequest) error { return a.filesystem.CopyPath(req.Src, req.Dst) } func (a *App) MovePath(req CopyMoveRequest) error { return a.filesystem.MovePath(req.Src, req.Dst) } ``` **文件**: `frontend/src/api/system.ts` ```ts export async function copyPath(src: string, dst: string): Promise export async function movePath(src: string, dst: string): Promise ``` ### Step 2:前端 — 多选状态管理 **文件**: `frontend/src/components/FileSystem/index.vue` 核心改动:`selectedFileItem: FileItem | null` → `selectedFiles: FileItem[]` ```ts // 改前 const selectedFileItem = ref(null) // 改后 const selectedFiles = ref([]) const selectedFileItem = computed(() => selectedFiles.value[0] || null) // 兼容现有单选逻辑 ``` **文件**: `frontend/src/types/file-system.ts` `FileListPanelConfig.selectedFileItem` 类型改为 `selectedFiles: FileItem[]` **文件**: `frontend/src/components/FileSystem/components/FileListPanel.vue` 改造行点击支持多选: ```ts const handleRowClick = (record: FileItem, ev: MouseEvent) => { if (target.closest('.arco-btn') || target.closest('.arco-input-wrapper')) return if (ev.ctrlKey || ev.metaKey) { emit('toggleSelect', record) // Ctrl+Click: 切换选中 } else if (ev.shiftKey && props.selectedFiles.length > 0) { emit('rangeSelect', record) // Shift+Click: 范围选择 } else { emit('fileClick', record) // 普通点击: 单选 } } ``` `getRowClassName` 改为匹配数组: ```ts const getRowClassName = (record: FileItem): string => [ props.selectedFiles.some(f => f.path === record.path) && 'row-selected', props.config.editingFilePath === record.path && 'row-editing' ].filter(Boolean).join(' ') ``` Props 变更: ```ts // 改前 selectedFileItem: FileItem | null // 改后 selectedFiles: FileItem[] ``` 新增 Emits: ```ts (e: 'toggleSelect', file: FileItem): void (e: 'rangeSelect', file: FileItem): void ``` ### Step 3:前端 — 剪贴板状态(应用级) **新建文件**: `frontend/src/components/FileSystem/composables/useClipboard.ts` ```ts type ClipboardOp = 'copy' | 'cut' interface ClipboardState { op: ClipboardOp | null files: FileItem[] // 源文件列表 sourceDir: string // 来源目录 } const clipboard = reactive({ op: null, files: [], sourceDir: '', }) export function useClipboard() { const copy = (files: FileItem[]) => { /* ... */ } const cut = (files: FileItem[]) => { /* ... */ } const clear = () => { clipboard.op = null; clipboard.files = [] } const canPaste = computed(() => clipboard.op !== null && clipboard.files.length > 0) return { clipboard, copy, cut, clear, canPaste } } ``` ### Step 4:前端 — 右键菜单扩展 **文件**: `frontend/src/components/FileSystem/components/ContextMenu.vue` 文件菜单追加 Copy / Cut: ```vue
📋复制Ctrl+C
✂️剪切Ctrl+X
``` 空白区域菜单追加 Paste: ```vue
📌粘贴Ctrl+V
``` 新增 Props: `selectedCount?: number`, `canPaste?: boolean` 新增 Emits: `'action': 'copy' | 'cut' | 'paste'` ### Step 5:前端 — 快捷键扩展 **文件**: `frontend/src/components/FileSystem/index.vue` 的 `handleKeyDown` 在 Delete 处理之后追加: ```ts // Ctrl+C 复制 if ((event.ctrlKey || event.metaKey) && event.key === 'c' && selectedFiles.value.length > 0) { event.preventDefault(); handleCopy(); return } // Ctrl+X 剪切 if ((event.ctrlKey || event.metaKey) && event.key === 'x' && selectedFiles.value.length > 0) { event.preventDefault(); handleCut(); return } // Ctrl+V 粘贴 if ((event.ctrlKey || event.metaKey) && event.key === 'v') { event.preventDefault(); await handlePaste(); return } // Ctrl+A 全选 if ((event.ctrlKey || event.metaKey) && event.key === 'a') { event.preventDefault() selectedFiles.value = [...fileList.value] return } ``` ### Step 6:前端 — 操作执行逻辑 **文件**: `frontend/src/components/FileSystem/composables/useFileOperations.ts` 实现 `copy()` 和 `move()`(替换 TODO stub): ```ts const copy = async (fromPath: string, toPath: string): Promise => { await copyPathApi(fromPath, toPath) Message.success('复制完成') } const move = async (fromPath: string, toPath: string): Promise => { await movePathApi(fromPath, toPath) Message.success('移动完成') } ``` **文件**: `frontend/src/components/FileSystem/index.vue` 核心处理函数: ```ts const { clipboard, copy: clipCopy, cut: clipCut, canPaste } = useClipboard() const handleCopy = () => { clipCopy(selectedFiles.value) Message.info(`已复制 ${selectedFiles.value.length} 项`) } const handleCut = () => { clipCut(selectedFiles.value) Message.info(`已剪切 ${selectedFiles.value.length} 项`) } const handlePaste = async () => { for (const file of clipboard.files) { const dst = filePath.value + '/' + file.name clipboard.op === 'cut' ? await fileOps.move(file.path, dst) : await fileOps.copy(file.path, dst) } if (clipboard.op === 'cut') clipClear() loadDirectory(filePath.value) } const handleToggleSelect = (file: FileItem) => { /* 切换单项 */ } const handleRangeSelect = (file: FileItem) => { /* 范围选择 */ } // handleContextMenuAction 扩展 case 'copy': handleCopy(); break case 'cut': handleCut(); break case 'paste': await handlePaste(); break ``` ## 四、改动文件清单 | 文件 | 改动类型 | 说明 | |------|---------|------| | `internal/filesystem/service.go` | 修改 | 新增 `CopyPath()`、`MovePath()` | | `app.go` | 修改 | 新增绑定方法 + `CopyMoveRequest` 结构体 | | `frontend/src/api/system.ts` | 修改 | 新增 `copyPath()`、`movePath()` | | `frontend/src/types/file-system.ts` | 修改 | `selectedFileItem` → `selectedFiles` 数组 | | `frontend/src/components/FileSystem/composables/useClipboard.ts` | **新建** | 剪贴板 composable | | `frontend/src/components/FileSystem/composables/useFileOperations.ts` | 修改 | 实现 copy/move TODO | | `frontend/src/components/FileSystem/components/FileListPanel.vue` | 修改 | 多选行点击 + 行样式 | | `frontend/src/components/FileSystem/components/ContextMenu.vue` | 修改 | 追加 Copy/Cut/Paste 菜单项 | | `frontend/src/components/FileSystem/index.vue` | 修改 | 多选状态 + 快捷键 + 操作函数 | 共 **9 个文件**(1 个新建 + 8 个修改) ## 五、验证标准 1. **Ctrl+Click** 切换单文件选中状态,高亮多行 2. **Shift+Click** 选中范围文件 3. **Ctrl+A** 全选当前目录所有文件 4. **Ctrl+C** 复制选中文件 → 提示"N 项已复制" 5. **Ctrl+X** 剪切选中文件 → 提示"N 项已剪切" 6. **Ctrl+V** 在目标目录粘贴 → 文件出现 7. **右键菜单** 显示 Copy / Cut / Paste(空白区) 8. **跨目录粘贴** 正确复制到目标路径 9. **剪切粘贴** 源文件消失(移动效果) 10. **大文件夹** 复制不卡顿(Go io.Copy 流式) ## 六、不做(明确边界) - **拖拽移动文件** — 本轮不做,后续可加 - **外部文件粘贴** — 不支持从系统资源管理器粘贴到 u-desk(Wails 限制) - **进度条** — 大文件复制暂不加进度条 - **撤销(Ctrl+Z)** — 仅保留编辑器内容重置,不做操作历史撤销