新增:独立的文件管理模块,优化文件操作功能
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
<h2>Go Desk</h2>
|
<h2>Go Desk</h2>
|
||||||
<a-tabs v-model:active-key="activeTab" class="header-tabs">
|
<a-tabs v-model:active-key="activeTab" class="header-tabs">
|
||||||
<a-tab-pane key="db-cli" title="数据库客户端"/>
|
<a-tab-pane key="db-cli" title="数据库客户端"/>
|
||||||
|
<a-tab-pane key="file-system" title="文件管理"/>
|
||||||
<a-tab-pane key="user" title="用户查询"/>
|
<a-tab-pane key="user" title="用户查询"/>
|
||||||
<a-tab-pane key="device" title="设备调用测试"/>
|
<a-tab-pane key="device" title="设备调用测试"/>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@@ -24,6 +25,9 @@
|
|||||||
<!-- 数据库客户端 -->
|
<!-- 数据库客户端 -->
|
||||||
<DbCli v-if="activeTab === 'db-cli'"/>
|
<DbCli v-if="activeTab === 'db-cli'"/>
|
||||||
|
|
||||||
|
<!-- 文件管理 -->
|
||||||
|
<FileSystem v-if="activeTab === 'file-system'"/>
|
||||||
|
|
||||||
<!-- 用户查询页面 -->
|
<!-- 用户查询页面 -->
|
||||||
<div v-if="activeTab === 'user'">
|
<div v-if="activeTab === 'user'">
|
||||||
<!-- 查询表单 -->
|
<!-- 查询表单 -->
|
||||||
@@ -110,6 +114,7 @@ import DeviceTest from './components/DeviceTest.vue'
|
|||||||
import DbCli from './views/db-cli/index.vue'
|
import DbCli from './views/db-cli/index.vue'
|
||||||
import ThemeToggle from './components/ThemeToggle.vue'
|
import ThemeToggle from './components/ThemeToggle.vue'
|
||||||
import UpdatePanel from './components/UpdatePanel.vue'
|
import UpdatePanel from './components/UpdatePanel.vue'
|
||||||
|
import FileSystem from './components/FileSystem.vue'
|
||||||
|
|
||||||
const activeTab = ref('db-cli')
|
const activeTab = ref('db-cli')
|
||||||
const showUpdateModal = ref(false)
|
const showUpdateModal = ref(false)
|
||||||
|
|||||||
@@ -63,6 +63,26 @@
|
|||||||
<a-button @click="browseDirectory">浏览</a-button>
|
<a-button @click="browseDirectory">浏览</a-button>
|
||||||
<a-button type="primary" @click="listDirectory">列出目录</a-button>
|
<a-button type="primary" @click="listDirectory">列出目录</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
|
|
||||||
|
<!-- 收藏的文件 -->
|
||||||
|
<a-card size="small" title="⭐ 收藏的文件" v-if="favoriteFiles.length > 0">
|
||||||
|
<a-space wrap>
|
||||||
|
<a-tag
|
||||||
|
v-for="fav in favoriteFiles"
|
||||||
|
:key="fav.path"
|
||||||
|
closable
|
||||||
|
@close="removeFavorite(fav.path)"
|
||||||
|
@click="openFavoriteFile(fav.path)"
|
||||||
|
style="cursor: pointer; margin-bottom: 4px"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<span>{{ fav.is_dir ? '📁' : '📄' }}</span>
|
||||||
|
</template>
|
||||||
|
{{ fav.name }}
|
||||||
|
</a-tag>
|
||||||
|
</a-space>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
<a-row :gutter="16">
|
<a-row :gutter="16">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-card size="small" title="文件列表">
|
<a-card size="small" title="文件列表">
|
||||||
@@ -78,6 +98,14 @@
|
|||||||
<a-space>
|
<a-space>
|
||||||
<span>{{ item.is_dir ? '📁' : '📄' }}</span>
|
<span>{{ item.is_dir ? '📁' : '📄' }}</span>
|
||||||
<a @click="selectFile(item.path)">{{ item.name }}</a>
|
<a @click="selectFile(item.path)">{{ item.name }}</a>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click.stop="toggleFavorite(item)"
|
||||||
|
:style="{ color: isFavorite(item.path) ? '#ffcd00' : '' }"
|
||||||
|
>
|
||||||
|
{{ isFavorite(item.path) ? '⭐' : '☆' }}
|
||||||
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
@@ -93,11 +121,23 @@
|
|||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-card size="small" title="文件内容">
|
<a-card size="small" title="文件内容">
|
||||||
<a-space direction="vertical" :size="8" style="width: 100%">
|
<a-space direction="vertical" :size="8" style="width: 100%">
|
||||||
<a-textarea
|
<div
|
||||||
v-model="fileContent"
|
class="file-content-wrapper"
|
||||||
:rows="10"
|
:style="{ height: fileContentHeight + 'px' }"
|
||||||
placeholder="文件内容将显示在这里"
|
>
|
||||||
/>
|
<a-textarea
|
||||||
|
v-model="fileContent"
|
||||||
|
class="file-content-textarea"
|
||||||
|
placeholder="文件内容将显示在这里"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="resize-handle"
|
||||||
|
@mousedown="startResize"
|
||||||
|
title="拖拽调整高度"
|
||||||
|
>
|
||||||
|
<div class="resize-handle-bar"></div>
|
||||||
|
</div>
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button type="primary" @click="readFile" :loading="fileLoading">读取文件</a-button>
|
<a-button type="primary" @click="readFile" :loading="fileLoading">读取文件</a-button>
|
||||||
<a-button @click="writeFile" :loading="fileLoading">写入文件</a-button>
|
<a-button @click="writeFile" :loading="fileLoading">写入文件</a-button>
|
||||||
@@ -145,7 +185,9 @@ const STORAGE_KEYS = {
|
|||||||
FILE_PATH: 'device-test-file-path',
|
FILE_PATH: 'device-test-file-path',
|
||||||
FILE_LIST: 'device-test-file-list',
|
FILE_LIST: 'device-test-file-list',
|
||||||
FILE_CONTENT: 'device-test-file-content',
|
FILE_CONTENT: 'device-test-file-content',
|
||||||
PATH_HISTORY: 'device-test-path-history'
|
PATH_HISTORY: 'device-test-path-history',
|
||||||
|
FILE_CONTENT_HEIGHT: 'device-test-file-content-height',
|
||||||
|
FAVORITE_FILES: 'device-test-favorite-files'
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemInfo = ref(null)
|
const systemInfo = ref(null)
|
||||||
@@ -159,6 +201,9 @@ const fileLoading = ref(false)
|
|||||||
const envVars = ref(null)
|
const envVars = ref(null)
|
||||||
const envLoading = ref(false)
|
const envLoading = ref(false)
|
||||||
const pathHistory = ref([]) // 路径历史记录
|
const pathHistory = ref([]) // 路径历史记录
|
||||||
|
const fileContentHeight = ref(200) // 文件内容区域高度(默认200px)
|
||||||
|
const isResizing = ref(false) // 是否正在拖拽
|
||||||
|
const favoriteFiles = ref([]) // 收藏的文件列表
|
||||||
|
|
||||||
const diskColumns = [
|
const diskColumns = [
|
||||||
{title: '设备', dataIndex: 'device', width: 120},
|
{title: '设备', dataIndex: 'device', width: 120},
|
||||||
@@ -317,6 +362,37 @@ const formatBytes = (bytes) => {
|
|||||||
return (bytes / Math.pow(unit, exp)).toFixed(2) + ' ' + 'KMGTPE'[exp - 1] + 'B'
|
return (bytes / Math.pow(unit, exp)).toFixed(2) + ' ' + 'KMGTPE'[exp - 1] + 'B'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开始拖拽
|
||||||
|
const startResize = (e) => {
|
||||||
|
isResizing.value = true
|
||||||
|
const startY = e.clientY
|
||||||
|
const startHeight = fileContentHeight.value
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
if (!isResizing.value) return
|
||||||
|
|
||||||
|
const deltaY = moveEvent.clientY - startY
|
||||||
|
const newHeight = startHeight + deltaY
|
||||||
|
|
||||||
|
// 限制高度范围
|
||||||
|
if (newHeight >= 100 && newHeight <= 800) {
|
||||||
|
fileContentHeight.value = newHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isResizing.value = false
|
||||||
|
document.removeEventListener('mousemove', onMouseMove)
|
||||||
|
document.removeEventListener('mouseup', onMouseUp)
|
||||||
|
|
||||||
|
// 保存高度到 localStorage
|
||||||
|
saveToStorage(STORAGE_KEYS.FILE_CONTENT_HEIGHT, fileContentHeight.value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
// 从 localStorage 加载数据
|
// 从 localStorage 加载数据
|
||||||
const loadFromStorage = () => {
|
const loadFromStorage = () => {
|
||||||
try {
|
try {
|
||||||
@@ -324,11 +400,20 @@ const loadFromStorage = () => {
|
|||||||
const savedFileList = localStorage.getItem(STORAGE_KEYS.FILE_LIST)
|
const savedFileList = localStorage.getItem(STORAGE_KEYS.FILE_LIST)
|
||||||
const savedFileContent = localStorage.getItem(STORAGE_KEYS.FILE_CONTENT)
|
const savedFileContent = localStorage.getItem(STORAGE_KEYS.FILE_CONTENT)
|
||||||
const savedHistory = localStorage.getItem(STORAGE_KEYS.PATH_HISTORY)
|
const savedHistory = localStorage.getItem(STORAGE_KEYS.PATH_HISTORY)
|
||||||
|
const savedHeight = localStorage.getItem(STORAGE_KEYS.FILE_CONTENT_HEIGHT)
|
||||||
|
const savedFavorites = localStorage.getItem(STORAGE_KEYS.FAVORITE_FILES)
|
||||||
|
|
||||||
if (savedPath) filePath.value = savedPath
|
if (savedPath) filePath.value = savedPath
|
||||||
if (savedFileList) fileList.value = JSON.parse(savedFileList)
|
if (savedFileList) fileList.value = JSON.parse(savedFileList)
|
||||||
if (savedFileContent) fileContent.value = savedFileContent
|
if (savedFileContent) fileContent.value = savedFileContent
|
||||||
if (savedHistory) pathHistory.value = JSON.parse(savedHistory)
|
if (savedHistory) pathHistory.value = JSON.parse(savedHistory)
|
||||||
|
if (savedFavorites) favoriteFiles.value = JSON.parse(savedFavorites)
|
||||||
|
if (savedHeight) {
|
||||||
|
const height = parseInt(savedHeight)
|
||||||
|
if (!isNaN(height) && height >= 100 && height <= 800) {
|
||||||
|
fileContentHeight.value = height
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('从 localStorage 加载数据失败:', error)
|
console.error('从 localStorage 加载数据失败:', error)
|
||||||
}
|
}
|
||||||
@@ -368,6 +453,58 @@ const addToHistory = (path) => {
|
|||||||
saveToStorage(STORAGE_KEYS.PATH_HISTORY, pathHistory.value)
|
saveToStorage(STORAGE_KEYS.PATH_HISTORY, pathHistory.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否已收藏
|
||||||
|
const isFavorite = (path) => {
|
||||||
|
return favoriteFiles.value.some(fav => fav.path === path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换收藏状态
|
||||||
|
const toggleFavorite = (item) => {
|
||||||
|
const index = favoriteFiles.value.findIndex(fav => fav.path === item.path)
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
// 已收藏,取消收藏
|
||||||
|
favoriteFiles.value.splice(index, 1)
|
||||||
|
Message.info('已取消收藏: ' + item.name)
|
||||||
|
} else {
|
||||||
|
// 未收藏,添加收藏
|
||||||
|
favoriteFiles.value.push({
|
||||||
|
path: item.path,
|
||||||
|
name: item.name,
|
||||||
|
is_dir: item.is_dir
|
||||||
|
})
|
||||||
|
Message.success('已收藏: ' + item.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
saveToStorage(STORAGE_KEYS.FAVORITE_FILES, favoriteFiles.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除收藏
|
||||||
|
const removeFavorite = (path) => {
|
||||||
|
const index = favoriteFiles.value.findIndex(fav => fav.path === path)
|
||||||
|
if (index > -1) {
|
||||||
|
const name = favoriteFiles.value[index].name
|
||||||
|
favoriteFiles.value.splice(index, 1)
|
||||||
|
saveToStorage(STORAGE_KEYS.FAVORITE_FILES, favoriteFiles.value)
|
||||||
|
Message.info('已取消收藏: ' + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开收藏的文件
|
||||||
|
const openFavoriteFile = (path) => {
|
||||||
|
filePath.value = path
|
||||||
|
addToHistory(path)
|
||||||
|
|
||||||
|
// 判断是文件还是目录
|
||||||
|
const fav = favoriteFiles.value.find(f => f.path === path)
|
||||||
|
if (fav && fav.is_dir) {
|
||||||
|
listDirectory()
|
||||||
|
} else {
|
||||||
|
readFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 监听数据变化自动保存
|
// 监听数据变化自动保存
|
||||||
watch(filePath, (newPath) => {
|
watch(filePath, (newPath) => {
|
||||||
saveToStorage(STORAGE_KEYS.FILE_PATH, newPath)
|
saveToStorage(STORAGE_KEYS.FILE_PATH, newPath)
|
||||||
@@ -397,4 +534,47 @@ onMounted(() => {
|
|||||||
.test-card {
|
.test-card {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文件内容区域容器 */
|
||||||
|
.file-content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件内容文本框 */
|
||||||
|
.file-content-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽手柄 */
|
||||||
|
.resize-handle {
|
||||||
|
height: 8px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 4px 0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
background: var(--color-fill-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽手柄的视觉指示条 */
|
||||||
|
.resize-handle-bar {
|
||||||
|
width: 40px;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--color-border-3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover .resize-handle-bar {
|
||||||
|
background: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
559
web/src/components/FileSystem.vue
Normal file
559
web/src/components/FileSystem.vue
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-system">
|
||||||
|
<a-space direction="vertical" style="width: 100%" :size="16">
|
||||||
|
|
||||||
|
<!-- 路径输入 -->
|
||||||
|
<a-card title="📂 文件浏览">
|
||||||
|
<a-space direction="vertical" :size="12" style="width: 100%">
|
||||||
|
<a-input-group>
|
||||||
|
<a-auto-complete
|
||||||
|
v-model="filePath"
|
||||||
|
:data="pathHistory"
|
||||||
|
placeholder="输入文件或目录路径 (如: C:\Users 或 /home/user)"
|
||||||
|
style="flex: 1"
|
||||||
|
@select="onPathSelect"
|
||||||
|
/>
|
||||||
|
<a-button @click="browseDirectory">
|
||||||
|
<template #icon>
|
||||||
|
<icon-folder />
|
||||||
|
</template>
|
||||||
|
浏览
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="listDirectory" :loading="fileLoading">
|
||||||
|
<template #icon>
|
||||||
|
<icon-list />
|
||||||
|
</template>
|
||||||
|
列出目录
|
||||||
|
</a-button>
|
||||||
|
</a-input-group>
|
||||||
|
|
||||||
|
<!-- 常用路径快捷按钮 -->
|
||||||
|
<a-space wrap>
|
||||||
|
<a-tag
|
||||||
|
v-for="shortcut in commonPaths"
|
||||||
|
:key="shortcut.path"
|
||||||
|
@click="openPath(shortcut.path)"
|
||||||
|
style="cursor: pointer"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-forward />
|
||||||
|
</template>
|
||||||
|
{{ shortcut.name }}
|
||||||
|
</a-tag>
|
||||||
|
</a-space>
|
||||||
|
</a-space>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 收藏的文件 -->
|
||||||
|
<a-card title="⭐ 收藏夹" v-if="favoriteFiles.length > 0">
|
||||||
|
<a-space wrap>
|
||||||
|
<a-tag
|
||||||
|
v-for="fav in favoriteFiles"
|
||||||
|
:key="fav.path"
|
||||||
|
closable
|
||||||
|
@close="removeFavorite(fav.path)"
|
||||||
|
@click="openFavoriteFile(fav.path)"
|
||||||
|
style="cursor: pointer; margin-bottom: 4px; padding: 4px 8px"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<span style="font-size: 16px">{{ fav.is_dir ? '📁' : '📄' }}</span>
|
||||||
|
</template>
|
||||||
|
{{ fav.name }}
|
||||||
|
</a-tag>
|
||||||
|
</a-space>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 文件列表和编辑器 -->
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-card title="📋 文件列表" style="height: 100%">
|
||||||
|
<a-list
|
||||||
|
:data="fileList"
|
||||||
|
:loading="fileLoading"
|
||||||
|
:bordered="false"
|
||||||
|
style="max-height: 500px; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<template #item="{ item }">
|
||||||
|
<a-list-item class="file-item">
|
||||||
|
<a-list-item-meta>
|
||||||
|
<template #title>
|
||||||
|
<a-space>
|
||||||
|
<span style="font-size: 18px">{{ item.is_dir ? '📁' : '📄' }}</span>
|
||||||
|
<a @click="selectFile(item.path)" class="file-name">{{ item.name }}</a>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click.stop="toggleFavorite(item)"
|
||||||
|
:style="{ color: isFavorite(item.path) ? '#ffcd00' : '#86909c' }"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-star-fill v-if="isFavorite(item.path)" />
|
||||||
|
<icon-star v-else />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<a-space split="|">
|
||||||
|
<span v-if="!item.is_dir">{{ formatBytes(item.size) }}</span>
|
||||||
|
<span>{{ item.mod_time }}</span>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-card title="📝 文件内容">
|
||||||
|
<a-space direction="vertical" :size="8" style="width: 100%">
|
||||||
|
<div
|
||||||
|
class="file-content-wrapper"
|
||||||
|
:style="{ height: fileContentHeight + 'px' }"
|
||||||
|
>
|
||||||
|
<a-textarea
|
||||||
|
v-model="fileContent"
|
||||||
|
class="file-content-textarea"
|
||||||
|
placeholder="文件内容将显示在这里,点击文件列表中的文件即可查看"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="resize-handle"
|
||||||
|
@mousedown="startResize"
|
||||||
|
title="拖拽调整高度"
|
||||||
|
>
|
||||||
|
<div class="resize-handle-bar"></div>
|
||||||
|
</div>
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="readFile" :loading="fileLoading">
|
||||||
|
<template #icon>
|
||||||
|
<icon-file />
|
||||||
|
</template>
|
||||||
|
读取
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="writeFile" :loading="fileLoading">
|
||||||
|
<template #icon>
|
||||||
|
<icon-save />
|
||||||
|
</template>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
<a-button status="danger" @click="deleteFile" :loading="fileLoading">
|
||||||
|
<template #icon>
|
||||||
|
<icon-delete />
|
||||||
|
</template>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="clearContent">
|
||||||
|
<template #icon>
|
||||||
|
<icon-eraser />
|
||||||
|
</template>
|
||||||
|
清空
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-space>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
|
import {
|
||||||
|
IconFolder,
|
||||||
|
IconList,
|
||||||
|
IconForward,
|
||||||
|
IconStarFill,
|
||||||
|
IconStar,
|
||||||
|
IconFile,
|
||||||
|
IconSave,
|
||||||
|
IconDelete,
|
||||||
|
IconEraser
|
||||||
|
} from '@arco-design/web-vue/es/icon'
|
||||||
|
import {
|
||||||
|
listDir,
|
||||||
|
readFile as readFileApi,
|
||||||
|
writeFile as writeFileApi,
|
||||||
|
deletePath
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
// localStorage 键名
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
FILE_PATH: 'filesystem-file-path',
|
||||||
|
FILE_LIST: 'filesystem-file-list',
|
||||||
|
FILE_CONTENT: 'filesystem-file-content',
|
||||||
|
PATH_HISTORY: 'filesystem-path-history',
|
||||||
|
FILE_CONTENT_HEIGHT: 'filesystem-file-content-height',
|
||||||
|
FAVORITE_FILES: 'filesystem-favorite-files'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 常用路径快捷方式
|
||||||
|
const commonPaths = computed(() => {
|
||||||
|
const platform = window.navigator.platform
|
||||||
|
if (platform.includes('Win')) {
|
||||||
|
return [
|
||||||
|
{ name: '桌面', path: `${process.env.USERPROFILE || ''}\\Desktop` },
|
||||||
|
{ name: '文档', path: `${process.env.USERPROFILE || ''}\\Documents` },
|
||||||
|
{ name: '下载', path: `${process.env.USERPROFILE || ''}\\Downloads` },
|
||||||
|
{ name: 'C盘根目录', path: 'C:\\' },
|
||||||
|
{ name: 'D盘根目录', path: 'D:\\' }
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{ name: '用户主目录', path: '~' },
|
||||||
|
{ name: '桌面', path: '~/Desktop' },
|
||||||
|
{ name: '文档', path: '~/Documents' },
|
||||||
|
{ name: '下载', path: '~/Downloads' },
|
||||||
|
{ name: '根目录', path: '/' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const filePath = ref('')
|
||||||
|
const fileContent = ref('')
|
||||||
|
const fileList = ref([])
|
||||||
|
const fileLoading = ref(false)
|
||||||
|
const pathHistory = ref([])
|
||||||
|
const fileContentHeight = ref(250)
|
||||||
|
const favoriteFiles = ref([])
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatBytes = (bytes) => {
|
||||||
|
if (!bytes) return '0 B'
|
||||||
|
const unit = 1024
|
||||||
|
if (bytes < unit) return bytes + ' B'
|
||||||
|
const exp = Math.floor(Math.log(bytes) / Math.log(unit))
|
||||||
|
return (bytes / Math.pow(unit, exp)).toFixed(2) + ' ' + 'KMGTPE'[exp - 1] + 'B'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列出目录
|
||||||
|
const listDirectory = async () => {
|
||||||
|
if (!filePath.value) {
|
||||||
|
Message.error('请输入目录路径')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addToHistory(filePath.value)
|
||||||
|
fileLoading.value = true
|
||||||
|
try {
|
||||||
|
fileList.value = await listDir(filePath.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('列出目录失败:', error)
|
||||||
|
Message.error('列出目录失败: ' + (error.message || error))
|
||||||
|
} finally {
|
||||||
|
fileLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路径选择
|
||||||
|
const onPathSelect = (value) => {
|
||||||
|
filePath.value = value
|
||||||
|
listDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开路径
|
||||||
|
const openPath = (path) => {
|
||||||
|
filePath.value = path
|
||||||
|
listDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 浏览目录
|
||||||
|
const browseDirectory = () => {
|
||||||
|
const path = prompt('请输入目录路径(例如: C:\\Users 或 /home/user)')
|
||||||
|
if (path) {
|
||||||
|
filePath.value = path
|
||||||
|
listDirectory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择文件
|
||||||
|
const selectFile = (path) => {
|
||||||
|
filePath.value = path
|
||||||
|
addToHistory(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件
|
||||||
|
const readFile = async () => {
|
||||||
|
if (!filePath.value) {
|
||||||
|
Message.error('请输入文件路径')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addToHistory(filePath.value)
|
||||||
|
fileLoading.value = true
|
||||||
|
try {
|
||||||
|
fileContent.value = await readFileApi(filePath.value)
|
||||||
|
Message.success('文件读取成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取文件失败:', error)
|
||||||
|
Message.error('读取文件失败: ' + (error.message || error))
|
||||||
|
} finally {
|
||||||
|
fileLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
const writeFile = async () => {
|
||||||
|
if (!filePath.value) {
|
||||||
|
Message.error('请输入文件路径')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileLoading.value = true
|
||||||
|
try {
|
||||||
|
await writeFileApi(filePath.value, fileContent.value)
|
||||||
|
Message.success('文件保存成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('写入文件失败:', error)
|
||||||
|
Message.error('写入文件失败: ' + (error.message || error))
|
||||||
|
} finally {
|
||||||
|
fileLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
const deleteFile = async () => {
|
||||||
|
if (!filePath.value) {
|
||||||
|
Message.error('请输入文件路径')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除 ${filePath.value} 吗?此操作不可恢复!`,
|
||||||
|
onOk: async () => {
|
||||||
|
fileLoading.value = true
|
||||||
|
try {
|
||||||
|
await deletePath(filePath.value)
|
||||||
|
Message.success('删除成功')
|
||||||
|
// 清空状态
|
||||||
|
filePath.value = ''
|
||||||
|
fileContent.value = ''
|
||||||
|
fileList.value = []
|
||||||
|
// 重新列出目录
|
||||||
|
if (pathHistory.value.length > 0) {
|
||||||
|
filePath.value = pathHistory.value[0]
|
||||||
|
listDirectory()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
Message.error('删除失败: ' + (error.message || error))
|
||||||
|
} finally {
|
||||||
|
fileLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空内容
|
||||||
|
const clearContent = () => {
|
||||||
|
fileContent.value = ''
|
||||||
|
Message.info('内容已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽调整高度
|
||||||
|
const startResize = (e) => {
|
||||||
|
const startY = e.clientY
|
||||||
|
const startHeight = fileContentHeight.value
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
const deltaY = moveEvent.clientY - startY
|
||||||
|
const newHeight = startHeight + deltaY
|
||||||
|
if (newHeight >= 150 && newHeight <= 800) {
|
||||||
|
fileContentHeight.value = newHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove)
|
||||||
|
document.removeEventListener('mouseup', onMouseUp)
|
||||||
|
saveToStorage(STORAGE_KEYS.FILE_CONTENT_HEIGHT, fileContentHeight.value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 历史记录
|
||||||
|
const addToHistory = (path) => {
|
||||||
|
if (!path || path.trim() === '') return
|
||||||
|
|
||||||
|
const index = pathHistory.value.indexOf(path)
|
||||||
|
if (index > -1) {
|
||||||
|
pathHistory.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathHistory.value.unshift(path)
|
||||||
|
if (pathHistory.value.length > 20) {
|
||||||
|
pathHistory.value = pathHistory.value.slice(0, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToStorage(STORAGE_KEYS.PATH_HISTORY, pathHistory.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收藏功能
|
||||||
|
const isFavorite = (path) => {
|
||||||
|
return favoriteFiles.value.some(fav => fav.path === path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFavorite = (item) => {
|
||||||
|
const index = favoriteFiles.value.findIndex(fav => fav.path === item.path)
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
favoriteFiles.value.splice(index, 1)
|
||||||
|
Message.info('已取消收藏: ' + item.name)
|
||||||
|
} else {
|
||||||
|
favoriteFiles.value.push({
|
||||||
|
path: item.path,
|
||||||
|
name: item.name,
|
||||||
|
is_dir: item.is_dir
|
||||||
|
})
|
||||||
|
Message.success('已收藏: ' + item.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToStorage(STORAGE_KEYS.FAVORITE_FILES, favoriteFiles.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFavorite = (path) => {
|
||||||
|
const index = favoriteFiles.value.findIndex(fav => fav.path === path)
|
||||||
|
if (index > -1) {
|
||||||
|
const name = favoriteFiles.value[index].name
|
||||||
|
favoriteFiles.value.splice(index, 1)
|
||||||
|
saveToStorage(STORAGE_KEYS.FAVORITE_FILES, favoriteFiles.value)
|
||||||
|
Message.info('已取消收藏: ' + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openFavoriteFile = (path) => {
|
||||||
|
filePath.value = path
|
||||||
|
addToHistory(path)
|
||||||
|
|
||||||
|
const fav = favoriteFiles.value.find(f => f.path === path)
|
||||||
|
if (fav && fav.is_dir) {
|
||||||
|
listDirectory()
|
||||||
|
} else {
|
||||||
|
readFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// localStorage 操作
|
||||||
|
const loadFromStorage = () => {
|
||||||
|
try {
|
||||||
|
const savedPath = localStorage.getItem(STORAGE_KEYS.FILE_PATH)
|
||||||
|
const savedFileList = localStorage.getItem(STORAGE_KEYS.FILE_LIST)
|
||||||
|
const savedFileContent = localStorage.getItem(STORAGE_KEYS.FILE_CONTENT)
|
||||||
|
const savedHistory = localStorage.getItem(STORAGE_KEYS.PATH_HISTORY)
|
||||||
|
const savedHeight = localStorage.getItem(STORAGE_KEYS.FILE_CONTENT_HEIGHT)
|
||||||
|
const savedFavorites = localStorage.getItem(STORAGE_KEYS.FAVORITE_FILES)
|
||||||
|
|
||||||
|
if (savedPath) filePath.value = savedPath
|
||||||
|
if (savedFileList) fileList.value = JSON.parse(savedFileList)
|
||||||
|
if (savedFileContent) fileContent.value = savedFileContent
|
||||||
|
if (savedHistory) pathHistory.value = JSON.parse(savedHistory)
|
||||||
|
if (savedFavorites) favoriteFiles.value = JSON.parse(savedFavorites)
|
||||||
|
if (savedHeight) {
|
||||||
|
const height = parseInt(savedHeight)
|
||||||
|
if (!isNaN(height) && height >= 150 && height <= 800) {
|
||||||
|
fileContentHeight.value = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从 localStorage 加载数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveToStorage = (key, value) => {
|
||||||
|
try {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存到 localStorage 失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据变化自动保存
|
||||||
|
watch(filePath, (newPath) => {
|
||||||
|
saveToStorage(STORAGE_KEYS.FILE_PATH, newPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(fileContent, (newContent) => {
|
||||||
|
saveToStorage(STORAGE_KEYS.FILE_CONTENT, newContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(fileList, (newList) => {
|
||||||
|
saveToStorage(STORAGE_KEYS.FILE_LIST, newList)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadFromStorage()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-system {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height 0.1s ease;
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
height: 8px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 4px 0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
background: var(--color-fill-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-bar {
|
||||||
|
width: 40px;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--color-border-3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover .resize-handle-bar {
|
||||||
|
background: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user