Private
Public Access
1
0

新增:独立的文件管理模块,优化文件操作功能

This commit is contained in:
2026-01-26 02:02:39 +08:00
parent cc50de0323
commit 5ef483c830
3 changed files with 750 additions and 6 deletions

View File

@@ -5,6 +5,7 @@
<h2>Go Desk</h2>
<a-tabs v-model:active-key="activeTab" class="header-tabs">
<a-tab-pane key="db-cli" title="数据库客户端"/>
<a-tab-pane key="file-system" title="文件管理"/>
<a-tab-pane key="user" title="用户查询"/>
<a-tab-pane key="device" title="设备调用测试"/>
</a-tabs>
@@ -24,6 +25,9 @@
<!-- 数据库客户端 -->
<DbCli v-if="activeTab === 'db-cli'"/>
<!-- 文件管理 -->
<FileSystem v-if="activeTab === 'file-system'"/>
<!-- 用户查询页面 -->
<div v-if="activeTab === 'user'">
<!-- 查询表单 -->
@@ -110,6 +114,7 @@ import DeviceTest from './components/DeviceTest.vue'
import DbCli from './views/db-cli/index.vue'
import ThemeToggle from './components/ThemeToggle.vue'
import UpdatePanel from './components/UpdatePanel.vue'
import FileSystem from './components/FileSystem.vue'
const activeTab = ref('db-cli')
const showUpdateModal = ref(false)

View File

@@ -63,6 +63,26 @@
<a-button @click="browseDirectory">浏览</a-button>
<a-button type="primary" @click="listDirectory">列出目录</a-button>
</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-col :span="12">
<a-card size="small" title="文件列表">
@@ -78,6 +98,14 @@
<a-space>
<span>{{ item.is_dir ? '📁' : '📄' }}</span>
<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>
</template>
<template #description>
@@ -93,11 +121,23 @@
<a-col :span="12">
<a-card size="small" title="文件内容">
<a-space direction="vertical" :size="8" style="width: 100%">
<a-textarea
v-model="fileContent"
:rows="10"
placeholder="文件内容将显示在这里"
/>
<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">读取文件</a-button>
<a-button @click="writeFile" :loading="fileLoading">写入文件</a-button>
@@ -145,7 +185,9 @@ const STORAGE_KEYS = {
FILE_PATH: 'device-test-file-path',
FILE_LIST: 'device-test-file-list',
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)
@@ -159,6 +201,9 @@ const fileLoading = ref(false)
const envVars = ref(null)
const envLoading = ref(false)
const pathHistory = ref([]) // 路径历史记录
const fileContentHeight = ref(200) // 文件内容区域高度默认200px
const isResizing = ref(false) // 是否正在拖拽
const favoriteFiles = ref([]) // 收藏的文件列表
const diskColumns = [
{title: '设备', dataIndex: 'device', width: 120},
@@ -317,6 +362,37 @@ const formatBytes = (bytes) => {
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 加载数据
const loadFromStorage = () => {
try {
@@ -324,11 +400,20 @@ const loadFromStorage = () => {
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 >= 100 && height <= 800) {
fileContentHeight.value = height
}
}
} catch (error) {
console.error('从 localStorage 加载数据失败:', error)
}
@@ -368,6 +453,58 @@ const addToHistory = (path) => {
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) => {
saveToStorage(STORAGE_KEYS.FILE_PATH, newPath)
@@ -397,4 +534,47 @@ onMounted(() => {
.test-card {
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>

View 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>