2025-10-28 17:22:27 +08:00

674 lines
19 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>
<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>