Private
Public Access
1
0
Files
u-desk/docs/04-功能迭代/GO-DESK-6.文件操作增强/文件操作增强方案.md

321 lines
11 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 文件操作增强:多选 / 复制粘贴剪切 / 移动
> 状态:方案设计完成,待实施
> 日期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<void>
export async function movePath(src: string, dst: string): Promise<void>
```
### Step 2前端 — 多选状态管理
**文件**: `frontend/src/components/FileSystem/index.vue`
核心改动:`selectedFileItem: FileItem | null``selectedFiles: FileItem[]`
```ts
// 改前
const selectedFileItem = ref<FileItem | null>(null)
// 改后
const selectedFiles = ref<FileItem[]>([])
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<ClipboardState>({
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
<div class="context-menu-item" @click="handleCopy">
<span>📋</span><span>复制</span><span class="shortcut">Ctrl+C</span>
</div>
<div class="context-menu-item" @click="handleCut">
<span></span><span>剪切</span><span class="shortcut">Ctrl+X</span>
</div>
<div class="context-menu-divider"></div>
<!-- 现有重命名/删除保持不变 -->
```
空白区域菜单追加 Paste
```vue
<div class="context-menu-divider"></div>
<div class="context-menu-item" :disabled="!canPaste" @click="handlePaste">
<span>📌</span><span>粘贴</span><span class="shortcut">Ctrl+V</span>
</div>
```
新增 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<void> => {
await copyPathApi(fromPath, toPath)
Message.success('复制完成')
}
const move = async (fromPath: string, toPath: string): Promise<void> => {
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-deskWails 限制)
- **进度条** — 大文件复制暂不加进度条
- **撤销(Ctrl+Z)** — 仅保留编辑器内容重置,不做操作历史撤销