PhysicalLibraryWeb/src/views/DataImport.vue

496 lines
14 KiB
Vue
Raw Normal View History

2026-06-13 10:28:43 +08:00
<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>