Files
supervision-test-frontend-repo/src/components/semri/fileComponent/SUploadFile.vue
T

468 lines
12 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="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>