refactor(system): 重构文件管理页面为卡片视图

- 将文件列表从表格布局改为响应式卡片网格布局
- 添加文件类型图标和颜色分类,提升视觉识别效果
- 重构页面头部结构,整合统计数据、筛选标签和工具栏
- 实现卡片多选功能和批量操作支持
- 优化文件详情弹窗布局,调整预览和元数据区域
- 更新样式系统,统一间距和交互反馈效果
- 移除高级搜索切换功能,简化查询界面
- 添加文件扩展名获取和图标分类辅助方法
This commit is contained in:
jhd
2026-07-10 10:22:48 +08:00
parent bf1ac5ed2e
commit 4ad0261622

View File

@@ -1,98 +1,78 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 左右分栏布局 --> <!-- 顶部统计卡片 + 工具栏 + 筛选标签 -->
<div class="file-layout"> <div class="page-header">
<!-- 左侧边栏 --> <div class="stats-area">
<div class="file-sidebar"> <el-card class="stats-card" shadow="hover" :body-style="{ padding: '0' }">
<!-- 统计卡片 --> <div class="stats-grid">
<el-card class="summary-card" shadow="hover" :body-style="{ padding: '10px' }"> <div class="stats-item">
<el-row :gutter="6"> <span class="stats-label">文件总数</span>
<el-col :span="12"> <span class="stats-value">{{ stats.totalFiles }}</span>
<div class="summary-item"> </div>
<span class="item-label">文件总数</span> <div class="stats-item">
<span class="item-value">{{ stats.totalFiles }}</span> <span class="stats-label">公开文件</span>
</div> <span class="stats-value">{{ stats.publicFiles }}</span>
</el-col> </div>
<el-col :span="12"> <div class="stats-item">
<div class="summary-item"> <span class="stats-label">私有文件</span>
<span class="item-label">公开文件</span> <span class="stats-value">{{ stats.privateFiles }}</span>
<span class="item-value">{{ stats.publicFiles }}</span> </div>
</div> <div class="stats-item">
</el-col> <span class="stats-label">我上传的</span>
</el-row> <span class="stats-value">{{ stats.myFiles }}</span>
<el-row :gutter="6" style="margin-top: 4px;"> </div>
<el-col :span="12"> </div>
<div class="summary-item">
<span class="item-label">私有文件</span>
<span class="item-value">{{ stats.privateFiles }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="summary-item">
<span class="item-label">我上传的</span>
<span class="item-value">{{ stats.myFiles }}</span>
</div>
</el-col>
</el-row>
</el-card> </el-card>
</div>
<!-- 搜索框 --> <div class="header-main">
<div class="sidebar-section"> <div class="filter-row">
<div class="type-tags">
<span
v-for="opt in fileTypeOptions"
:key="opt.value"
class="type-tag"
:class="{ active: (queryParams.fileType || '') === opt.value }"
@click="handleTypeTabClick(opt.value)"
>{{ opt.label }}</span>
</div>
<div class="filter-extra">
<el-select v-model="queryParams.scopeType" placeholder="可见范围" clearable size="mini" @change="handleQuery" style="width:110px;">
<el-option label="公开" :value="1" />
<el-option label="私有" :value="2" />
</el-select>
</div>
</div>
<div class="toolbar-row">
<el-input <el-input
v-model="queryParams.fileName" v-model="queryParams.fileName"
prefix-icon="el-icon-search" prefix-icon="el-icon-search"
placeholder="按名称搜索" placeholder="按名称搜索"
clearable clearable
size="mini" size="mini"
style="width:200px;"
@change="handleQuery" @change="handleQuery"
@clear="handleQuery" @clear="handleQuery"
></el-input> />
</div>
<!-- 文件类型列表 -->
<div class="sidebar-section sidebar-file-types">
<div class="sidebar-label">文件类型</div>
<div class="file-type-list">
<div
v-for="opt in fileTypeOptions"
:key="opt.value"
class="type-list-item"
:class="{ active: (queryParams.fileType || '') === opt.value }"
@click="handleTypeTabClick(opt.value)"
>{{ opt.label }}</div>
</div>
</div>
</div>
<!-- 右侧主区域 -->
<div class="file-main">
<!-- 高级搜索 + 操作按钮 -->
<div class="main-toolbar">
<el-button icon="el-icon-sort" size="mini" @click="toggleQuery">高级搜索</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">上传文件</el-button> <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">上传文件</el-button>
<el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button> <el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
<span v-if="selectedCardIds.length > 0" class="selected-count">已选 {{ selectedCardIds.length }} </span>
</div> </div>
<div v-show="showQuery" class="advanced-search"> <div class="advanced-search">
<el-form :model="queryParams" inline size="mini" class="advanced-search-form"> <el-form :model="queryParams" inline size="mini" class="advanced-search-form">
<el-form-item label="订单编号"> <el-form-item label="订单编号">
<el-input v-model="queryParams.orderNo" placeholder="订单编号" clearable @change="handleQuery" style="width:140px;"></el-input> <el-input v-model="queryParams.orderNo" placeholder="订单编号" clearable @change="handleQuery" style="width:140px;" />
</el-form-item> </el-form-item>
<el-form-item label="所属部门"> <el-form-item label="所属部门">
<el-input v-model="queryParams.dept" placeholder="所属部门" clearable @change="handleQuery" style="width:140px;"></el-input> <el-input v-model="queryParams.dept" placeholder="所属部门" clearable @change="handleQuery" style="width:140px;" />
</el-form-item> </el-form-item>
<el-form-item label="文件类型"> <el-form-item label="文件类型">
<el-select v-model="queryParams.fileType" placeholder="文件类型" clearable @change="handleQuery" style="width:120px;"> <el-select v-model="queryParams.fileType" placeholder="文件类型" clearable @change="handleQuery" style="width:120px;">
<el-option v-for="item in dict.type.sys_file_type" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in dict.type.sys_file_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="可见范围">
<el-select v-model="queryParams.scopeType" placeholder="可见范围" clearable @change="handleQuery" style="width:120px;">
<el-option label="公开" :value="1" />
<el-option label="私有" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="上传时间"> <el-form-item label="上传时间">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="dateRange"
@@ -105,71 +85,56 @@
clearable clearable
style="width:240px;" style="width:240px;"
@change="handleQuery" @change="handleQuery"
></el-date-picker> />
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<!-- 文件表格 -->
<div class="file-table-wrapper">
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" highlight-current-row @row-click="handleRowClick">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="文件名称" align="center" prop="fileName" :show-overflow-tooltip="true" min-width="160">
<template slot-scope="scope">
<el-link type="primary" @click="handleShowInfo(scope.row)">{{ scope.row.fileName }}</el-link>
</template>
</el-table-column>
<el-table-column label="上传人" align="center" prop="createByName" width="120" :show-overflow-tooltip="true" />
<el-table-column label="所属部门" align="center" prop="dept" width="120" :show-overflow-tooltip="true" />
<el-table-column label="订单编号" align="center" prop="orderNo" width="130" :show-overflow-tooltip="true" />
<el-table-column label="文件类型" align="center" width="100">
<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" width="90">
<template slot-scope="scope">
<span>{{ formatFileSize(scope.row.fileSize) }}</span>
</template>
</el-table-column>
<el-table-column label="可见范围" align="center" width="80">
<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" width="155">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="浏览次数" align="center" width="75">
<template slot-scope="scope">
<span>{{ scope.row.viewCount != null ? scope.row.viewCount : 0 }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="140" fixed="right">
<template slot-scope="scope">
<div class="action-cell">
<el-button type="text" size="mini" icon="el-icon-view" @click.stop="handleShowInfo(scope.row)">详情</el-button>
<el-button v-if="canEdit(scope.row)" type="text" size="mini" icon="el-icon-edit" @click.stop="handleUpdate(scope.row)">编辑</el-button>
</div>
</template>
</el-table-column>
</KLPTable>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div> </div>
</div> </div>
<!-- 文件卡片网格 -->
<div class="card-container" v-loading="loading">
<div v-if="fileList.length > 0" class="card-grid">
<div
v-for="file in fileList"
:key="file.fileId"
class="file-card"
:class="{ selected: isSelected(file.fileId) }"
@click="handleCardClick(file)"
>
<div class="card-checkbox" @click.stop="toggleSelect(file)">
<i v-if="isSelected(file.fileId)" class="el-icon-check"></i>
</div>
<div class="card-icon" :class="getFileIconClass(file)">
<span class="card-ext">{{ getFileExt(file) }}</span>
</div>
<div class="card-body">
<div class="card-name" :title="file.fileName">{{ file.fileName }}</div>
<div class="card-uploader">{{ file.createByName || file.createBy }}</div>
<div class="card-meta">
<span>{{ formatFileSize(file.fileSize) }}</span>
<span class="meta-sep">·</span>
<span>{{ parseTime(file.createTime, '{mm}-{dd}') }}</span>
</div>
</div>
<div class="card-actions">
<el-button type="text" size="mini" icon="el-icon-view" @click.stop="handleShowInfo(file)">详情</el-button>
<el-button type="text" size="mini" icon="el-icon-download" @click.stop="downloadFile(file)">下载</el-button>
</div>
</div>
</div>
<el-empty v-else-if="!loading" description="暂无文件" />
</div>
<!-- 分页 -->
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 上传/编辑对话框 --> <!-- 上传/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="open" width="650px" append-to-body @close="handleDialogClose"> <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 ref="form" :model="form" :rules="rules" label-width="100px">
@@ -225,66 +190,68 @@
</el-dialog> </el-dialog>
<!-- 文件详情弹窗含预览 --> <!-- 文件详情弹窗含预览 -->
<el-dialog :title="infoTitle" :visible.sync="infoVisible" width="1100px" append-to-body top="5vh" @close="handleInfoClose"> <el-dialog :title="infoTitle" :visible.sync="infoVisible" width="1200px" append-to-body top="3vh" @close="handleInfoClose">
<div v-if="infoFile" class="info-dialog-body"> <div v-if="infoFile" class="info-dialog-body">
<div class="info-preview"> <div class="info-main-row">
<ImagePreview v-if="infoFileCategory === 'image'" :src="infoFile.filePath" /> <div class="info-preview">
<PdfPreview v-else-if="infoFileCategory === 'pdf'" :src="infoFile.filePath" /> <ImagePreview v-if="infoFileCategory === 'image'" :src="infoFile.filePath" />
<DocxPreview v-else-if="infoFileCategory === 'docx'" :src="infoFile.filePath" /> <PdfPreview v-else-if="infoFileCategory === 'pdf'" :src="infoFile.filePath" />
<XlsxPreview v-else-if="infoFileCategory === 'xlsx'" :src="infoFile.filePath" /> <DocxPreview v-else-if="infoFileCategory === 'docx'" :src="infoFile.filePath" />
<XlsPreview v-else-if="infoFileCategory === 'xls'" :src="infoFile.filePath" /> <XlsxPreview v-else-if="infoFileCategory === 'xlsx'" :src="infoFile.filePath" />
<div v-else class="info-preview-empty"> <XlsPreview v-else-if="infoFileCategory === 'xls'" :src="infoFile.filePath" />
<el-empty description="暂不支持预览此文件类型" /> <div v-else class="info-preview-empty">
<el-empty description="暂不支持预览此文件类型" />
</div>
</div>
<div class="info-meta">
<el-descriptions :column="1" border size="small">
<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.viewCount != null ? infoFile.viewCount : 0 }} </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.createByName || infoFile.createBy }}</el-descriptions-item>
<el-descriptions-item label="上传时间">{{ parseTime(infoFile.createTime) }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ infoFile.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<div style="margin-top:12px;text-align:right;">
<el-button type="warning" plain size="mini" icon="el-icon-download" @click="handleExportInfo">导出</el-button>
<el-button type="primary" size="mini" icon="el-icon-download" @click="downloadFile(infoFile)">下载</el-button>
</div>
</div> </div>
</div> </div>
<div class="info-meta"> <!-- 评论区域单独放在下面 -->
<el-descriptions :column="1" border size="small"> <div class="info-comment">
<el-descriptions-item label="文件名称">{{ infoFile.fileName }}</el-descriptions-item> <div class="comment-bar" @click="commentExpanded = !commentExpanded">
<el-descriptions-item label="文件类型"> <span><i class="el-icon-chat-dot-round"></i> 评论 ({{ comments.length }})</span>
<dict-tag :options="dict.type.sys_file_type" :value="infoFile.fileType"/> <span class="comment-toggle">{{ commentExpanded ? '收起' : '展开' }} <i :class="commentExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i></span>
</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.viewCount != null ? infoFile.viewCount : 0 }} </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.createByName || infoFile.createBy }}</el-descriptions-item>
<el-descriptions-item label="上传时间">{{ parseTime(infoFile.createTime) }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ infoFile.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 评论区域 -->
<div class="info-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>
<div style="margin-top:12px;text-align:right;"> <div v-show="commentExpanded" class="comment-body">
<el-button type="warning" plain size="mini" icon="el-icon-download" @click="handleExportInfo">导出</el-button> <div v-if="commentLoading" class="comment-hint"><i class="el-icon-loading"></i> 加载中...</div>
<el-button type="primary" size="mini" icon="el-icon-download" @click="downloadFile(infoFile)">下载</el-button> <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> </div>
</div> </div>
@@ -320,12 +287,11 @@ export default {
loading: false, loading: false,
// 选中数组 // 选中数组
ids: [], ids: [],
selectedCardIds: [],
// 非单个禁用 // 非单个禁用
single: true, single: true,
// 非多个禁用 // 非多个禁用
multiple: true, multiple: true,
// 显示高级搜索
showQuery: false,
// 总条数 // 总条数
total: 0, total: 0,
// 文件列表 // 文件列表
@@ -433,6 +399,9 @@ export default {
/** 查询文件列表 */ /** 查询文件列表 */
getList() { getList() {
this.loading = true this.loading = true
this.selectedCardIds = []
this.ids = []
this.multiple = true
const params = this.addDateRange(this.queryParams, this.dateRange) const params = this.addDateRange(this.queryParams, this.dateRange)
listFile(params).then(response => { listFile(params).then(response => {
this.fileList = response.rows this.fileList = response.rows
@@ -450,17 +419,57 @@ export default {
listFile({ pageNum: 1, pageSize: 1, createBy: this.$store.getters.name }).then(r => { this.stats.myFiles = 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 }) listFile({ pageNum: 1, pageSize: 1 }).then(r => { this.stats.totalFiles = r.total })
}, },
/** 获取文件扩展名(大写,用于图标显示) */
getFileExt(file) {
const raw = file.suffix || file.fileName || ''
const ext = raw.includes('.') ? raw.split('.').pop() : raw
return ext.slice(0, 4).toUpperCase()
},
/** 文件图标颜色类 */
getFileIconClass(file) {
const ext = (file.suffix || file.fileName || '').toLowerCase()
if (['doc', 'docx'].includes(ext)) return 'icon-word'
if (['xls', 'xlsx'].includes(ext)) return 'icon-excel'
if (['ppt', 'pptx'].includes(ext)) return 'icon-ppt'
if (ext === 'pdf') return 'icon-pdf'
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext)) return 'icon-image'
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) return 'icon-archive'
if (['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv'].includes(ext)) return 'icon-video'
if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(ext)) return 'icon-audio'
if (['txt', 'log', 'md'].includes(ext)) return 'icon-text'
return 'icon-other'
},
/** 格式化文件大小 */ /** 格式化文件大小 */
formatFileSize(size) { formatFileSize(size) {
if (!size) return '0 B' if (!size) return '0 B'
if (size < 1024) return size + ' B' if (size < 1024) return size + ' B'
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB' if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB'
return (size / 1024 / 1024).toFixed(2) + ' MB' return (size / 1024 / 1024).toFixed(1) + ' MB'
}, },
/** 是否可编辑 */ /** 是否可编辑 */
canEdit(row) { canEdit(row) {
return row.createBy === this.$store.getters.name return row.createBy === this.$store.getters.name
}, },
/** 卡片选中判断 */
isSelected(fileId) {
return this.selectedCardIds.includes(fileId)
},
/** 切换卡片选中 */
toggleSelect(file) {
const idx = this.selectedCardIds.indexOf(file.fileId)
if (idx > -1) {
this.selectedCardIds.splice(idx, 1)
} else {
this.selectedCardIds.push(file.fileId)
}
this.ids = [...this.selectedCardIds]
this.single = this.selectedCardIds.length !== 1
this.multiple = this.selectedCardIds.length === 0
},
/** 点击卡片(打开详情) */
handleCardClick(file) {
this.handleShowInfo(file)
},
/** 取消按钮 */ /** 取消按钮 */
cancel() { cancel() {
this.open = false this.open = false
@@ -501,10 +510,6 @@ export default {
this.queryParams.pageNum = 1 this.queryParams.pageNum = 1
this.getList() this.getList()
}, },
/** 切换高级搜索 */
toggleQuery() {
this.showQuery = !this.showQuery
},
/** 文件类型筛选切换 */ /** 文件类型筛选切换 */
handleTypeTabClick(value) { handleTypeTabClick(value) {
this.queryParams.fileType = value || undefined this.queryParams.fileType = value || undefined
@@ -525,7 +530,7 @@ export default {
} }
this.getList() this.getList()
}, },
/** 多选框 */ /** 多选框(兼容保留) */
handleSelectionChange(selection) { handleSelectionChange(selection) {
this.ids = selection.map(item => item.fileId) this.ids = selection.map(item => item.fileId)
this.single = selection.length !== 1 this.single = selection.length !== 1
@@ -719,126 +724,128 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* 左右分栏 */ /* ========== 页面顶部:统计 + 工具栏 + 筛选标签 ========== */
.file-layout { .page-header {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
gap: 0; gap: 12px;
height: calc(100vh - 100px); margin-bottom: 16px;
} }
/* 左侧边栏 */ /* 统计卡片区域 */
.file-sidebar { .stats-area {
width: 220px; width: 220px;
flex-shrink: 0; flex-shrink: 0;
display: flex;
flex-direction: column;
padding-right: 12px;
overflow-y: auto;
} }
.sidebar-section { .stats-card {
margin-bottom: 12px; height: 100%;
}
.sidebar-label {
font-size: 13px;
font-weight: 500;
color: #606266;
margin-bottom: 6px;
padding-left: 2px;
}
/* 文件类型列表(纵向) */
.sidebar-file-types {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.file-type-list {
flex: 1;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.type-list-item {
padding: 8px 12px;
font-size: 13px;
color: #606266;
cursor: pointer;
user-select: none;
border-bottom: 1px solid #f2f2f2;
transition: all 0.15s;
}
.type-list-item:last-child {
border-bottom: none;
}
.type-list-item:hover {
color: #5F7BA0;
background: #f5f7fa;
}
.type-list-item.active {
color: #fff;
background: #5F7BA0;
font-weight: 500;
}
/* 操作列按钮对齐 */
.action-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
white-space: nowrap;
}
/* 统计卡片 */
.summary-card {
border: none; border: none;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
} }
.summary-item { .stats-card /deep/ .el-card__body {
height: 100%;
display: flex;
align-items: center;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
width: 100%;
padding: 4px;
}
.stats-item {
text-align: center; text-align: center;
padding: 6px 4px; padding: 2px 4px;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
border-radius: 2px; border-radius: 4px;
} }
.item-label { .stats-label {
display: block; display: block;
font-size: 12px; font-size: 11px;
color: #909399; color: #909399;
margin-bottom: 2px; line-height: 1.4;
} }
.item-value { .stats-value {
display: block; display: block;
font-size: 20px; font-size: 18px;
font-weight: bold; font-weight: 700;
color: #1f2d3d; color: #1f2d3d;
line-height: 1.2; line-height: 1.3;
} }
/* 右侧主区域 */ /* 右侧主区域 */
.file-main { .header-main {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0;
min-width: 0; min-width: 0;
} }
.main-toolbar { /* 工具栏行 */
.toolbar-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 8px; padding: 2px 0;
}
.selected-count {
font-size: 12px;
color: #409eff;
margin-left: 4px;
}
/* 筛选标签行 */
.filter-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.type-tags {
display: flex;
align-items: center;
gap: 2px;
flex-wrap: wrap;
}
.type-tag {
padding: 3px 14px;
font-size: 12px;
border-radius: 14px;
cursor: pointer;
color: #606266;
background: #f5f7fa;
border: 1px solid #e4e7ed;
transition: all 0.2s;
user-select: none;
line-height: 22px;
}
.type-tag:hover {
color: #5F7BA0;
border-color: #5F7BA0;
background: #f0f4f8;
}
.type-tag.active {
color: #fff;
background: #5F7BA0;
border-color: #5F7BA0;
font-weight: 500;
}
.filter-extra {
flex-shrink: 0;
} }
/* 高级搜索 */ /* 高级搜索 */
@@ -847,7 +854,7 @@ export default {
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
border-radius: 4px; border-radius: 4px;
padding: 10px 16px; padding: 10px 16px;
margin-bottom: 8px; margin-top: 8px;
} }
.advanced-search-form { .advanced-search-form {
@@ -860,27 +867,191 @@ export default {
margin-bottom: 0; margin-bottom: 0;
} }
/* 表格区域 */ /* ========== 文件卡片网格 ========== */
.file-table-wrapper { .card-container {
flex: 1; min-height: 300px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
gap: 14px;
}
.file-card {
position: relative;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 20px 14px 14px;
cursor: pointer;
transition: all 0.25s;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid #ebeef5; align-items: center;
border-radius: 4px;
overflow: hidden;
} }
.file-table-wrapper .KLPTable { .file-card:hover {
flex: 1; border-color: #c0c4cc;
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
} }
/* 上传区域 */ .file-card.selected {
.upload-demo { border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15), 0 4px 16px 0 rgba(0, 0, 0, 0.06);
}
/* 卡片选中复选框 */
.card-checkbox {
position: absolute;
top: 6px;
right: 6px;
width: 20px;
height: 20px;
border: 1px solid #d0d5dd;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
opacity: 0;
transition: opacity 0.2s;
z-index: 2;
}
.file-card:hover .card-checkbox {
opacity: 1;
}
.file-card.selected .card-checkbox {
opacity: 1;
background: #409eff;
border-color: #409eff;
}
.card-checkbox .el-icon-check {
font-size: 12px;
color: #fff;
font-weight: 700;
}
/* 文件类型图标 */
.card-icon {
width: 64px;
height: 64px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
flex-shrink: 0;
}
.card-ext {
font-size: 13px;
font-weight: 700;
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* 各文件类型颜色梯度 */
.icon-word {
background: linear-gradient(135deg, #4a90d9, #357abd);
}
.icon-excel {
background: linear-gradient(135deg, #52b86a, #3d9e55);
}
.icon-ppt {
background: linear-gradient(135deg, #f09b4a, #d9822e);
}
.icon-pdf {
background: linear-gradient(135deg, #e06060, #cc4444);
}
.icon-image {
background: linear-gradient(135deg, #a877d9, #8e5fc4);
}
.icon-archive {
background: linear-gradient(135deg, #8a9bb0, #6d7e93);
}
.icon-video {
background: linear-gradient(135deg, #4db8a8, #389e8e);
}
.icon-audio {
background: linear-gradient(135deg, #e8924a, #d47d35);
}
.icon-text {
background: linear-gradient(135deg, #90a4ae, #78909c);
}
.icon-other {
background: linear-gradient(135deg, #b0bec5, #90a4ae);
}
/* 卡片内容 */
.card-body {
text-align: center;
width: 100%; width: 100%;
min-width: 0;
} }
/* 详情弹窗主体 */ .card-name {
font-size: 13px;
color: #303133;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
}
.card-uploader {
font-size: 11px;
color: #aaa;
margin-top: 2px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-meta {
font-size: 11px;
color: #909399;
margin-top: 4px;
line-height: 1.4;
}
.meta-sep {
margin: 0 4px;
}
/* 卡片悬停操作按钮 */
.card-actions {
margin-top: 10px;
display: flex;
gap: 6px;
opacity: 0;
transition: opacity 0.2s;
}
.file-card:hover .card-actions {
opacity: 1;
}
.card-actions .el-button {
padding: 2px 8px;
font-size: 12px;
}
/* ========== 详情弹窗 ========== */
.info-dialog-body { .info-dialog-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-main-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
min-height: 400px; min-height: 400px;
@@ -908,14 +1079,13 @@ export default {
} }
.info-meta { .info-meta {
width: 320px; width: 340px;
flex-shrink: 0; flex-shrink: 0;
overflow-y: auto; overflow-y: auto;
} }
/* 详情弹窗评论区 */ /* 评论区 */
.info-comment { .info-comment {
margin-top: 12px;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
border-radius: 4px; border-radius: 4px;
} }
@@ -999,10 +1169,3 @@ export default {
border-top: 1px solid #f2f2f2; border-top: 1px solid #f2f2f2;
} }
</style> </style>
<!-- 表格选中行高亮 -->
<style>
.file-main .el-table .current-row > td {
background-color: #ecf5ff !important;
}
</style>