PhysicalLibraryWeb/src/views/DataImport.vue

496 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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