370 lines
10 KiB
Vue
370 lines
10 KiB
Vue
<!--文件上传组件-->
|
||
<template>
|
||
<div class="w-full upload-file">
|
||
<!-- 当禁用时只显示文件列表,不使用el-upload组件 -->
|
||
<div v-if="props.disabled">
|
||
<div v-if="fileList.length === 0" class="flex justify-center items-center px-4 text-gray-400 bg-gray-50 rounded-md p">
|
||
<el-icon class="mr-2 text-lg"><Document /></el-icon>
|
||
<span class="text-sm">{{ $t('excel.noFiles') }}</span>
|
||
</div>
|
||
<div v-else>
|
||
<div
|
||
v-for="(file, index) in fileList"
|
||
:key="index"
|
||
class="flex items-center px-4 py-3 mb-1 rounded transition-colors duration-200 cursor-pointer group hover:bg-blue-50"
|
||
@click="handlePreview(file)"
|
||
>
|
||
<el-icon class="mr-3 text-blue-500"><Document /></el-icon>
|
||
<span class="flex-1 text-gray-700 truncate transition-colors duration-200 group-hover:text-blue-600">
|
||
{{ getFileName(file) }}
|
||
</span>
|
||
<el-icon class="text-gray-400 transition-colors duration-200 group-hover:text-blue-500"><Download /></el-icon>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 默认上传组件 - 简洁模式 -->
|
||
<div v-if="props.type === 'default' && !props.disabled" class="upload-file-default">
|
||
<el-upload
|
||
ref="fileUpload"
|
||
:action="baseUrl + other.adaptationUrl(props.uploadFileUrl)"
|
||
:before-upload="handleBeforeUpload"
|
||
:file-list="[]"
|
||
:headers="headers"
|
||
:limit="limit"
|
||
:on-error="handleUploadError"
|
||
:on-exceed="handleExceed"
|
||
:data="formData"
|
||
:auto-upload="autoUpload"
|
||
:on-success="handleUploadSuccess"
|
||
:accept="fileAccept"
|
||
class="upload-file-uploader-simple"
|
||
multiple
|
||
>
|
||
<el-button type="primary" size="small">
|
||
<el-icon class="mr-1"><Upload /></el-icon>
|
||
{{ $t('excel.clickUpload') }}
|
||
</el-button>
|
||
<template #tip>
|
||
<div class="el-upload__tip" v-if="props.isShowTip" style="margin-top: 8px; font-size: 12px; color: #909399;">
|
||
{{ $t('excel.pleaseUpload') }}
|
||
<template v-if="props.fileSize">
|
||
{{ $t('excel.size') }} <b style="color: #f56c6c">{{ props.fileSize }}MB</b></template
|
||
>
|
||
<template v-if="props.fileType">
|
||
{{ $t('excel.format') }} <b style="color: #f56c6c">{{ props.fileType.join('/') }}</b>
|
||
</template>
|
||
{{ $t('excel.file') }}
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
<!-- 已上传文件列表 -->
|
||
<div v-if="fileList.length > 0" class="uploaded-files-list" style="margin-top: 12px;">
|
||
<div
|
||
v-for="(file, index) in fileList"
|
||
:key="index"
|
||
class="uploaded-file-item"
|
||
style="display: flex; align-items: center; padding: 8px 12px; margin-bottom: 8px; background: #f5f7fa; border-radius: 4px;"
|
||
>
|
||
<el-icon class="mr-2" style="color: #409eff;"><Document /></el-icon>
|
||
<span class="file-name" style="flex: 1; font-size: 14px; color: #606266; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||
{{ getFileName(file) }}
|
||
</span>
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click="handleRemove(file)"
|
||
style="margin-left: 8px;"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handlePreview(file)"
|
||
style="margin-left: 4px;"
|
||
>
|
||
<el-icon><Download /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 简单上传组件 -->
|
||
<el-upload
|
||
ref="fileUpload"
|
||
v-if="props.type === 'simple' && !props.disabled"
|
||
:action="baseUrl + other.adaptationUrl(props.uploadFileUrl)"
|
||
:before-upload="handleBeforeUpload"
|
||
:file-list="fileList"
|
||
:headers="headers"
|
||
:limit="limit"
|
||
:auto-upload="autoUpload"
|
||
:on-error="handleUploadError"
|
||
:on-remove="handleRemove"
|
||
:on-exceed="handleExceed"
|
||
:data="formData"
|
||
:on-success="handleUploadSuccess"
|
||
:accept="fileAccept"
|
||
class="upload-file-uploader"
|
||
multiple
|
||
>
|
||
<el-button type="primary" link>{{ $t('excel.clickUpload') }}</el-button>
|
||
<template #tip>
|
||
<div class="el-upload__tip" v-if="props.isShowTip">
|
||
{{ $t('excel.pleaseUpload') }}
|
||
<template v-if="props.fileSize">
|
||
{{ $t('excel.size') }} <b style="color: #f56c6c">{{ props.fileSize }}MB</b></template
|
||
>
|
||
<template v-if="props.fileType">
|
||
{{ $t('excel.format') }} <b style="color: #f56c6c">{{ props.fileType.join('/') }}</b>
|
||
</template>
|
||
{{ $t('excel.file') }}
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="upload-file">
|
||
import { useMessage } from '/@/hooks/message';
|
||
import { Session } from '/@/utils/storage';
|
||
import other from '/@/utils/other';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { ref, computed, watch } from 'vue';
|
||
import { Document, Download, Upload, Delete } from '@element-plus/icons-vue';
|
||
|
||
// 定义基础URL
|
||
const baseUrl = import.meta.env.VITE_API_URL || '';
|
||
|
||
// 获取文件名
|
||
const getFileName = (file: any): string => {
|
||
return file.url ? other.getQueryString(file.url, 'fileName') || other.getQueryString(file.url, 'originalFileName') : 'File';
|
||
};
|
||
|
||
// 根据文件类型生成accept属性值
|
||
const fileAccept = computed(() => {
|
||
if (!props.fileType || props.fileType.length === 0) return '';
|
||
|
||
let acceptValues: string[] = [];
|
||
|
||
for (const ext of props.fileType) {
|
||
if (typeof ext === 'string') {
|
||
acceptValues.push(`.${ext}`);
|
||
}
|
||
}
|
||
|
||
return acceptValues.join(',');
|
||
});
|
||
|
||
interface FileItem {
|
||
name?: string;
|
||
url?: string;
|
||
uid?: number;
|
||
}
|
||
|
||
interface UploadFileItem {
|
||
name: string;
|
||
url: string;
|
||
fileUrl: string;
|
||
fileSize: number;
|
||
fileName: string;
|
||
fileType: string;
|
||
}
|
||
|
||
const props = defineProps({
|
||
modelValue: [String, Array],
|
||
// 数量限制
|
||
limit: {
|
||
type: Number,
|
||
default: 5,
|
||
},
|
||
// 大小限制(MB)
|
||
fileSize: {
|
||
type: Number,
|
||
default: 5,
|
||
},
|
||
fileType: {
|
||
type: Array,
|
||
default: () => ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'txt', 'pdf', 'docx', 'xlsx', 'pptx'],
|
||
},
|
||
// 是否显示提示
|
||
isShowTip: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
uploadFileUrl: {
|
||
type: String,
|
||
default: '/admin/sys-file/upload',
|
||
},
|
||
type: {
|
||
type: String,
|
||
default: 'default',
|
||
validator: (value: string) => {
|
||
return ['default', 'simple'].includes(value);
|
||
},
|
||
},
|
||
data: {
|
||
type: Object,
|
||
default: () => ({}),
|
||
},
|
||
dir: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
autoUpload: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits(['update:modelValue', 'change']);
|
||
|
||
const number = ref(0);
|
||
const fileList = ref<FileItem[]>([]);
|
||
const uploadList = ref<UploadFileItem[]>([]);
|
||
const fileUpload = ref();
|
||
const { t } = useI18n();
|
||
|
||
// 请求头处理
|
||
const headers = computed(() => {
|
||
return {
|
||
Authorization: 'Bearer ' + Session.get('token'),
|
||
'TENANT-ID': Session.getTenant(),
|
||
};
|
||
});
|
||
|
||
// 请求参数处理
|
||
const formData = computed(() => {
|
||
return Object.assign(props.data, { dir: props.dir });
|
||
});
|
||
|
||
// 上传前校检格式和大小
|
||
const handleBeforeUpload = (file: File) => {
|
||
// 校检文件类型
|
||
if (props.fileType.length) {
|
||
const fileName = file.name.split('.');
|
||
const fileExt = fileName[fileName.length - 1];
|
||
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
|
||
if (!isTypeOk) {
|
||
useMessage().error(`${t('excel.typeErrorText')} ${props.fileType.join('/')}!`);
|
||
return false;
|
||
}
|
||
}
|
||
// 校检文件大小
|
||
if (props.fileSize) {
|
||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||
if (!isLt) {
|
||
useMessage().error(`${t('excel.sizeErrorText')} ${props.fileSize} MB!`);
|
||
return false;
|
||
}
|
||
}
|
||
number.value++;
|
||
return true;
|
||
};
|
||
|
||
// 上传成功回调
|
||
function handleUploadSuccess(res: any, file: any) {
|
||
if (res.code === 0) {
|
||
uploadList.value.push({
|
||
name: file.name,
|
||
url: `${res.data?.url}&originalFileName=${file.name}`,
|
||
fileUrl: res.data?.fileName,
|
||
fileSize: file.size,
|
||
fileName: file.name,
|
||
fileType: file.raw.type,
|
||
});
|
||
uploadedSuccessfully();
|
||
} else {
|
||
number.value--;
|
||
useMessage().error(res.msg);
|
||
fileUpload.value.handleRemove(file);
|
||
uploadedSuccessfully();
|
||
}
|
||
}
|
||
|
||
// 上传结束处理
|
||
const uploadedSuccessfully = () => {
|
||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
|
||
uploadList.value = [];
|
||
number.value = 0;
|
||
emit('update:modelValue', listToString(fileList.value));
|
||
emit('change', listToString(fileList.value), fileList.value);
|
||
}
|
||
};
|
||
|
||
const handleRemove = (file: { name: string }) => {
|
||
fileList.value = fileList.value.filter((f) => f.name !== file.name);
|
||
emit('update:modelValue', listToString(fileList.value));
|
||
emit('change', listToString(fileList.value), fileList.value);
|
||
};
|
||
|
||
const handlePreview = (file: any) => {
|
||
other.downBlobFile(file.url, {}, file.name);
|
||
};
|
||
|
||
// 添加 handleExceed 函数
|
||
const handleExceed = () => {
|
||
useMessage().warning(`${t('excel.uploadLimit')} ${props.limit} ${t('excel.files')}`);
|
||
};
|
||
|
||
/**
|
||
* 将对象数组转为字符串,以逗号分隔。
|
||
* @param list 待转换的对象数组。
|
||
* @param separator 分隔符,默认为逗号。
|
||
* @returns {string} 返回转换后的字符串。
|
||
*/
|
||
const listToString = (list: FileItem[], separator = ','): string => {
|
||
let strs = '';
|
||
separator = separator || ',';
|
||
for (let i in list) {
|
||
if (list[i].url) {
|
||
strs += list[i].url + separator;
|
||
}
|
||
}
|
||
return strs !== '' ? strs.substr(0, strs.length - 1) : '';
|
||
};
|
||
|
||
const handleUploadError = () => {
|
||
useMessage().error('上传文件失败');
|
||
};
|
||
|
||
/**
|
||
* 监听 props 中的 modelValue 值变化,更新 fileList。
|
||
*/
|
||
watch(
|
||
() => props.modelValue,
|
||
(val) => {
|
||
if (val) {
|
||
let temp = 1;
|
||
// 首先将值转为数组
|
||
const list = Array.isArray(val) ? val : (props.modelValue as string).split(',');
|
||
// 然后将数组转为对象数组
|
||
fileList.value = list.map((item: any) => {
|
||
if (typeof item === 'string') {
|
||
item = { name: other.getQueryString(item, 'originalFileName') || other.getQueryString(item, 'fileName'), url: item };
|
||
}
|
||
item.uid = item.uid || new Date().getTime() + temp++;
|
||
return item as FileItem;
|
||
});
|
||
} else {
|
||
fileList.value = [];
|
||
}
|
||
},
|
||
{ deep: true, immediate: true }
|
||
);
|
||
|
||
const submit = () => {
|
||
fileUpload.value?.submit();
|
||
};
|
||
|
||
defineExpose({
|
||
submit,
|
||
});
|
||
</script>
|