Files
klp-oa/klp-ui/src/views/system/file/index.vue
jhd 9ef387114d refactor(file): 优化文件管理页面布局和元数据显示
- 将文件列表表格包装在可滚动容器中,支持固定高度显示
- 新增选中文件元数据卡片组件,显示文件详细信息
- 调整左右分栏布局为拉伸对齐,优化高度计算
- 移除预览区域的文件元数据行,统一在卡片中显示
- 为共享文件和相关文件标签页添加独立的滚动样式
- 简化预览内容区域的flex属性配置
2026-07-06 09:51:28 +08:00

1152 lines
38 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="app-container">
<!-- 统计卡片 -->
<el-row :gutter="16" class="stat-row" v-if="activeTab === 'all'">
<el-col :span="6">
<div class="stat-card">
<div class="stat-num">{{ stats.totalFiles }}</div>
<div class="stat-label">文件总数</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card public">
<div class="stat-num">{{ stats.publicFiles }}</div>
<div class="stat-label">公开文件</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card private">
<div class="stat-num">{{ stats.privateFiles }}</div>
<div class="stat-label">私有文件</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-num">{{ stats.myFiles }}</div>
<div class="stat-label">我上传的</div>
</div>
</el-col>
</el-row>
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="我的文件" name="my"></el-tab-pane>
<el-tab-pane label="共享文件" name="share"></el-tab-pane>
<el-tab-pane label="与我相关" name="related"></el-tab-pane>
</el-tabs>
<!-- 左右分栏布局 -->
<div class="file-layout" ref="layoutContainer">
<!-- 左侧文件列表 -->
<div class="file-left" :style="{ width: activeTab === 'share' || activeTab === 'related' ? leftPanelWidth : '100%' }">
<!-- 搜索栏 -->
<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"
placeholder="请输入文件名称"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="订单编号" prop="orderNo">
<el-input
v-model="queryParams.orderNo"
placeholder="请输入订单编号"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="所属部门" prop="dept">
<el-input
v-model="queryParams.dept"
placeholder="请输入部门"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="文件类型" prop="fileType">
<el-select
v-model="queryParams.fileType"
placeholder="请选择文件类型"
clearable
style="width: 200px"
>
<el-option
v-for="item in dict.type.sys_file_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="可见范围" prop="scopeType" v-if="activeTab !== 'share' && activeTab !== 'related'">
<el-select
v-model="queryParams.scopeType"
placeholder="请选择可见范围"
clearable
style="width: 200px"
>
<el-option label="公开" :value="1" />
<el-option label="私有" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" v-if="activeTab === 'my' || activeTab === 'all'">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>上传文件</el-button>
</el-col>
<el-col :span="1.5" v-if="activeTab === 'my' || activeTab === 'all'">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 文件列表表格固定高度可滚动 -->
<div :class="['file-table-wrapper', { scrolling: activeTab === 'share' || activeTab === 'related' }]">
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" :highlight-current-row="activeTab === 'share' || activeTab === 'related'" @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" v-if="activeTab !== 'share' && activeTab !== 'related'">
<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" v-if="activeTab !== 'share' && activeTab !== 'related'">
<template slot-scope="scope">
{{ formatFileSize(scope.row.fileSize) }}
</template>
</el-table-column>
<el-table-column label="订单编号" align="center" prop="orderNo" :show-overflow-tooltip="true" v-if="activeTab !== 'share' && activeTab !== 'related'" />
<el-table-column label="所属部门" align="center" prop="dept" :show-overflow-tooltip="true" v-if="activeTab !== 'share' && activeTab !== 'related'" />
<el-table-column label="可见范围" align="center" prop="scopeType" v-if="activeTab !== 'share' && activeTab !== 'related'">
<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" width="90" v-if="activeTab === 'share' || activeTab === 'related'">
<template slot-scope="scope">
<span>{{ scope.row.viewCount != null ? scope.row.viewCount : 0 }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" v-if="activeTab !== 'share' && activeTab !== 'related'" />
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width" v-if="activeTab !== 'share' && activeTab !== 'related'">
<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
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 选中文件元数据卡片仅共享文件 / 与我相关 tab -->
<div class="file-meta-card" v-if="selectedFile && (activeTab === 'share' || activeTab === 'related')">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="文件名称">{{ selectedFile.fileName }}</el-descriptions-item>
<el-descriptions-item label="文件类型">
<dict-tag :options="dict.type.sys_file_type" :value="selectedFile.fileType"/>
</el-descriptions-item>
<el-descriptions-item label="文件大小">{{ formatFileSize(selectedFile.fileSize) }}</el-descriptions-item>
<el-descriptions-item label="文件后缀">{{ selectedFile.suffix || '-' }}</el-descriptions-item>
<el-descriptions-item label="浏览次数">{{ selectedFile.viewCount != null ? selectedFile.viewCount : 0 }} </el-descriptions-item>
<el-descriptions-item label="订单编号">{{ selectedFile.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="所属部门">{{ selectedFile.dept || '-' }}</el-descriptions-item>
<el-descriptions-item label="可见范围">
<el-tag :type="selectedFile.scopeType === 1 ? 'success' : 'warning'" size="small">
{{ selectedFile.scopeType === 1 ? '公开' : '私有' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="上传人">{{ selectedFile.createBy }}</el-descriptions-item>
<el-descriptions-item label="上传时间">{{ parseTime(selectedFile.createTime) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ selectedFile.remark || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
</div>
<!-- 拖拽分隔条共享文件 / 与我相关 tab -->
<div class="resize-handle" v-if="activeTab === 'share' || activeTab === 'related'" @mousedown="startResize" :class="{ dragging: isDragging }"></div>
<!-- 右侧文件预览共享文件 / 与我相关 tab -->
<div class="file-right" v-if="activeTab === 'share' || activeTab === 'related'">
<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="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-comment">
<div class="comment-bar" @click="commentExpanded = !commentExpanded">
<span><i class="el-icon-chat-dot-round"></i> 评论 ({{ comments.length }})</span>
<span class="comment-toggle">{{ commentExpanded ? '收起' : '展开' }} <i :class="commentExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i></span>
</div>
<div v-show="commentExpanded" class="comment-body">
<div v-if="commentLoading" class="comment-hint"><i class="el-icon-loading"></i> 加载中...</div>
<div v-else-if="comments.length === 0" class="comment-hint comment-empty">暂无评论</div>
<div v-else class="comment-list">
<div v-for="item in comments" :key="item.commentId" class="comment-item">
<div class="comment-meta">
<span class="comment-dept">{{ item.dept }}</span>
<span class="comment-user">{{ item.createBy }}</span>
<span class="comment-time">{{ parseTime(item.createTime) }}</span>
</div>
<div class="comment-text">{{ item.content }}</div>
</div>
</div>
<div class="comment-input-area">
<el-input
v-model="commentInput"
type="textarea"
:rows="2"
placeholder="输入评论..."
resize="none"
/>
<el-button type="primary" size="mini" @click="handleAddComment" :disabled="!commentInput.trim()" style="margin-top: 6px;">发送</el-button>
</div>
</div>
</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">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="form.fileName" placeholder="请输入文件名称" :disabled="isEdit" />
</el-form-item>
<el-form-item label="上传文件" v-if="!isEdit">
<el-upload
ref="upload"
:action="uploadUrl"
:headers="uploadHeaders"
:file-list="uploadFileList"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:auto-upload="false"
:limit="1"
class="upload-demo"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击选取</em></div>
</el-upload>
</el-form-item>
<el-form-item label="订单编号" prop="orderNo">
<el-input v-model="form.orderNo" placeholder="请输入订单编号" />
</el-form-item>
<el-form-item label="所属部门" prop="dept">
<el-input v-model="form.dept" placeholder="请输入所属部门" />
</el-form-item>
<el-form-item label="文件类型" prop="fileType">
<el-select v-model="form.fileType" placeholder="请选择文件类型" style="width: 100%">
<el-option
v-for="item in dict.type.sys_file_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="可见范围" prop="scopeType">
<el-radio-group v-model="form.scopeType" @change="handleScopeChange">
<el-radio :label="1">公开所有人可见</el-radio>
<el-radio :label="2">私有指定用户可见</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="可见用户" v-if="form.scopeType === 2">
<user-select
v-model="form.visibleUsers"
:multiple="true"
placeholder="请选择可见用户"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 文件预览对话框仅非共享文件 tab 使用 -->
<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>
</div>
</template>
<script>
import { listFile, getFile, addFile, updateFile, delFile, exportFile, listVisibleUser, addVisibleUser, delVisibleUser, listVisibleUserByFileId, listRelatedToMe, incrementView } from '@/api/system/file'
import { listFileComment, addFileComment } from '@/api/system/fileComment'
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,
ImagePreview,
PdfPreview,
DocxPreview,
XlsxPreview,
XlsPreview
},
data() {
return {
// 当前激活tab
activeTab: 'my',
// 遮罩层
loading: false,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 文件列表
fileList: [],
// 弹出层标题
dialogTitle: '',
// 是否显示弹出层
open: false,
// 是否编辑
isEdit: false,
// 日期范围
dateRange: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 20,
fileName: undefined,
orderNo: undefined,
dept: undefined,
fileType: undefined,
scopeType: undefined
},
// 表单
form: {
fileId: undefined,
fileName: undefined,
filePath: undefined,
fileSize: undefined,
suffix: undefined,
orderNo: undefined,
dept: undefined,
fileType: undefined,
scopeType: 1,
visibleUsers: [],
remark: undefined
},
// 可见用户原始数据(编辑时回显)
originalVisibleUsers: [],
// 表单校验
rules: {
fileName: [
{ required: true, message: '文件名称不能为空', trigger: 'blur' }
],
fileType: [
{ required: true, message: '请选择文件类型', trigger: 'change' }
]
},
// 统计
stats: {
totalFiles: 0,
publicFiles: 0,
privateFiles: 0,
myFiles: 0
},
// 上传相关
uploadUrl: process.env.VUE_APP_BASE_API + '/system/oss/upload',
uploadHeaders: {
Authorization: 'Bearer ' + getToken()
},
uploadFileList: [],
// 预览对话框(非共享文件 tab
previewVisible: false,
previewTitle: '',
previewFile: null,
// 选中的文件(共享文件 tab 右侧预览)
selectedFile: null,
// 评论
comments: [],
commentExpanded: false,
commentInput: '',
commentLoading: false,
// 拖拽调节宽度(共享文件 tab
leftPanelWidth: '40%',
isDragging: false,
startX: 0,
startLeftWidth: 60
}
},
computed: {
/** 根据文件后缀分类,决定右侧用哪个预览组件 */
fileTypeCategory() {
if (!this.selectedFile) return null
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() {
this.getList()
},
watch: {
selectedFile(val) {
if (val) {
this.loadComments()
} else {
this.comments = []
this.commentExpanded = false
}
}
},
methods: {
/** 切换tab */
handleTabClick(tab) {
this.resetQuery()
},
/** 查询文件列表 */
getList() {
this.loading = true
const params = this.addDateRange(this.queryParams, this.dateRange)
// 根据tab设置额外参数
if (this.activeTab === 'my') {
params.createBy = this.$store.getters.name
} else if (this.activeTab === 'share') {
params.scopeType = 1
}
// 根据tab选择不同的API
let request
if (this.activeTab === 'related') {
request = listRelatedToMe(params)
} else {
request = listFile(params)
}
request.then(response => {
this.fileList = response.rows
this.total = response.total
this.loading = false
// 加载统计数据
if (this.activeTab === 'all') {
this.loadStats()
}
}).catch(() => {
this.loading = false
})
},
/** 加载统计数据 */
loadStats() {
// 获取所有公开文件数量
listFile({ pageNum: 1, pageSize: 1, scopeType: 1 }).then(r => { this.stats.publicFiles = r.total })
// 获取所有私有文件数量
listFile({ pageNum: 1, pageSize: 1, scopeType: 2 }).then(r => { this.stats.privateFiles = r.total })
// 我上传的
listFile({ pageNum: 1, pageSize: 1, createBy: this.$store.getters.name }).then(r => { this.stats.myFiles = r.total })
// 总计
listFile({ pageNum: 1, pageSize: 1 }).then(r => { this.stats.totalFiles = r.total })
},
/** 格式化文件大小 */
formatFileSize(size) {
if (!size) return '0 B'
if (size < 1024) return size + ' B'
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
return (size / 1024 / 1024).toFixed(2) + ' MB'
},
/** 是否可编辑 */
canEdit(row) {
if (this.activeTab === 'related') return false
if (this.activeTab === 'share') return false
// 我的文件tab 或 所有文件tab中属于当前用户的
return row.createBy === this.$store.getters.name
},
/** 取消按钮 */
cancel() {
this.open = false
this.reset()
},
/** 对话框关闭回调 */
handleDialogClose() {
this.reset()
},
/** 表单重置 */
reset() {
this.form = {
fileId: undefined,
fileName: undefined,
filePath: undefined,
fileSize: undefined,
suffix: undefined,
orderNo: undefined,
dept: undefined,
fileType: undefined,
scopeType: 1,
visibleUsers: [],
remark: undefined
}
this.uploadFileList = []
this.originalVisibleUsers = []
this.isEdit = false
this.resetForm('form')
},
/** 可见范围切换 */
handleScopeChange(val) {
if (val === 1) {
this.form.visibleUsers = []
}
},
/** 搜索按钮 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮 */
resetQuery() {
this.dateRange = []
this.selectedFile = null
this.resetForm('queryForm')
this.queryParams = {
pageNum: 1,
pageSize: 20,
fileName: undefined,
orderNo: undefined,
dept: undefined,
fileType: undefined,
scopeType: undefined
}
this.handleQuery()
},
/** 多选框 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.fileId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增/上传 */
handleAdd() {
this.reset()
this.isEdit = false
this.open = true
this.dialogTitle = '上传文件'
},
/** 修改 */
handleUpdate(row) {
this.reset()
this.isEdit = true
const fileId = row.fileId || this.ids[0]
getFile(fileId).then(response => {
const data = response.data
this.form = {
fileId: data.fileId,
fileName: data.fileName,
filePath: data.filePath,
fileSize: data.fileSize,
suffix: data.suffix,
orderNo: data.orderNo,
dept: data.dept,
fileType: data.fileType,
scopeType: data.scopeType,
visibleUsers: [],
remark: data.remark
}
// 如果是私有文件,加载可见用户
if (data.scopeType === 2) {
listVisibleUserByFileId(fileId).then(res => {
const rows = res.rows || []
this.form.visibleUsers = rows.map(r => r.userId)
this.originalVisibleUsers = [...this.form.visibleUsers]
})
}
this.open = true
this.dialogTitle = '编辑文件'
})
},
/** 上传前校验 */
handleBeforeUpload(file) {
// 自动设置文件名
if (!this.form.fileName) {
this.form.fileName = file.name
}
const ext = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase()
this.form.suffix = ext
this.form.fileSize = file.size
},
/** 上传成功 */
handleUploadSuccess(response, file, fileList) {
if (response.code === 200) {
this.form.filePath = response.data.url
this.form.fileName = this.form.fileName || response.data.fileName
this.$modal.msgSuccess('文件上传成功')
// 自动提交表单
this.doSubmit()
} else {
this.$modal.msgError(response.msg || '上传失败')
}
},
/** 上传失败 */
handleUploadError(err, file, fileList) {
this.$modal.msgError('文件上传失败')
},
/** 提交表单 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.isEdit) {
// 编辑模式直接提交
this.doSubmit()
} else {
// 新增模式,先上传文件
if (this.$refs.upload && this.$refs.upload.uploadFiles.length > 0) {
this.$refs.upload.submit()
} else {
this.$modal.msgWarning('请选择要上传的文件')
}
}
}
})
},
/** 执行提交 */
doSubmit() {
const data = { ...this.form }
const visibleUsers = data.visibleUsers
delete data.visibleUsers
if (data.fileId) {
// 编辑模式
updateFile(data).then(response => {
this.saveVisibleUsers(data.fileId, visibleUsers)
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
}).catch(() => {})
} else {
// 新增模式
addFile(data).then(response => {
const newFileId = response.data
if (newFileId && visibleUsers && visibleUsers.length > 0) {
this.saveVisibleUsers(newFileId, visibleUsers)
}
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
}).catch(() => {})
}
},
/** 保存可见用户 */
saveVisibleUsers(fileId, visibleUsers) {
if (!visibleUsers || visibleUsers.length === 0) {
// 删除所有可见用户
if (this.originalVisibleUsers.length > 0) {
delVisibleUser(this.originalVisibleUsers.join(','))
}
return
}
// 新增的
const toAdd = visibleUsers.filter(u => !this.originalVisibleUsers.includes(u))
// 删除的
const toDel = this.originalVisibleUsers.filter(u => !visibleUsers.includes(u))
// 删除
if (toDel.length > 0) {
// 需要先查出visibleId
listVisibleUserByFileId(fileId).then(res => {
const rows = res.rows || []
const delIds = rows.filter(r => toDel.includes(r.userId)).map(r => r.visibleId)
if (delIds.length > 0) {
delVisibleUser(delIds.join(','))
}
})
}
// 新增
toAdd.forEach(userId => {
addVisibleUser({ fileId, userId })
})
},
/** 删除 */
handleDelete(row) {
const fileIds = row.fileId || this.ids.join(',')
this.$modal.confirm('是否确认删除所选文件?').then(() => {
return delFile(fileIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 导出 */
handleExport() {
this.download('system/file/export', {
...this.queryParams
}, `file_${new Date().getTime()}.xlsx`)
},
/** 预览 */
handlePreview(row) {
if (this.activeTab === 'share' || this.activeTab === 'related') {
this.selectedFile = row
incrementView(row.fileId)
} else {
this.previewFile = row
this.previewTitle = '文件详情 - ' + row.fileName
this.previewVisible = true
}
},
/** 点击行选中预览(共享文件 / 与我相关 tab */
handleRowClick(row) {
if (this.activeTab === 'share' || this.activeTab === 'related') {
this.selectedFile = row
}
},
/** 查看文件详情 */
handleShowInfo(row) {
this.previewFile = row
this.previewTitle = '文件详情 - ' + row.fileName
this.previewVisible = true
},
/** 下载文件 */
downloadFile(row) {
if (row.filePath) {
window.open(row.filePath, '_blank')
}
},
/** 加载评论列表 */
loadComments() {
this.commentLoading = true
listFileComment(this.selectedFile.fileId).then(res => {
this.comments = res.data || []
this.commentLoading = false
}).catch(() => {
this.commentLoading = false
})
},
/** 发送评论 */
handleAddComment() {
const content = this.commentInput.trim()
if (!content) return
addFileComment({ fileId: this.selectedFile.fileId, content }).then(() => {
this.commentInput = ''
this.loadComments()
})
},
/** 开始拖拽 */
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 = ''
}
}
}
</script>
<style scoped>
.stat-row {
margin-bottom: 16px;
}
.stat-card {
background: #fff;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-left: 4px solid #409eff;
}
.stat-card.public {
border-left-color: #67c23a;
}
.stat-card.private {
border-left-color: #e6a23c;
}
.stat-num {
font-size: 32px;
font-weight: 700;
color: #303133;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 8px;
}
.file-preview-container {
padding: 8px;
}
.upload-demo {
width: 100%;
}
/* 左右分栏布局 */
.file-layout {
display: flex;
align-items: stretch;
gap: 0;
}
.file-left {
flex-shrink: 0;
display: flex;
flex-direction: column;
height: calc(100vh - 240px);
}
.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: 100%;
min-height: calc(100vh - 300px);
display: flex;
flex-direction: column;
}
.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-comment {
border-bottom: 1px solid #ebeef5;
}
.comment-bar {
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-size: 13px;
color: #606266;
user-select: none;
}
.comment-bar:hover {
background: #f5f7fa;
}
.comment-toggle {
color: #909399;
font-size: 12px;
}
.comment-body {
max-height: 260px;
overflow-y: auto;
}
.comment-hint {
padding: 16px;
text-align: center;
color: #909399;
font-size: 13px;
}
.comment-list {
padding: 4px 0;
}
.comment-item {
padding: 8px 16px;
border-bottom: 1px solid #f2f2f2;
}
.comment-item:last-child {
border-bottom: none;
}
.comment-meta {
display: flex;
gap: 8px;
align-items: center;
font-size: 12px;
margin-bottom: 4px;
}
.comment-dept {
color: #409eff;
font-weight: 500;
}
.comment-user {
color: #606266;
}
.comment-time {
color: #c0c4cc;
}
.comment-text {
font-size: 13px;
color: #303133;
line-height: 1.5;
word-break: break-all;
}
.comment-input-area {
padding: 8px 16px;
border-top: 1px solid #f2f2f2;
}
.preview-content {
overflow: auto;
display: flex;
}
.preview-content > div {
width: 100%;
}
.preview-not-supported {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
/* 左侧表格区域撑满剩余空间可滚动(仅 share / related tab */
.file-table-wrapper.scrolling {
flex: 1;
overflow-y: auto;
}
/* 左侧文件元数据卡片 */
.file-meta-card {
margin-top: 12px;
}
</style>
<!-- 表格选中行高亮 scoped穿透 el-table -->
<style>
.file-left .el-table .current-row > td {
background-color: #ecf5ff !important;
}
</style>