496 lines
14 KiB
Vue
496 lines
14 KiB
Vue
<template>
|
||
<div class="data-space-page">
|
||
<!-- 顶部工具栏 -->
|
||
<div class="top-toolbar">
|
||
<div class="toolbar-left">
|
||
<el-button link type="primary" @click="goBackList">
|
||
<el-icon><ArrowLeft /></el-icon>
|
||
返回列表
|
||
</el-button>
|
||
<el-select v-model="currentSpace" class="space-select" size="large">
|
||
<el-option
|
||
v-for="space in spaceList"
|
||
:key="space.id"
|
||
:label="space.label"
|
||
:value="space.id"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<el-button-group class="view-toggle">
|
||
<el-button :type="viewMode === 'grid' ? 'primary' : 'default'" @click="viewMode = 'grid'">
|
||
<el-icon><Grid /></el-icon>
|
||
</el-button>
|
||
<el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
|
||
<el-icon><List /></el-icon>
|
||
</el-button>
|
||
</el-button-group>
|
||
<el-button type="primary" @click="uploadDialogVisible = true">
|
||
<el-icon><Upload /></el-icon>
|
||
上传文件
|
||
</el-button>
|
||
<el-button type="warning" plain @click="importDialogVisible = true">
|
||
<el-icon><Download /></el-icon>
|
||
空间导入
|
||
</el-button>
|
||
<el-button type="success" plain @click="ftpDialogVisible = true">
|
||
<el-icon><Monitor /></el-icon>
|
||
FTP地址
|
||
</el-button>
|
||
<el-button @click="spaceDialogVisible = true">空间管理</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主体:目录树 + 文件预览 -->
|
||
<div class="main-panel">
|
||
<aside class="tree-panel">
|
||
<div class="panel-title">
|
||
<el-icon><FolderOpened /></el-icon>
|
||
文件目录
|
||
</div>
|
||
<el-tree
|
||
ref="treeRef"
|
||
:data="treeData"
|
||
node-key="id"
|
||
:props="treeProps"
|
||
:default-expanded-keys="defaultExpandedKeys"
|
||
:current-node-key="currentFolderId"
|
||
highlight-current
|
||
@node-click="handleNodeClick"
|
||
>
|
||
<template #default="{ node, data }">
|
||
<span class="tree-node">
|
||
<FileTypeIcon type="folder" :size="18" class="tree-folder-icon" />
|
||
<span :class="{ 'is-active': data.id === currentFolderId }">{{ node.label }}</span>
|
||
</span>
|
||
</template>
|
||
</el-tree>
|
||
</aside>
|
||
|
||
<section class="preview-panel">
|
||
<div class="preview-header">
|
||
<span class="path">{{ currentPathLabel }}</span>
|
||
<span class="file-count">共 {{ fileList.length }} 项</span>
|
||
</div>
|
||
|
||
<!-- 网格视图 -->
|
||
<div v-if="viewMode === 'grid'" class="file-grid">
|
||
<div
|
||
v-for="file in fileList"
|
||
:key="file.id"
|
||
class="file-item"
|
||
:class="{ selected: selectedFileId === file.id }"
|
||
@click="selectFile(file)"
|
||
@dblclick="previewFile(file)"
|
||
>
|
||
<FileTypeIcon :ext="file.ext" :size="72" />
|
||
<div class="file-name" :title="file.name">{{ file.name }}</div>
|
||
</div>
|
||
<el-empty v-if="!fileList.length" description="当前目录暂无文件" />
|
||
</div>
|
||
|
||
<!-- 列表视图 -->
|
||
<el-table
|
||
v-else
|
||
:data="fileList"
|
||
border
|
||
stripe
|
||
highlight-current-row
|
||
@row-click="(row) => selectFile(row)"
|
||
@row-dblclick="previewFile"
|
||
>
|
||
<el-table-column label="名称" min-width="260">
|
||
<template #default="{ row }">
|
||
<div class="list-name">
|
||
<FileTypeIcon :ext="row.ext" :size="24" />
|
||
<span>{{ row.name }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="ext" label="类型" width="80" />
|
||
<el-table-column label="大小" width="120">
|
||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="update_time" label="修改时间" width="180" />
|
||
</el-table>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- 上传文件弹窗 -->
|
||
<el-dialog v-model="uploadDialogVisible" title="上传文件" width="520" destroy-on-close>
|
||
<el-upload drag :auto-upload="false" :limit="5">
|
||
<el-icon class="upload-icon"><Upload /></el-icon>
|
||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||
<template #tip>
|
||
<div class="el-upload__tip">上传至当前目录:{{ currentPathLabel }}</div>
|
||
</template>
|
||
</el-upload>
|
||
<template #footer>
|
||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmUpload">确认上传</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 空间导入弹窗 -->
|
||
<el-dialog v-model="importDialogVisible" title="空间导入" width="560" destroy-on-close>
|
||
<el-form label-width="100px">
|
||
<el-form-item label="源空间路径">
|
||
<el-input v-model="importForm.sourcePath" placeholder="s3://bucket/path 或 NAS 路径" />
|
||
</el-form-item>
|
||
<el-form-item label="目标目录">
|
||
<el-input :model-value="currentPathLabel" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="导入说明">
|
||
<el-input v-model="importForm.remark" type="textarea" :rows="2" placeholder="可选" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="importDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmImport">开始导入</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- FTP 地址弹窗 -->
|
||
<el-dialog v-model="ftpDialogVisible" title="FTP 地址" width="520" destroy-on-close>
|
||
<el-descriptions :column="1" border>
|
||
<el-descriptions-item label="FTP 主机">ftp.idsse.ac.cn</el-descriptions-item>
|
||
<el-descriptions-item label="端口">21</el-descriptions-item>
|
||
<el-descriptions-item label="当前路径">/geophys/{{ currentFolderId }}</el-descriptions-item>
|
||
<el-descriptions-item label="完整地址">ftp://ftp.idsse.ac.cn/geophys/{{ currentFolderId }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
<template #footer>
|
||
<el-button @click="ftpDialogVisible = false">关闭</el-button>
|
||
<el-button type="primary" @click="copyFtpPath">复制地址</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 空间管理弹窗 -->
|
||
<el-dialog v-model="spaceDialogVisible" title="空间管理" width="480">
|
||
<p>当前数据空间:<strong>{{ currentSpaceLabel }}</strong></p>
|
||
<p class="tip-text">支持创建子目录、设置权限及配额管理(演示功能)。</p>
|
||
<template #footer>
|
||
<el-button type="primary" @click="spaceDialogVisible = false">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 文件预览弹窗 -->
|
||
<el-dialog v-model="previewVisible" :title="previewFileInfo?.name" width="640">
|
||
<el-descriptions v-if="previewFileInfo" :column="1" border>
|
||
<el-descriptions-item label="文件名">{{ previewFileInfo.name }}</el-descriptions-item>
|
||
<el-descriptions-item label="类型">{{ previewFileInfo.ext?.toUpperCase() }}</el-descriptions-item>
|
||
<el-descriptions-item label="大小">{{ formatFileSize(previewFileInfo.size) }}</el-descriptions-item>
|
||
<el-descriptions-item label="修改时间">{{ previewFileInfo.update_time }}</el-descriptions-item>
|
||
<el-descriptions-item label="所在目录">{{ currentPathLabel }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
ArrowLeft, Grid, List, Upload, Download,
|
||
Monitor, FolderOpened
|
||
} from '@element-plus/icons-vue'
|
||
import FileTypeIcon from '@/components/dataSpace/FileTypeIcon.vue'
|
||
import {
|
||
getDataSpaces,
|
||
getDirectoryTree,
|
||
getFolderFiles
|
||
} from '@/api/dataSpace.js'
|
||
import { DEFAULT_FOLDER_ID } from '@/mock/dataSpace.js'
|
||
|
||
const router = useRouter()
|
||
const treeRef = ref(null)
|
||
|
||
const spaceList = ref([])
|
||
const treeData = ref([])
|
||
const fileList = ref([])
|
||
const currentSpace = ref('geophys-main')
|
||
const currentFolderId = ref(DEFAULT_FOLDER_ID)
|
||
const viewMode = ref('grid')
|
||
const selectedFileId = ref('')
|
||
const defaultExpandedKeys = ref(['root', 'multibeam', 'seismic', 'magnetic'])
|
||
|
||
const uploadDialogVisible = ref(false)
|
||
const importDialogVisible = ref(false)
|
||
const ftpDialogVisible = ref(false)
|
||
const spaceDialogVisible = ref(false)
|
||
const previewVisible = ref(false)
|
||
const previewFileInfo = ref(null)
|
||
|
||
const importForm = ref({ sourcePath: '', remark: '' })
|
||
|
||
const treeProps = { label: 'label', children: 'children' }
|
||
|
||
const currentSpaceLabel = computed(() =>
|
||
spaceList.value.find((s) => s.id === currentSpace.value)?.label || ''
|
||
)
|
||
|
||
/**
|
||
* 构建当前目录路径显示
|
||
*/
|
||
const currentPathLabel = computed(() => {
|
||
const parts = []
|
||
const buildPath = (nodes, targetId, path = []) => {
|
||
for (const node of nodes) {
|
||
const next = [...path, node.label]
|
||
if (node.id === targetId) {
|
||
parts.push(...next)
|
||
return true
|
||
}
|
||
if (node.children?.length && buildPath(node.children, targetId, next)) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
buildPath(treeData.value, currentFolderId.value)
|
||
return parts.join(' / ') || '深海地球物理数据空间'
|
||
})
|
||
|
||
/**
|
||
* 格式化文件大小
|
||
* @param {number} bytes
|
||
*/
|
||
const formatFileSize = (bytes) => {
|
||
if (!bytes) return '—'
|
||
const k = 1024
|
||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
|
||
}
|
||
|
||
const loadSpaces = () => {
|
||
getDataSpaces().then((res) => {
|
||
spaceList.value = res.array || []
|
||
})
|
||
}
|
||
|
||
const loadTree = () => {
|
||
getDirectoryTree().then((res) => {
|
||
treeData.value = res.array || []
|
||
})
|
||
}
|
||
|
||
const loadFiles = () => {
|
||
getFolderFiles({ folder_id: currentFolderId.value }).then((res) => {
|
||
fileList.value = res.array || []
|
||
selectedFileId.value = ''
|
||
})
|
||
}
|
||
|
||
const handleNodeClick = (data) => {
|
||
if (!data.children?.length) {
|
||
currentFolderId.value = data.id
|
||
loadFiles()
|
||
}
|
||
}
|
||
|
||
const selectFile = (file) => {
|
||
selectedFileId.value = file.id
|
||
}
|
||
|
||
const previewFile = (file) => {
|
||
previewFileInfo.value = file
|
||
previewVisible.value = true
|
||
}
|
||
|
||
const goBackList = () => {
|
||
router.push('/demand')
|
||
}
|
||
|
||
const confirmUpload = () => {
|
||
ElMessage.success(`文件已上传至:${currentPathLabel.value}`)
|
||
uploadDialogVisible.value = false
|
||
}
|
||
|
||
const confirmImport = () => {
|
||
if (!importForm.value.sourcePath) {
|
||
ElMessage.warning('请输入源空间路径')
|
||
return
|
||
}
|
||
ElMessage.success('空间导入任务已提交')
|
||
importDialogVisible.value = false
|
||
}
|
||
|
||
const copyFtpPath = () => {
|
||
const path = `ftp://ftp.idsse.ac.cn/geophys/${currentFolderId.value}`
|
||
navigator.clipboard?.writeText(path).then(() => {
|
||
ElMessage.success('FTP 地址已复制')
|
||
}).catch(() => {
|
||
ElMessage.info(path)
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadSpaces()
|
||
loadTree()
|
||
loadFiles()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.data-space-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 220px);
|
||
min-height: 600px;
|
||
background: #fff;
|
||
margin: 0;
|
||
}
|
||
|
||
.top-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #e8e8e8;
|
||
background: #fafafa;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
|
||
.space-select {
|
||
width: 280px;
|
||
}
|
||
}
|
||
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.view-toggle {
|
||
margin-right: 4px;
|
||
}
|
||
}
|
||
|
||
.main-panel {
|
||
flex: 1;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tree-panel {
|
||
width: 280px;
|
||
border-right: 1px solid #e8e8e8;
|
||
overflow-y: auto;
|
||
background: #fafafa;
|
||
|
||
.panel-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 14px 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.tree-node {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 14px;
|
||
|
||
.is-active {
|
||
color: #e6a23c;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
:deep(.el-tree-node__content) {
|
||
height: 36px;
|
||
}
|
||
|
||
:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
|
||
background-color: #ecf5ff;
|
||
}
|
||
}
|
||
|
||
.preview-panel {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
|
||
.preview-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
font-size: 13px;
|
||
color: #666;
|
||
|
||
.path {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.file-grid {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-content: flex-start;
|
||
gap: 24px;
|
||
padding: 24px;
|
||
overflow-y: auto;
|
||
|
||
.file-item {
|
||
width: 120px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 12px 8px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
|
||
&:hover,
|
||
&.selected {
|
||
background: #f0f7ff;
|
||
}
|
||
|
||
.file-name {
|
||
margin-top: 10px;
|
||
font-size: 12px;
|
||
color: #333;
|
||
text-align: center;
|
||
word-break: break-all;
|
||
line-height: 1.4;
|
||
max-width: 110px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.list-name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 48px;
|
||
color: #909399;
|
||
}
|
||
|
||
.tip-text {
|
||
color: #999;
|
||
font-size: 13px;
|
||
}
|
||
|
||
</style>
|