468 lines
12 KiB
Vue
468 lines
12 KiB
Vue
<!-- 文件上传下载删除组件 -->
|
||
<template>
|
||
<div class="s-upload-file-container">
|
||
<!-- 上传按钮区域 -->
|
||
<div class="upload-btn-wrapper" v-if="!disabled">
|
||
<a-button type="primary" @click="handleOpenUploadModal">
|
||
<Icon icon="ant-design:upload-outlined" />
|
||
选择文件上传
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 文件列表区域 -->
|
||
<div class="file-list-wrapper">
|
||
<div v-if="displayFileList.length > 0" class="file-list-header">
|
||
<span class="file-count">共 {{ displayFileList.length }} 个文件</span>
|
||
<a-button
|
||
type="link"
|
||
size="small"
|
||
:loading="downloadingAll"
|
||
@click="handleDownloadAll"
|
||
>
|
||
<Icon icon="ant-design:download-outlined" />
|
||
下载全部
|
||
</a-button>
|
||
</div>
|
||
<a-spin :spinning="loading">
|
||
<a-empty v-if="!loading && displayFileList.length === 0" description="暂无文件" />
|
||
<div v-else class="file-list">
|
||
<div
|
||
v-for="file in displayFileList"
|
||
:key="file.fileId || file.id"
|
||
class="file-item"
|
||
>
|
||
<!-- 文件信息 -->
|
||
<div class="file-info">
|
||
<Icon icon="ant-design:file-outlined" class="file-icon" />
|
||
<span class="file-name">{{ file.fileName }}</span>
|
||
<a-tag :color="getSecretLevelColor(file.secretLevel)" class="secret-tag">
|
||
{{ getSecretLevelLabel(file.secretLevel) }}
|
||
</a-tag>
|
||
<a-tag v-if="file.pending" color="orange" class="secret-tag">
|
||
待保存
|
||
</a-tag>
|
||
<span v-if="file.fileSize" class="file-size">{{ formatFileSize(file.fileSize) }}</span>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="file-actions">
|
||
<a-button
|
||
type="link"
|
||
size="small"
|
||
@click="handleDownload(file)"
|
||
>
|
||
<Icon icon="ant-design:download-outlined" />
|
||
下载
|
||
</a-button>
|
||
<a-button
|
||
v-if="!disabled"
|
||
type="link"
|
||
size="small"
|
||
danger
|
||
@click="handleDelete(file)"
|
||
>
|
||
<Icon icon="ant-design:delete-outlined" />
|
||
删除
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
|
||
<!-- 上传弹窗 -->
|
||
<SFileUploadModal
|
||
v-model:visible="uploadModalVisible"
|
||
:secret-level-options="secretLevelOptions"
|
||
:business-id="bussinessId"
|
||
:multiple="multiple"
|
||
@upload-success="handleUploadSuccess"
|
||
@upload-error="handleUploadError"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, computed, watch } from 'vue';
|
||
import { Icon } from '/src/components/Icon';
|
||
import { defHttp } from '/src/utils/http/axios';
|
||
import { useMessage } from '/src/hooks/web/useMessage';
|
||
import SFileUploadModal from './SFileUploadModal.vue';
|
||
|
||
const { createMessage, createConfirm } = useMessage();
|
||
|
||
const props = defineProps({
|
||
bussinessId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
bussinessSecretLevel: {
|
||
type: Number,
|
||
default: 1,
|
||
validator: (value: number) => [1, 2, 3, 4].includes(value)
|
||
},
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
multiple: {
|
||
type: Boolean,
|
||
default: true
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['upload-success', 'delete-success', 'upload-error']);
|
||
|
||
const serverFileList = ref<any[]>([]);
|
||
const pendingFileList = ref<any[]>([]);
|
||
const loading = ref<boolean>(false);
|
||
const uploadModalVisible = ref<boolean>(false);
|
||
const binding = ref<boolean>(false);
|
||
const downloadingAll = ref<boolean>(false);
|
||
|
||
const displayFileList = computed(() => {
|
||
return [
|
||
...serverFileList.value.map(f => ({ ...f, pending: false })),
|
||
...pendingFileList.value.map(f => ({ ...f, pending: true }))
|
||
];
|
||
});
|
||
|
||
const SECRET_LEVEL_CONFIG = {
|
||
1: { label: '非密', color: 'success' },
|
||
2: { label: '内部', color: 'processing' },
|
||
3: { label: '秘密', color: 'warning' },
|
||
4: { label: '机密', color: 'error' }
|
||
};
|
||
|
||
function getSecretLevelInfo(level: number) {
|
||
return SECRET_LEVEL_CONFIG[level] || SECRET_LEVEL_CONFIG[1];
|
||
}
|
||
|
||
function getSecretLevelLabel(level: number) {
|
||
return getSecretLevelInfo(level).label;
|
||
}
|
||
|
||
function getSecretLevelColor(level: number) {
|
||
return getSecretLevelInfo(level).color;
|
||
}
|
||
|
||
function formatFileSize(bytes: number): string {
|
||
if (!bytes || bytes === 0) return '';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||
}
|
||
|
||
const secretLevelOptions = computed(() => {
|
||
const level = props.bussinessSecretLevel;
|
||
const options = [];
|
||
options.push({ label: '非密', value: 1 });
|
||
if (level >= 2) options.push({ label: '内部', value: 2 });
|
||
if (level >= 3) options.push({ label: '秘密', value: 3 });
|
||
if (level >= 4) options.push({ label: '机密', value: 4 });
|
||
return options;
|
||
});
|
||
|
||
function handleOpenUploadModal() {
|
||
uploadModalVisible.value = true;
|
||
}
|
||
|
||
async function handleUploadSuccess(result: any) {
|
||
if (result.results && Array.isArray(result.results)) {
|
||
result.results.forEach((fileInfo: any) => {
|
||
if (!fileInfo.bound) {
|
||
pendingFileList.value.push({
|
||
fileId: fileInfo.fileId,
|
||
fileName: fileInfo.fileName,
|
||
filePath: fileInfo.filePath,
|
||
fileSize: fileInfo.fileSize,
|
||
secretLevel: fileInfo.secretLevel,
|
||
fileUploadType: fileInfo.fileUploadType
|
||
});
|
||
}
|
||
});
|
||
}
|
||
if (props.bussinessId) {
|
||
await loadFileList();
|
||
}
|
||
emit('upload-success', result);
|
||
}
|
||
|
||
function handleUploadError(error: any) {
|
||
emit('upload-error', error);
|
||
}
|
||
|
||
async function handleDownload(file: any) {
|
||
try {
|
||
const fileUrl = file.filePath;
|
||
const fileName = file.fileName || fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
|
||
const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL;
|
||
const data = await defHttp.get(
|
||
{ url: `${domainUrl}/sys/file/download`, params: { fileUrl }, responseType: 'blob' },
|
||
{ isTransformResponse: false }
|
||
);
|
||
if (!data || data.size === 0) {
|
||
createMessage.warning('文件下载失败');
|
||
return;
|
||
}
|
||
if (typeof window.navigator.msSaveBlob !== 'undefined') {
|
||
window.navigator.msSaveBlob(new Blob([data]), fileName);
|
||
} else {
|
||
const blobUrl = window.URL.createObjectURL(new Blob([data]));
|
||
const link = document.createElement('a');
|
||
link.style.display = 'none';
|
||
link.href = blobUrl;
|
||
link.setAttribute('download', fileName);
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(blobUrl);
|
||
}
|
||
} catch (error) {
|
||
console.error('下载失败:', error);
|
||
createMessage.error('文件下载失败');
|
||
}
|
||
}
|
||
|
||
async function handleDownloadAll() {
|
||
const files = displayFileList.value;
|
||
if (files.length === 0) return;
|
||
|
||
downloadingAll.value = true;
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
|
||
for (const file of files) {
|
||
try {
|
||
await handleDownload(file);
|
||
successCount++;
|
||
} catch (error) {
|
||
failCount++;
|
||
console.error(`下载文件"${file.fileName}"失败:`, error);
|
||
}
|
||
}
|
||
|
||
downloadingAll.value = false;
|
||
|
||
if (successCount > 0) {
|
||
createMessage.success(`成功下载 ${successCount} 个文件${failCount > 0 ? `,${failCount} 个失败` : ''}`);
|
||
} else {
|
||
createMessage.error('所有文件下载失败');
|
||
}
|
||
}
|
||
|
||
function handleDelete(file: any) {
|
||
createConfirm({
|
||
iconType: 'warning',
|
||
title: '确认删除',
|
||
content: `确定要删除文件"${file.fileName}"吗?`,
|
||
okText: '确认',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
try {
|
||
loading.value = true;
|
||
await defHttp.get({
|
||
url: '/sys/file/delete',
|
||
params: { id: file.fileId || file.id }
|
||
});
|
||
createMessage.success('文件删除成功');
|
||
if (file.pending) {
|
||
pendingFileList.value = pendingFileList.value.filter(f => f.fileId !== file.fileId);
|
||
} else {
|
||
serverFileList.value = serverFileList.value.filter(f => (f.id || f.fileId) !== (file.id || file.fileId));
|
||
}
|
||
emit('delete-success', file.fileId || file.id);
|
||
} catch (error) {
|
||
console.error('删除失败:', error);
|
||
createMessage.error('文件删除失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async function loadFileList() {
|
||
if (!props.bussinessId) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
loading.value = true;
|
||
const result = await defHttp.get({
|
||
url: '/sys/file/getFileInfoByBussinessId',
|
||
params: { bussinessId: props.bussinessId }
|
||
});
|
||
|
||
if (Array.isArray(result)) {
|
||
serverFileList.value = result;
|
||
} else {
|
||
serverFileList.value = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('加载文件列表失败:', error);
|
||
serverFileList.value = [];
|
||
createMessage.error('加载文件列表失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
//这个是在表单id还没有生成之前上传了附件,附件表没有存储bussinessId。当业务表保存后有id了,再把bussinessId更新到这些附件上去
|
||
async function bindBussinessId(newBussinessId: string) {
|
||
if (!newBussinessId) {
|
||
createMessage.warning('bussinessId 不能为空');
|
||
return false;
|
||
}
|
||
|
||
if (pendingFileList.value.length === 0) {
|
||
return true;
|
||
}
|
||
|
||
const fileIds = pendingFileList.value.map(f => f.fileId).join(',');
|
||
|
||
try {
|
||
binding.value = true;
|
||
await defHttp.get({
|
||
url: '/sys/file/updateFilesInfo',
|
||
params: {
|
||
bussinessId: newBussinessId,
|
||
fileIds: fileIds
|
||
}
|
||
});
|
||
createMessage.success(`成功关联 ${pendingFileList.value.length} 个文件`);
|
||
pendingFileList.value = [];
|
||
await loadFileList();
|
||
return true;
|
||
} catch (error) {
|
||
console.error('关联文件失败:', error);
|
||
createMessage.error('关联文件失败,请重试');
|
||
return false;
|
||
} finally {
|
||
binding.value = false;
|
||
}
|
||
}
|
||
|
||
function getPendingFileIds(): string[] {
|
||
return pendingFileList.value.map(f => f.fileId);
|
||
}
|
||
|
||
function hasPendingFiles(): boolean {
|
||
return pendingFileList.value.length > 0;
|
||
}
|
||
|
||
defineExpose({
|
||
bindBussinessId,
|
||
getPendingFileIds,
|
||
hasPendingFiles,
|
||
loadFileList
|
||
});
|
||
|
||
watch(() => props.bussinessId, (newVal) => {
|
||
if (newVal && newVal.length > 0) {
|
||
loadFileList();
|
||
} else {
|
||
serverFileList.value = [];
|
||
}
|
||
}, { immediate: true });
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.s-upload-file-container {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
|
||
.upload-btn-wrapper {
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.file-list-wrapper {
|
||
width: 100%;
|
||
min-height: 100px;
|
||
padding: 12px;
|
||
background: #fafafa;
|
||
border-radius: 4px;
|
||
|
||
.file-list-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
.file-count {
|
||
font-size: 13px;
|
||
color: #8c8c8c;
|
||
}
|
||
}
|
||
}
|
||
|
||
.file-list {
|
||
.file-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px;
|
||
background: #fff;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 4px;
|
||
margin-bottom: 8px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.file-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
|
||
.file-icon {
|
||
font-size: 16px;
|
||
color: #1890ff;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file-name {
|
||
font-size: 14px;
|
||
color: #262626;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.secret-tag {
|
||
margin-left: 4px;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|