+
+
-
+
-
-
+
+
-
+
@@ -41,35 +50,27 @@
@@ -77,9 +78,7 @@
@@ -98,6 +98,10 @@
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { projectApi } from '../api'
import { normalizeFileTree } from '../utils/fileTree'
+import { useModal } from '../composables/useModal'
+import { useToast } from '../composables/useToast'
+import { icons } from '../utils/icons'
+import { getFileIconColor } from '../utils/fileUtils'
const props = defineProps({
item: { type: Object, required: true },
@@ -106,7 +110,10 @@ const props = defineProps({
projectId: { type: String, required: true },
})
-const emit = defineEmits(['select', 'refresh'])
+const emit = defineEmits(['select', 'refresh', 'move'])
+
+const modal = useModal()
+const toast = useToast()
const expanded = ref(false)
const loading = ref(false)
@@ -119,14 +126,8 @@ const renameInput = ref(null)
const isActive = computed(() => props.activePath === props.item.path)
const iconColor = computed(() => {
- const ext = props.item.extension?.toLowerCase() || ''
- const colorMap = {
- '.py': '#3572A5', '.js': '#f1e05a', '.ts': '#3178c6', '.vue': '#41b883',
- '.html': '#e34c26', '.css': '#563d7c', '.json': '#292929', '.md': '#083fa1',
- '.yml': '#cb171e', '.yaml': '#cb171e', '.toml': '#9c4221', '.sql': '#e38c00',
- '.sh': '#89e051', '.java': '#b07219', '.go': '#00ADD8', '.rs': '#dea584',
- }
- return colorMap[ext] || 'var(--text-tertiary)'
+ const ext = props.item.extension?.replace(/^\./, '').toLowerCase() || ''
+ return getFileIconColor(ext)
})
function onMouseLeave() {
@@ -186,9 +187,10 @@ async function confirmRename() {
try {
await projectApi.renameFile(props.projectId, props.item.path, newPath)
+ toast.success(`已重命名为「${newName}」`)
emit('refresh')
} catch (e) {
- alert('重命名失败: ' + e.message)
+ toast.error('重命名失败: ' + e.message)
}
}
@@ -199,42 +201,102 @@ function cancelRename() {
// -- Delete --
async function deleteItem() {
const type = props.item.type === 'dir' ? '文件夹' : '文件'
- if (!confirm(`确定要删除${type}「${props.item.name}」吗?`)) return
+ const ok = await modal.confirm('删除确认', `确定要删除${type}「${props.item.name}」吗?`, { danger: true })
+ if (!ok) return
try {
await projectApi.deleteFile(props.projectId, props.item.path)
+ toast.success(`已删除「${props.item.name}」`)
emit('refresh')
} catch (e) {
- alert('删除失败: ' + e.message)
+ toast.error('删除失败: ' + e.message)
}
}
// -- Create (in folder) --
async function createNewFile() {
showCreateMenu.value = false
- const name = prompt('文件名(例如 utils.py)')
- if (!name?.trim()) return
- const path = props.item.path ? `${props.item.path}/${name.trim()}` : name.trim()
+ const name = await modal.prompt('新建文件', '请输入文件名(例如 utils.py)')
+ if (!name) return
+ const path = props.item.path ? `${props.item.path}/${name}` : name
try {
await projectApi.writeFile(props.projectId, path, '')
+ toast.success(`已创建「${name}」`)
emit('refresh')
} catch (e) {
- alert('创建失败: ' + e.message)
+ toast.error('创建失败: ' + e.message)
}
}
async function createNewFolder() {
showCreateMenu.value = false
- const name = prompt('文件夹名称')
- if (!name?.trim()) return
- const path = props.item.path ? `${props.item.path}/${name.trim()}` : name.trim()
+ const name = await modal.prompt('新建文件夹', '请输入文件夹名称')
+ if (!name) return
+ const path = props.item.path ? `${props.item.path}/${name}` : name
try {
await projectApi.mkdir(props.projectId, path)
+ toast.success(`已创建文件夹「${name}」`)
emit('refresh')
} catch (e) {
- alert('创建失败: ' + e.message)
+ toast.error('创建失败: ' + e.message)
}
}
+
+// -- Drag & Drop (move file/folder) --
+const isDragOver = ref(false)
+let dragCounter = 0
+
+function onDragStart(e) {
+ e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.setData('application/json', JSON.stringify({
+ path: props.item.path,
+ name: props.item.name,
+ type: props.item.type,
+ }))
+}
+
+function onDragEnd() {
+ isDragOver.value = false
+ dragCounter = 0
+}
+
+function onDragOver(e) {
+ if (props.item.type !== 'dir') return
+ e.dataTransfer.dropEffect = 'move'
+}
+
+function onDragEnter() {
+ if (props.item.type !== 'dir') return
+ dragCounter++
+ isDragOver.value = true
+}
+
+function onDragLeave() {
+ dragCounter--
+ if (dragCounter <= 0) {
+ dragCounter = 0
+ isDragOver.value = false
+ }
+}
+
+function onDrop(e) {
+ isDragOver.value = false
+ dragCounter = 0
+ if (props.item.type !== 'dir') return
+
+ try {
+ const data = JSON.parse(e.dataTransfer.getData('application/json'))
+ if (!data.path || data.path === props.item.path) return
+
+ // Prevent moving a parent into its own child
+ if (props.item.path.startsWith(data.path + '/')) {
+ toast.error('不能将文件夹移动到其子文件夹内')
+ return
+ }
+
+ emit('move', { srcPath: data.path, destDir: props.item.path, name: data.name })
+ } catch (_) {}
+}
diff --git a/frontend/src/components/MessageBubble.vue b/frontend/src/components/MessageBubble.vue
index ca7ba11..c1e7f82 100644
--- a/frontend/src/components/MessageBubble.vue
+++ b/frontend/src/components/MessageBubble.vue
@@ -31,22 +31,13 @@
{{ tokenCount }} tokens
{{ formatTime(createdAt) }}