fix: file tree opreat logic
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { Folder, File, ChevronRight, ChevronDown, Trash2, Plus, Download } from 'lucide-vue-next'
|
import { Folder, File, ChevronRight, ChevronDown } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import type { FileNode } from '@/api'
|
import type { FileNode } from '@/api'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -18,9 +17,9 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
select: [node: FileNode]
|
select: [node: FileNode]
|
||||||
delete: [path: string]
|
delete: [path: string]
|
||||||
'download-file': [path: string]
|
|
||||||
create: [parentDir: string]
|
create: [parentDir: string]
|
||||||
move: [oldPath: string, newPath: string]
|
move: [oldPath: string, newPath: string]
|
||||||
|
rename: [path: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const depth = computed(() => props.depth ?? 0)
|
const depth = computed(() => props.depth ?? 0)
|
||||||
@@ -32,20 +31,7 @@ function handleSelect() {
|
|||||||
emit('select', props.node)
|
emit('select', props.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete(e: Event) {
|
|
||||||
e.stopPropagation()
|
|
||||||
emit('delete', props.node.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDownloadClick(e: Event) {
|
|
||||||
e.stopPropagation()
|
|
||||||
emit('download-file', props.node.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCreate(e: Event) {
|
|
||||||
e.stopPropagation()
|
|
||||||
emit('create', props.node.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragStart(e: DragEvent) {
|
function handleDragStart(e: DragEvent) {
|
||||||
e.dataTransfer?.setData('text/plain', props.node.path)
|
e.dataTransfer?.setData('text/plain', props.node.path)
|
||||||
@@ -99,23 +85,12 @@ function handleDrop(e: DragEvent) {
|
|||||||
<Folder v-if="node.isDir" class="h-3 w-3 text-yellow-500 flex-shrink-0" />
|
<Folder v-if="node.isDir" class="h-3 w-3 text-yellow-500 flex-shrink-0" />
|
||||||
<File v-else class="h-3 w-3 text-blue-500 flex-shrink-0" />
|
<File v-else class="h-3 w-3 text-blue-500 flex-shrink-0" />
|
||||||
<span class="truncate flex-1">{{ node.name }}</span>
|
<span class="truncate flex-1">{{ node.name }}</span>
|
||||||
<Button v-if="node.isDir" variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100"
|
|
||||||
@click="handleCreate">
|
|
||||||
<Plus class="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button v-if="!node.isDir" variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100"
|
|
||||||
@click="handleDownloadClick">
|
|
||||||
<Download class="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100" @click="handleDelete">
|
|
||||||
<Trash2 class="h-3 w-3 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="node.isDir && isExpanded && node.children">
|
<template v-if="node.isDir && isExpanded && node.children">
|
||||||
<FileTreeNode v-for="child in node.children" :key="child.path" :node="child" :expanded-dirs="expandedDirs"
|
<FileTreeNode v-for="child in node.children" :key="child.path" :node="child" :expanded-dirs="expandedDirs"
|
||||||
:selected-path="selectedPath" :depth="depth + 1" @select="$emit('select', $event)"
|
:selected-path="selectedPath" :depth="depth + 1" @select="$emit('select', $event)"
|
||||||
@delete="$emit('delete', $event)" @download-file="$emit('download-file', $event)"
|
@create="$emit('create', $event)" @move="(oldPath, newPath) => $emit('move', oldPath, newPath)"
|
||||||
@create="$emit('create', $event)" @move="(oldPath, newPath) => $emit('move', oldPath, newPath)" />
|
@rename="$emit('rename', $event)" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Label } from '@/components/ui/label'
|
|||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||||
import FileTreeNode from '@/components/FileTreeNode.vue'
|
import FileTreeNode from '@/components/FileTreeNode.vue'
|
||||||
import XTerminal from '@/components/XTerminal.vue'
|
import XTerminal from '@/components/XTerminal.vue'
|
||||||
import { Plus, Save, Play, RefreshCw, Upload, FolderUp, Pencil, Eye, X } from 'lucide-vue-next'
|
import { Plus, Save, Play, RefreshCw, Upload, FolderUp, Pencil, Eye, X, Download, Trash2, Edit3 } from 'lucide-vue-next'
|
||||||
import { api, type FileNode } from '@/api'
|
import { api, type FileNode } from '@/api'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { PATHS, FILE_RUNNERS } from '@/constants'
|
import { PATHS, FILE_RUNNERS } from '@/constants'
|
||||||
@@ -37,6 +37,10 @@ const archiveInputRef = ref<HTMLInputElement | null>(null)
|
|||||||
const filesInputRef = ref<HTMLInputElement | null>(null)
|
const filesInputRef = ref<HTMLInputElement | null>(null)
|
||||||
const uploadTargetDir = ref('')
|
const uploadTargetDir = ref('')
|
||||||
|
|
||||||
|
const showRenameDialog = ref(false)
|
||||||
|
const renamePath = ref('')
|
||||||
|
const newName = ref('')
|
||||||
|
|
||||||
const isEditMode = ref(false)
|
const isEditMode = ref(false)
|
||||||
const hasChanges = computed(() => fileContent.value !== originalContent.value)
|
const hasChanges = computed(() => fileContent.value !== originalContent.value)
|
||||||
|
|
||||||
@@ -184,6 +188,34 @@ async function deleteItem() {
|
|||||||
showDeleteDialog.value = false
|
showDeleteDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRenameDialog(path: string) {
|
||||||
|
renamePath.value = path
|
||||||
|
newName.value = path.split('/').pop() || ''
|
||||||
|
showRenameDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameItem() {
|
||||||
|
if (!newName.value.trim()) {
|
||||||
|
toast.error('请输入名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parts = renamePath.value.split('/')
|
||||||
|
parts[parts.length - 1] = newName.value
|
||||||
|
const newPath = parts.join('/')
|
||||||
|
|
||||||
|
if (newPath === renamePath.value) {
|
||||||
|
showRenameDialog.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleMove(renamePath.value, newPath, '重命名成功')
|
||||||
|
showRenameDialog.value = false
|
||||||
|
} catch {
|
||||||
|
// Error handled in handleMove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runScript() {
|
async function runScript() {
|
||||||
if (!selectedFile.value) return
|
if (!selectedFile.value) return
|
||||||
|
|
||||||
@@ -236,10 +268,10 @@ async function handleDownload(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMove(oldPath: string, newPath: string) {
|
async function handleMove(oldPath: string, newPath: string, successMsg = '移动成功') {
|
||||||
try {
|
try {
|
||||||
await api.files.rename(oldPath, newPath)
|
await api.files.rename(oldPath, newPath)
|
||||||
toast.success('移动成功')
|
toast.success(successMsg)
|
||||||
if (selectedFile.value === oldPath) {
|
if (selectedFile.value === oldPath) {
|
||||||
selectedFile.value = newPath
|
selectedFile.value = newPath
|
||||||
selectedPath.value = newPath
|
selectedPath.value = newPath
|
||||||
@@ -401,7 +433,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<FileTreeNode v-for="node in fileTree" :key="node.path" :node="node" :expanded-dirs="expandedDirs"
|
<FileTreeNode v-for="node in fileTree" :key="node.path" :node="node" :expanded-dirs="expandedDirs"
|
||||||
:selected-path="selectedPath" @select="handleSelect" @delete="confirmDelete" @create="handleCreate"
|
:selected-path="selectedPath" @select="handleSelect" @delete="confirmDelete" @create="handleCreate"
|
||||||
@download-file="handleDownload" @move="handleMove" />
|
@download-file="handleDownload" @move="handleMove" @rename="openRenameDialog" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -409,10 +441,21 @@ onUnmounted(() => {
|
|||||||
<div class="flex-1 min-h-[300px] border rounded-md flex flex-col overflow-hidden">
|
<div class="flex-1 min-h-[300px] border rounded-md flex flex-col overflow-hidden">
|
||||||
<div class="flex items-center justify-between p-2 border-b gap-2">
|
<div class="flex items-center justify-between p-2 border-b gap-2">
|
||||||
<span class="text-xs font-medium truncate flex-1 min-w-0">
|
<span class="text-xs font-medium truncate flex-1 min-w-0">
|
||||||
{{ selectedFile || '选择文件进行编辑' }}
|
{{ selectedPath || '选择文件或文件夹进行操作' }}
|
||||||
<span v-if="hasChanges" class="text-orange-500 ml-1">●</span>
|
<span v-if="hasChanges" class="text-orange-500 ml-1">●</span>
|
||||||
</span>
|
</span>
|
||||||
<div v-if="selectedFile" class="flex gap-1 shrink-0">
|
<div v-if="selectedPath" class="flex gap-1 shrink-0">
|
||||||
|
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2" @click="openRenameDialog(selectedPath)">
|
||||||
|
<Edit3 class="h-3 w-3" /> <span class="hidden sm:inline">重命名</span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2" @click="confirmDelete(selectedPath)">
|
||||||
|
<Trash2 class="h-3 w-3 text-destructive" /> <span class="hidden sm:inline">删除</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<template v-if="selectedFile">
|
||||||
|
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2" @click="handleDownload(selectedFile)">
|
||||||
|
<Download class="h-3 w-3" /> <span class="hidden sm:inline">下载</span>
|
||||||
|
</Button>
|
||||||
<Button v-if="!isEditMode" variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2"
|
<Button v-if="!isEditMode" variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2"
|
||||||
@click="isEditMode = true">
|
@click="isEditMode = true">
|
||||||
<Pencil class="h-3 w-3" /> <span class="hidden sm:inline">编辑</span>
|
<Pencil class="h-3 w-3" /> <span class="hidden sm:inline">编辑</span>
|
||||||
@@ -422,13 +465,15 @@ onUnmounted(() => {
|
|||||||
@click="isEditMode = false; fileContent = originalContent">
|
@click="isEditMode = false; fileContent = originalContent">
|
||||||
<Eye class="h-3 w-3" /> <span class="hidden sm:inline">查看</span>
|
<Eye class="h-3 w-3" /> <span class="hidden sm:inline">查看</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2" :disabled="!hasChanges" @click="saveFile">
|
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2" :disabled="!hasChanges"
|
||||||
|
@click="saveFile">
|
||||||
<Save class="h-3 w-3" /> <span class="hidden sm:inline">保存</span>
|
<Save class="h-3 w-3" /> <span class="hidden sm:inline">保存</span>
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2" @click="runScript">
|
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1 px-2" @click="runScript">
|
||||||
<Play class="h-3 w-3" /> <span class="hidden sm:inline">运行</span>
|
<Play class="h-3 w-3" /> <span class="hidden sm:inline">运行</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -507,6 +552,25 @@ onUnmounted(() => {
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
<!-- 重命名对话框 -->
|
||||||
|
<Dialog v-model:open="showRenameDialog">
|
||||||
|
<DialogContent class="max-w-xs" @open-auto-focus.prevent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="text-sm">重命名</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="space-y-3 py-2">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label class="text-xs">新名称</Label>
|
||||||
|
<Input v-model="newName" class="h-8 text-xs" placeholder="new_name.sh" @keyup.enter="renameItem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" size="sm" class="h-7 text-xs" @click="showRenameDialog = false">取消</Button>
|
||||||
|
<Button size="sm" class="h-7 text-xs" @click="renameItem">确定</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<!-- 终端弹窗 -->
|
<!-- 终端弹窗 -->
|
||||||
<Dialog v-model:open="showTerminalDialog">
|
<Dialog v-model:open="showTerminalDialog">
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
|||||||
Reference in New Issue
Block a user