feat(file): 实现文件管理页面左右分栏布局和实时预览功能
- 添加左右分栏布局结构,支持拖拽调节面板宽度 - 在左侧显示文件列表表格,右侧显示选中文件的详细信息和预览 - 集成多种文件类型的预览组件(图片、PDF、Word、Excel等) - 实现点击表格行选中文件并同步到右侧预览区域 - 添加文件详情弹窗用于显示完整元数据信息 - 优化文件统计卡片的样式设计和间距布局 - 移除原有的独立预览对话框,整合到右侧预览面板中
This commit is contained in:
@@ -28,8 +28,12 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
|
||||
<!-- 左右分栏布局(可拖拽调节宽度) -->
|
||||
<div class="file-layout" ref="layoutContainer">
|
||||
<!-- 左侧:文件列表 -->
|
||||
<div class="file-left" :style="{ width: leftPanelWidth }">
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
|
||||
<el-form-item label="文件名称" prop="fileName">
|
||||
<el-input
|
||||
v-model="queryParams.fileName"
|
||||
@@ -135,63 +139,19 @@
|
||||
</el-row>
|
||||
|
||||
<!-- 文件列表表格 -->
|
||||
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange">
|
||||
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" highlight-current-row @row-click="handleRowClick">
|
||||
<el-table-column type="selection" width="55" align="center" v-if="activeTab === 'my' || activeTab === 'all'" />
|
||||
<el-table-column label="文件名称" align="center" prop="fileName" :show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" @click="handlePreview(scope.row)">{{ scope.row.fileName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件类型" align="center" prop="fileType">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_file_type" :value="scope.row.fileType"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件大小" align="center" prop="fileSize">
|
||||
<template slot-scope="scope">
|
||||
{{ formatFileSize(scope.row.fileSize) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单编号" align="center" prop="orderNo" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="所属部门" align="center" prop="dept" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="可见范围" align="center" prop="scopeType">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.scopeType === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ scope.row.scopeType === 1 ? '公开' : '私有' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上传人" align="center" prop="createBy" />
|
||||
<el-table-column label="上传时间" align="center" prop="createTime" width="160">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handlePreview(scope.row)"
|
||||
>预览</el-button>
|
||||
<el-button
|
||||
v-if="canEdit(scope.row)"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="canEdit(scope.row)"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</KLPTable>
|
||||
|
||||
<pagination
|
||||
@@ -201,6 +161,46 @@
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<!-- 拖拽分隔条 -->
|
||||
<div class="resize-handle" @mousedown="startResize" :class="{ dragging: isDragging }"></div>
|
||||
<!-- 右侧:文件预览 -->
|
||||
<div class="file-right">
|
||||
<div class="preview-panel">
|
||||
<div v-if="!selectedFile" class="preview-empty">
|
||||
<el-empty description="请选择左侧文件进行预览" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="preview-header">
|
||||
<span class="preview-filename" :title="selectedFile.fileName">{{ selectedFile.fileName }}</span>
|
||||
<div>
|
||||
<el-button size="mini" type="text" @click="handlePreview(selectedFile)">预览</el-button>
|
||||
<el-button size="mini" plain icon="el-icon-info" @click="handleShowInfo(selectedFile)">查看</el-button>
|
||||
<el-button size="mini" type="primary" icon="el-icon-download" @click="downloadFile(selectedFile)">下载</el-button>
|
||||
<el-button v-if="canEdit(selectedFile)" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(selectedFile)">编辑</el-button>
|
||||
<el-button v-if="canEdit(selectedFile)" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(selectedFile)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-meta">
|
||||
<span>{{ formatFileSize(selectedFile.fileSize) }}</span>
|
||||
<dict-tag :options="dict.type.sys_file_type" :value="selectedFile.fileType" />
|
||||
<span>{{ selectedFile.createBy }}</span>
|
||||
<span>{{ parseTime(selectedFile.createTime) }}</span>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<ImagePreview v-if="fileTypeCategory === 'image'" :src="selectedFile.filePath" />
|
||||
<PdfPreview v-else-if="fileTypeCategory === 'pdf'" :src="selectedFile.filePath" />
|
||||
<DocxPreview v-else-if="fileTypeCategory === 'docx'" :src="selectedFile.filePath" />
|
||||
<XlsxPreview v-else-if="fileTypeCategory === 'xlsx'" :src="selectedFile.filePath" />
|
||||
<XlsPreview v-else-if="fileTypeCategory === 'xls'" :src="selectedFile.filePath" />
|
||||
<div v-else class="preview-not-supported">
|
||||
<el-empty description="暂不支持预览此文件类型" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传/编辑对话框 -->
|
||||
<el-dialog :title="dialogTitle" :visible.sync="open" width="650px" append-to-body @close="handleDialogClose">
|
||||
@@ -265,32 +265,28 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 文件预览对话框 -->
|
||||
<el-dialog :title="previewTitle" :visible.sync="previewVisible" width="80%" append-to-body>
|
||||
<div v-if="previewFile" class="file-preview-container">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="文件名称">{{ previewFile.fileName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件类型">
|
||||
<dict-tag :options="dict.type.sys_file_type" :value="previewFile.fileType"/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件大小">{{ formatFileSize(previewFile.fileSize) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件后缀">{{ previewFile.suffix }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单编号">{{ previewFile.orderNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属部门">{{ previewFile.dept || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="可见范围">
|
||||
<el-tag :type="previewFile.scopeType === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ previewFile.scopeType === 1 ? '公开' : '私有' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传人">{{ previewFile.createBy }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上传时间">{{ parseTime(previewFile.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ previewFile.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="previewFile.filePath" style="margin-top: 16px;">
|
||||
<el-button type="primary" size="small" @click="downloadFile(previewFile)">下载文件</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件详情对话框 -->
|
||||
<el-dialog :title="infoTitle" :visible.sync="infoVisible" width="600px" append-to-body>
|
||||
<el-descriptions v-if="infoFile" :column="2" border>
|
||||
<el-descriptions-item label="文件名称">{{ infoFile.fileName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件类型">
|
||||
<dict-tag :options="dict.type.sys_file_type" :value="infoFile.fileType"/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件大小">{{ formatFileSize(infoFile.fileSize) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件后缀">{{ infoFile.suffix || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单编号">{{ infoFile.orderNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属部门">{{ infoFile.dept || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="可见范围">
|
||||
<el-tag :type="infoFile.scopeType === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ infoFile.scopeType === 1 ? '公开' : '私有' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传人">{{ infoFile.createBy }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上传时间">{{ parseTime(infoFile.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ infoFile.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -298,12 +294,22 @@
|
||||
import { listFile, getFile, addFile, updateFile, delFile, exportFile, listVisibleUser, addVisibleUser, delVisibleUser, listVisibleUserByFileId, listRelatedToMe } from '@/api/system/file'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import UserSelect from '@/components/KLPService/UserSelect/index'
|
||||
import ImagePreview from '@/components/FilePreview/preview/image/index.vue'
|
||||
import PdfPreview from '@/components/FilePreview/preview/pdf/index.vue'
|
||||
import DocxPreview from '@/components/FilePreview/preview/docx/index.vue'
|
||||
import XlsxPreview from '@/components/FilePreview/preview/xlsx/index.vue'
|
||||
import XlsPreview from '@/components/FilePreview/preview/xls/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'SysFile',
|
||||
dicts: ['sys_file_type'],
|
||||
components: {
|
||||
UserSelect
|
||||
UserSelect,
|
||||
ImagePreview,
|
||||
PdfPreview,
|
||||
DocxPreview,
|
||||
XlsxPreview,
|
||||
XlsPreview
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -379,10 +385,32 @@ export default {
|
||||
Authorization: 'Bearer ' + getToken()
|
||||
},
|
||||
uploadFileList: [],
|
||||
// 预览
|
||||
previewVisible: false,
|
||||
previewTitle: '',
|
||||
previewFile: null
|
||||
// 选中的文件(右侧预览)
|
||||
selectedFile: null,
|
||||
// 文件详情弹窗
|
||||
infoVisible: false,
|
||||
infoTitle: '',
|
||||
infoFile: null,
|
||||
// 拖拽调节宽度
|
||||
leftPanelWidth: '40%',
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startLeftWidth: 60
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** 根据文件后缀分类,决定右侧用哪个预览组件 */
|
||||
fileTypeCategory() {
|
||||
if (!this.selectedFile) return null
|
||||
// 优先使用 suffix 字段,其次从 fileName 提取
|
||||
const raw = this.selectedFile.suffix || this.selectedFile.fileName || ''
|
||||
const ext = (raw.includes('.') ? raw.split('.').pop() : raw).toLowerCase()
|
||||
if (['png', 'jpg', 'jpeg', 'bmp', 'webp'].includes(ext)) return 'image'
|
||||
if (ext === 'pdf') return 'pdf'
|
||||
if (ext === 'docx') return 'docx'
|
||||
if (ext === 'xlsx') return 'xlsx'
|
||||
if (ext === 'xls') return 'xls'
|
||||
return 'other'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -492,6 +520,7 @@ export default {
|
||||
/** 重置按钮 */
|
||||
resetQuery() {
|
||||
this.dateRange = []
|
||||
this.selectedFile = null
|
||||
this.resetForm('queryForm')
|
||||
this.queryParams = {
|
||||
pageNum: 1,
|
||||
@@ -666,17 +695,54 @@ export default {
|
||||
...this.queryParams
|
||||
}, `file_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
/** 预览 */
|
||||
/** 点击文件名查看预览 */
|
||||
handlePreview(row) {
|
||||
this.previewFile = row
|
||||
this.previewTitle = '文件详情 - ' + row.fileName
|
||||
this.previewVisible = true
|
||||
this.selectedFile = row
|
||||
},
|
||||
/** 点击查看按钮弹出详情 */
|
||||
handleShowInfo(row) {
|
||||
this.infoFile = row
|
||||
this.infoTitle = '文件详情 - ' + row.fileName
|
||||
this.infoVisible = true
|
||||
},
|
||||
/** 点击行选中预览 */
|
||||
handleRowClick(row) {
|
||||
this.selectedFile = row
|
||||
},
|
||||
/** 下载文件 */
|
||||
downloadFile(row) {
|
||||
if (row.filePath) {
|
||||
window.open(row.filePath, '_blank')
|
||||
}
|
||||
},
|
||||
/** 开始拖拽 */
|
||||
startResize(e) {
|
||||
this.isDragging = true
|
||||
this.startX = e.clientX
|
||||
this.startLeftWidth = parseFloat(this.leftPanelWidth)
|
||||
document.addEventListener('mousemove', this.doResize)
|
||||
document.addEventListener('mouseup', this.stopResize)
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
},
|
||||
/** 拖拽中 */
|
||||
doResize(e) {
|
||||
if (!this.isDragging) return
|
||||
const container = this.$refs.layoutContainer
|
||||
const rect = container.getBoundingClientRect()
|
||||
const deltaX = e.clientX - this.startX
|
||||
let percent = this.startLeftWidth + (deltaX / rect.width) * 100
|
||||
percent = Math.max(30, Math.min(80, percent))
|
||||
this.leftPanelWidth = percent + '%'
|
||||
},
|
||||
/** 结束拖拽 */
|
||||
stopResize() {
|
||||
if (!this.isDragging) return
|
||||
this.isDragging = false
|
||||
document.removeEventListener('mousemove', this.doResize)
|
||||
document.removeEventListener('mouseup', this.stopResize)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -684,16 +750,16 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.stat-row {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border-left: 4px solid #409eff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.stat-card.public {
|
||||
@@ -705,23 +771,143 @@ export default {
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-size: 32px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.file-preview-container {
|
||||
padding: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 左右分栏布局 */
|
||||
.file-layout {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.file-left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-right {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
/* 拖拽分隔条 */
|
||||
.resize-handle {
|
||||
width: 6px;
|
||||
cursor: col-resize;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
margin: 0 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle.dragging {
|
||||
background: #409eff;
|
||||
}
|
||||
|
||||
.resize-handle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: #dcdfe6;
|
||||
border-radius: 1px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.resize-handle:hover::after,
|
||||
.resize-handle.dragging::after {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
height: calc(100vh - 280px);
|
||||
min-height: 450px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-filename {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.preview-content > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-not-supported {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 表格选中行高亮(非 scoped,穿透 el-table) -->
|
||||
<style>
|
||||
.file-left .el-table .current-row > td {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user