674 lines
19 KiB
Vue
674 lines
19 KiB
Vue
<template>
|
||
<el-card class="box-card">
|
||
<div class="header-bar">
|
||
<h2>文件管理</h2>
|
||
<!-- <el-button type="primary" @click="showProgramDialog = true">
|
||
<el-icon><Plus /></el-icon>
|
||
添加程序
|
||
</el-button> -->
|
||
</div>
|
||
|
||
<div v-if="loading" class="loading-state">
|
||
<div class="loading-spinner"></div>
|
||
<p>正在加载程序数据...</p>
|
||
</div>
|
||
|
||
<!-- 错误状态 -->
|
||
<div v-else-if="error" class="error-state">
|
||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||
<el-button type="primary" @click="fetchPrograms">重试</el-button>
|
||
</div>
|
||
|
||
<div v-else class="files-module fancy-bg">
|
||
<!-- 顶部功能卡片区域 -->
|
||
<div class="function-cards">
|
||
<el-row :gutter="24">
|
||
<el-col :xs="24" :sm="12" :lg="8">
|
||
<el-card
|
||
class="function-card card-gradient"
|
||
shadow="hover"
|
||
@click="$router.push('/files/list')"
|
||
>
|
||
<div class="card-content">
|
||
<div class="card-icon">
|
||
<i class="fa-solid fa-folder-open"></i>
|
||
</div>
|
||
<div class="card-info">
|
||
<span class="card-title">文件列表</span>
|
||
<span class="card-desc">查看和管理所有文件</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :lg="8">
|
||
<el-card
|
||
class="function-card card-gradient"
|
||
shadow="hover"
|
||
@click="$router.push('/files/upload')"
|
||
>
|
||
<div class="card-content">
|
||
<div class="card-icon upload">
|
||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||
</div>
|
||
<div class="card-info">
|
||
<span class="card-title">批量上传</span>
|
||
<span class="card-desc">批量上传文件至系统</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :lg="8">
|
||
<el-card
|
||
class="function-card card-gradient"
|
||
shadow="hover"
|
||
@click="$router.push('/files/categories')"
|
||
>
|
||
<div class="card-content">
|
||
<div class="card-icon tags">
|
||
<i class="fa-solid fa-layer-group"></i>
|
||
</div>
|
||
<div class="card-info">
|
||
<span class="card-title">分类管理</span>
|
||
<span class="card-desc">管理分类/标签</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 搜索和筛选 -->
|
||
<div class="search-filter stylish-panel">
|
||
<el-input
|
||
v-model="searchKeyword"
|
||
placeholder="搜索文件名、原始文件名或分类"
|
||
clearable
|
||
@input="handleSearch"
|
||
size="large"
|
||
class="input-search"
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
|
||
<el-select
|
||
v-model="filterCategory"
|
||
placeholder="全部分类"
|
||
clearable
|
||
@change="handleFilter"
|
||
size="large"
|
||
class="select-category"
|
||
>
|
||
<el-option
|
||
v-for="category in categories"
|
||
:key="category"
|
||
:label="category"
|
||
:value="category"
|
||
/>
|
||
</el-select>
|
||
|
||
<el-switch
|
||
v-model="showMyFiles"
|
||
class="switch-mine"
|
||
active-text="仅看我的文件"
|
||
inactive-text="全部文件"
|
||
@change="handleFilter"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 文件数据列表 -->
|
||
<div class="file-list stylish-panel">
|
||
<el-table
|
||
:data="filteredFiles"
|
||
v-loading="loading"
|
||
highlight-current-row
|
||
border
|
||
:default-sort="{ prop: 'upload_time', order: 'descending' }"
|
||
>
|
||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||
<el-table-column prop="file_name" label="文件名" min-width="200">
|
||
<template #default="{ row }">
|
||
<div class="file-name">
|
||
<el-icon class="icon-doc"><Document /></el-icon>
|
||
<span>{{ row.file_name }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="original_name"
|
||
label="原始文件名"
|
||
min-width="180"
|
||
/>
|
||
<el-table-column prop="category" label="分类" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag effect="dark" type="success">{{ row.category }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="file_size" label="大小" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag effect="plain">{{
|
||
formatFileSize(row.file_size)
|
||
}}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="file_type" label="类型" width="90" />
|
||
<el-table-column prop="upload_by" label="上传人" width="120" />
|
||
<el-table-column
|
||
prop="upload_time"
|
||
label="上传时间"
|
||
width="170"
|
||
sortable
|
||
>
|
||
<template #default="{ row }">
|
||
<span style="color: #8ea1d6">{{
|
||
formatDate(row.upload_time)
|
||
}}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="操作"
|
||
width="170"
|
||
fixed="right"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip content="查看详情">
|
||
<el-button
|
||
type="primary"
|
||
circle
|
||
plain
|
||
size="small"
|
||
@click="viewFile(row)"
|
||
>
|
||
<el-icon><View /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
<el-tooltip content="删除">
|
||
<el-button
|
||
circle
|
||
type="danger"
|
||
plain
|
||
size="small"
|
||
@click="deleteFile(row)"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 分页区域 -->
|
||
<div class="pagination">
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="totalFiles"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 上传文件对话框 -->
|
||
<el-dialog
|
||
v-model="showUploadDialog"
|
||
title="上传文件"
|
||
width="500px"
|
||
:before-close="handleUploadClose"
|
||
class="upload-dialog"
|
||
>
|
||
<el-upload
|
||
ref="uploadRef"
|
||
class="upload-demo"
|
||
drag
|
||
:action="uploadUrl"
|
||
:headers="uploadHeaders"
|
||
:data="{ category: uploadForm.category }"
|
||
:on-success="handleUploadSuccess"
|
||
:on-error="handleUploadError"
|
||
:before-upload="beforeUpload"
|
||
:show-file-list="false"
|
||
multiple
|
||
>
|
||
<el-icon
|
||
class="el-icon--upload"
|
||
style="font-size: 44px; color: #36b2fa"
|
||
><upload-filled
|
||
/></el-icon>
|
||
<div class="el-upload__text">
|
||
将文件拖到此处,或<em style="color: #409eff">点击上传</em>
|
||
</div>
|
||
<template #tip>
|
||
<div class="el-upload__tip">
|
||
支持单个或批量上传,单文件大小不超过
|
||
<span style="color: #f56c6c; font-weight: 600">10MB</span>
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
<div class="upload-form upform-padding">
|
||
<el-form :model="uploadForm" label-width="80px">
|
||
<el-form-item label="分类">
|
||
<el-select
|
||
v-model="uploadForm.category"
|
||
placeholder="请选择分类"
|
||
style="width: 100%"
|
||
>
|
||
<el-option label="文档" value="文档" />
|
||
<el-option label="图片" value="图片" />
|
||
<el-option label="视频" value="视频" />
|
||
<el-option label="音频" value="音频" />
|
||
<el-option label="其他" value="其他" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="是否公开">
|
||
<el-switch
|
||
v-model="uploadForm.isPublic"
|
||
active-color="#409EFF"
|
||
inactive-color="#bfbfbf"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="showUploadDialog = false">取 消</el-button>
|
||
<el-button type="primary" @click="submitUpload">上传</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 文件详情对话框 -->
|
||
<el-dialog
|
||
v-model="showFileDetail"
|
||
:title="currentFile ? currentFile.original_name : '文件详情'"
|
||
width="650px"
|
||
class="file-detail-dialog"
|
||
>
|
||
<div v-if="currentFile" class="file-detail">
|
||
<el-descriptions :column="2" border size="large">
|
||
<el-descriptions-item label="文件ID">
|
||
<el-tag>{{ currentFile.id }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="文件名">
|
||
<span class="detail-primary">{{ currentFile.file_name }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="原始文件名">{{
|
||
currentFile.original_name
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="文件大小">
|
||
<el-tag type="info">{{
|
||
formatFileSize(currentFile.file_size)
|
||
}}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="文件类型">{{
|
||
currentFile.file_type
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="文件扩展名">{{
|
||
currentFile.file_ext
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="分类">
|
||
<el-tag type="success" effect="plain">{{
|
||
currentFile.category
|
||
}}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="上传人">
|
||
<el-tag>{{ currentFile.upload_by }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="上传时间">{{
|
||
formatDate(currentFile.upload_time)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="状态">
|
||
<el-tag
|
||
:type="currentFile.status === 1 ? 'success' : 'danger'"
|
||
effect="dark"
|
||
>
|
||
{{ currentFile.status === 1 ? "正常" : "已删除" }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="是否公开">
|
||
<el-tag
|
||
:type="currentFile.is_public === 1 ? 'success' : 'info'"
|
||
effect="plain"
|
||
>
|
||
{{ currentFile.is_public === 1 ? "公开" : "私有" }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<div class="detail-btns">
|
||
<el-button type="primary" @click="downloadFile(currentFile)">
|
||
<el-icon><Download /></el-icon> 下载文件
|
||
</el-button>
|
||
<el-button @click="copyFileUrl(currentFile)">
|
||
<el-icon><Link /></el-icon> 复制链接
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="showFileDetail = false">关闭</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</el-card>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import { ElMessage } from "element-plus";
|
||
import {
|
||
Download,
|
||
Delete,
|
||
Search,
|
||
Document,
|
||
View,
|
||
Link,
|
||
UploadFilled,
|
||
} from "@element-plus/icons-vue";
|
||
|
||
const router = useRouter();
|
||
|
||
const recentFiles = ref([
|
||
{
|
||
id: 1,
|
||
name: "项目报告.pdf",
|
||
size: 2456789,
|
||
type: "PDF",
|
||
uploadTime: "2024-01-15 10:30:25",
|
||
},
|
||
{
|
||
id: 2,
|
||
name: "数据统计.xlsx",
|
||
size: 123456,
|
||
type: "Excel",
|
||
uploadTime: "2024-01-14 14:20:10",
|
||
},
|
||
{
|
||
id: 3,
|
||
name: "产品图片.jpg",
|
||
size: 345678,
|
||
type: "Image",
|
||
uploadTime: "2024-01-13 09:15:45",
|
||
},
|
||
]);
|
||
|
||
const getFileIcon = (type: string) => {
|
||
const iconMap: Record<string, string> = {
|
||
PDF: "fa-solid fa-file-pdf",
|
||
Excel: "fa-solid fa-file-excel",
|
||
Word: "fa-solid fa-file-word",
|
||
Image: "fa-solid fa-file-image",
|
||
Video: "fa-solid fa-file-video",
|
||
Audio: "fa-solid fa-file-audio",
|
||
Archive: "fa-solid fa-file-archive",
|
||
};
|
||
return iconMap[type] || "fa-solid fa-file";
|
||
};
|
||
|
||
const formatFileSize = (bytes: number) => {
|
||
if (bytes === 0) return "0 B";
|
||
const k = 1024;
|
||
const sizes = ["B", "KB", "MB", "GB"];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||
};
|
||
|
||
// mock 下列数据和函数,由于原代码没提供全部上下文
|
||
const categories = ref(["文档", "图片", "视频", "音频", "其他"]);
|
||
const filteredFiles = ref([]);
|
||
const loading = ref(false);
|
||
const searchKeyword = ref("");
|
||
const filterCategory = ref("");
|
||
const showMyFiles = ref(false);
|
||
const pageSize = ref(10);
|
||
const currentPage = ref(1);
|
||
const totalFiles = ref(0);
|
||
const showUploadDialog = ref(false);
|
||
|
||
// 上传配置
|
||
const uploadUrl = computed(() => {
|
||
const baseUrl = import.meta.env.VITE_API_BASE_URL ;
|
||
return `${baseUrl}/api/files`;
|
||
});
|
||
|
||
const uploadHeaders = computed(() => {
|
||
const token = localStorage.getItem('token');
|
||
return {
|
||
'Authorization': `Bearer ${token}`
|
||
};
|
||
});
|
||
|
||
const uploadForm = ref({
|
||
category: "",
|
||
isPublic: false,
|
||
});
|
||
const showFileDetail = ref(false);
|
||
const currentFile = ref<any>(null);
|
||
|
||
const handleSearch = () => {};
|
||
const handleFilter = () => {};
|
||
const handleSizeChange = () => {};
|
||
const handleCurrentChange = () => {};
|
||
const handleUploadClose = () => {
|
||
showUploadDialog.value = false;
|
||
uploadForm.value.category = "";
|
||
uploadForm.value.isPublic = false;
|
||
};
|
||
|
||
const handleUploadSuccess = (response: any, file: any) => {
|
||
ElMessage.success('文件上传成功!');
|
||
// 关闭对话框
|
||
showUploadDialog.value = false;
|
||
// 可以刷新文件列表
|
||
// loadFiles();
|
||
};
|
||
|
||
const handleUploadError = (error: Error, file: any) => {
|
||
ElMessage.error('文件上传失败:' + error.message);
|
||
};
|
||
|
||
const beforeUpload = (file: File) => {
|
||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||
if (file.size > maxSize) {
|
||
ElMessage.error('文件大小不能超过 10MB!');
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
const submitUpload = () => {
|
||
// Element Plus 的 el-upload 组件会自动处理上传
|
||
ElMessage.info('开始上传文件...');
|
||
};
|
||
const viewFile = (row: any) => {};
|
||
const formatDate = (val: string | number) => val;
|
||
const copyFileUrl = (file: any) => {};
|
||
|
||
const downloadFile = (file: any) => {
|
||
// 这里实现下载逻辑
|
||
console.log("下载文件:", file.name || file.file_name);
|
||
};
|
||
|
||
const deleteFile = (file: any) => {
|
||
// 这里实现删除逻辑
|
||
console.log("删除文件:", file.name || file.file_name);
|
||
};
|
||
|
||
onMounted(() => {
|
||
console.log("文件管理模块已加载");
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.fancy-bg {
|
||
padding: 28px 14px 24px 14px;
|
||
}
|
||
.stylish-panel {
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
box-shadow: 0 4px 14px 0 #e2eafe77;
|
||
margin-bottom: 28px;
|
||
padding: 28px 18px 24px 18px;
|
||
}
|
||
.function-cards {
|
||
margin-bottom: 32px;
|
||
}
|
||
.function-card {
|
||
border: none !important;
|
||
border-radius: 14px !important;
|
||
box-shadow: 0 8px 24px 0 #bedcff11 !important;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
transition:
|
||
transform 0.15s cubic-bezier(0.17, 0.67, 0.83, 0.67),
|
||
box-shadow 0.18s;
|
||
}
|
||
.function-card:hover {
|
||
transform: translateY(-4px) scale(1.032);
|
||
box-shadow: 0 18px 48px 0 #6bbaf522 !important;
|
||
}
|
||
.card-gradient {
|
||
background: linear-gradient(120deg, #ecf4ff 94%, #eff8ff 100%) !important;
|
||
}
|
||
.card-content {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 26px 16px;
|
||
min-height: 95px;
|
||
}
|
||
.card-icon {
|
||
font-size: 34px;
|
||
width: 54px;
|
||
height: 54px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #3178ea;
|
||
background: linear-gradient(133deg, #d9eaff 82%, #f2f6fd 100%);
|
||
border-radius: 12px;
|
||
margin-right: 18px;
|
||
transition: color 0.25s;
|
||
}
|
||
.card-icon.upload {
|
||
color: #13c2c2;
|
||
background: linear-gradient(145deg, #c5faea 58%, #d6f3ff 98%);
|
||
}
|
||
.card-icon.tags {
|
||
color: #a048ff;
|
||
background: linear-gradient(143deg, #e8dafe 88%, #f3e2fd 100%);
|
||
}
|
||
.card-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
.card-title {
|
||
font-size: 19px;
|
||
font-weight: 700;
|
||
color: #2c3547;
|
||
letter-spacing: 1.5px;
|
||
margin-bottom: 3px;
|
||
}
|
||
.card-desc {
|
||
font-size: 13px;
|
||
color: #6c7ea5;
|
||
font-weight: 400;
|
||
}
|
||
.rc-title-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
.recent-icon {
|
||
font-size: 20px;
|
||
color: #409eff;
|
||
margin-right: 8px;
|
||
}
|
||
.recent-title {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
color: #293a46;
|
||
}
|
||
.recent-table {
|
||
border-radius: 6px !important;
|
||
}
|
||
.input-search {
|
||
width: 300px !important;
|
||
margin-right: 18px !important;
|
||
box-shadow: 0 2px 6px 0 #dae7fd24;
|
||
}
|
||
.select-category {
|
||
width: 170px !important;
|
||
margin-right: 18px;
|
||
}
|
||
.switch-mine {
|
||
margin-left: 8px;
|
||
}
|
||
.file-list {
|
||
margin-bottom: 28px;
|
||
background: #fff;
|
||
}
|
||
.file-name {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.icon-doc {
|
||
margin-right: 8px;
|
||
color: #2090ff;
|
||
}
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 12px;
|
||
background: none;
|
||
}
|
||
.upload-dialog .el-dialog__body {
|
||
padding-top: 20px;
|
||
}
|
||
.upform-padding {
|
||
margin-top: 20px;
|
||
border-top: 1px solid #eee;
|
||
padding-top: 20px;
|
||
}
|
||
.file-detail-dialog .el-dialog__body {
|
||
padding-top: 18px;
|
||
background: #fafcff;
|
||
}
|
||
.file-detail {
|
||
max-height: 380px;
|
||
overflow-y: auto;
|
||
}
|
||
.detail-primary {
|
||
color: #416cf7;
|
||
font-weight: 600;
|
||
}
|
||
.detail-btns {
|
||
margin-top: 28px;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 22px;
|
||
}
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.stylish-panel,
|
||
.file-list,
|
||
.function-cards {
|
||
padding: 10px !important;
|
||
}
|
||
.fancy-bg {
|
||
padding: 10px !important;
|
||
}
|
||
}
|
||
</style>
|