This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
<!-- excel 导入组件 -->
<template>
<el-dialog :title="prop.title" v-model="state.upload.open" :close-on-click-modal="false" draggable>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="headers"
:action="baseURL + other.adaptationUrl(url)"
:disabled="state.upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-error="handleFileError"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
{{ $t('excel.operationNotice') }}
<em>{{ $t('excel.clickUpload') }}</em>
</div>
<template #tip>
<div class="el-upload__tip text-center">
<span>{{ $t('excel.fileFormat') }}</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline"
@click="downExcelTemp" v-if="tempUrl"
>{{ $t('excel.downloadTemplate') }}
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button type="primary" @click="submitFileForm">{{ $t('common.confirmButtonText') }}</el-button>
<el-button @click="state.upload.open = false">{{ $t('common.cancelButtonText') }}</el-button>
</template>
</el-dialog>
<!--校验失败错误数据-->
<el-dialog :title="$t('excel.validationFailureData')" v-model="state.errorVisible">
<el-table :data="state.errorData">
<el-table-column property="lineNum" :label="$t('excel.lineNumbers')" width="100"></el-table-column>
<el-table-column property="errors" :label="$t('excel.misDescription')" show-overflow-tooltip>
<template v-slot="scope">
<el-tag type="danger" v-for="error in scope.row.errors" :key="error">{{ error }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script setup lang="ts" name="upload-excel">
import {useMessage} from '/@/hooks/message';
import other from '/@/utils/other';
import {Session} from '/@/utils/storage';
const emit = defineEmits(['sizeChange', 'refreshDataList']);
const prop = defineProps({
url: {
type: String,
},
title: {
type: String,
},
tempUrl: {
type: String,
},
});
const uploadRef = ref();
const state = reactive({
errorVisible: false,
errorData: [],
dialog: {
title: '',
isShowDialog: false,
},
upload: {
open: false,
isUploading: false,
},
});
/**
* 下载模板文件
*/
const downExcelTemp = async () => {
try {
await other.downBlobFile(other.adaptationUrl(prop.tempUrl), {}, 'temp.xlsx');
} catch (error) {
useMessage().error('模板下载失败,请先维护模板文件');
}
};
/**
* 上传进度条变化事件
*/
const handleFileUploadProgress = () => {
state.upload.isUploading = true;
};
/**
* 上传失败事件处理
*/
const handleFileError = () => {
useMessage().error('上传失败,数据格式不合法!');
state.upload.open = false;
};
/**
* 上传成功事件处理
* @param {any} response - 上传成功的响应结果
*/
const handleFileSuccess = (response: any) => {
state.upload.isUploading = false;
state.upload.open = false;
uploadRef.value.clearFiles();
// 校验失败
if (response.code === 1) {
useMessage().error('导入失败,以下数据不合法');
state.errorVisible = true;
state.errorData = response.data;
uploadRef.value.clearFiles();
// 刷新表格
emit?.('refreshDataList');
} else {
useMessage().success(response.msg ? response.msg : '导入成功');
// 刷新表格
emit?.('refreshDataList');
}
};
/**
* 提交表单,触发上传
*/
const submitFileForm = () => {
uploadRef.value.submit();
};
/**
* 显示上传文件对话框,并清除上传信息
*/
const show = () => {
state.upload.isUploading = false;
state.upload.open = true;
};
/**
* 计算请求头部信息
*/
const headers = computed(() => {
return {
Authorization: 'Bearer ' + Session.getToken(),
'TENANT-ID': Session.getTenant(),
};
});
// 暴露变量
defineExpose({
show,
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,361 @@
<!-- 单图图片上传组件, 推荐使用 ImagePlus 组件后续将删除本组件-->
<template>
<div class="upload-box">
<el-upload
action="#"
:id="uuid"
:class="['upload', self_disabled ? 'disabled' : '', drag ? 'no-border' : '']"
list-type="picture-card"
:disabled="self_disabled"
:show-file-list="false"
:http-request="handleHttpUpload"
:before-upload="beforeUpload"
:on-success="uploadSuccess"
:on-error="uploadError"
:drag="drag"
:accept="acceptType"
>
<template v-if="imageUrl || modelValue">
<!-- 如果返回的是OSS 地址则不需要增加 baseURL -->
<img
:src="(imageUrl || modelValue || '').includes('http') ? imageUrl || modelValue : baseURL + (imageUrl || modelValue)"
class="upload-image"
/>
<div class="upload-handle" @click.stop>
<div class="handle-icon" @click="editImg" v-if="!self_disabled">
<el-icon :size="props.iconSize"><Edit /></el-icon>
<span v-if="!props.iconSize">{{ $t('common.editBtn') }}</span>
</div>
<div class="handle-icon" @click="imgViewVisible = true">
<el-icon :size="props.iconSize"><ZoomIn /></el-icon>
<span v-if="!props.iconSize">{{ $t('common.viewBtn') }}</span>
</div>
<div class="handle-icon" @click="deleteImg" v-if="!self_disabled">
<el-icon :size="props.iconSize"><Delete /></el-icon>
<span v-if="!props.iconSize">{{ $t('common.delBtn') }}</span>
</div>
</div>
</template>
<template v-else>
<div class="upload-empty">
<slot name="empty">
<el-icon><Plus /></el-icon>
<!-- <span>请上传图片</span> -->
</slot>
</div>
</template>
</el-upload>
<div class="el-upload__tip">
<slot name="tip"></slot>
</div>
<el-image-viewer
:teleported="true"
v-if="imgViewVisible"
@close="imgViewVisible = false"
:url-list="[_imageUrl.includes('http') ? _imageUrl : baseURL + _imageUrl]"
/>
</div>
</template>
<script setup lang="ts" name="UploadImg">
import { ref, computed, inject, watch } from 'vue';
import { ElNotification, formContextKey, formItemContextKey } from 'element-plus';
import type { UploadProps, UploadRequestOptions } from 'element-plus';
import { generateUUID } from '/@/utils/other';
import request from '/@/utils/request';
// 定义图片MIME类型
type ImageMimeType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'image/svg+xml' | string;
interface UploadFileProps {
imageUrl?: string; // 图片地址 ==> 必传
modelValue?: string; // 图片地址 (v-model)
uploadFileUrl?: string; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true
disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false
fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M
fileType?: ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"]
height?: string; // 组件高度 ==> 非必传(默认为 150px
width?: string; // 组件宽度 ==> 非必传(默认为 150px
borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px
iconSize?: number;
dir?: string; // 文件目录
}
// 接受父组件参数
const props = withDefaults(defineProps<UploadFileProps>(), {
imageUrl: '',
modelValue: '',
uploadFileUrl: '/admin/sys-file/upload',
drag: true,
disabled: false,
fileSize: 5,
fileType: () => ['image/jpeg', 'image/png', 'image/gif'],
height: '150px',
width: '150px',
borderRadius: '8px',
dir: '',
});
// 计算accept属性值
const acceptType = computed(() => {
return props.fileType.join(',');
});
// 生成组件唯一id
const uuid = ref('id-' + generateUUID());
// 查看图片
const imgViewVisible = ref(false);
// 获取 el-form 组件上下文
const formContext = inject(formContextKey, void 0);
// 获取 el-form-item 组件上下文
const formItemContext = inject(formItemContextKey, void 0);
// 本地 image url
const _imageUrl = ref(props.imageUrl || props.modelValue);
// 监听 props 变化
watch(
() => props.imageUrl,
(val) => {
if (val !== _imageUrl.value) {
_imageUrl.value = val || '';
}
}
);
watch(
() => props.modelValue,
(val) => {
if (val !== _imageUrl.value) {
_imageUrl.value = val || '';
}
}
);
// 判断是否禁用上传和删除
const self_disabled = computed(() => {
return props.disabled || formContext?.disabled;
});
/**
* @description 图片上传
* @param options upload 所有配置项
* */
interface UploadEmits {
(e: 'update:imageUrl', value: string): void;
(e: 'update:modelValue', value: string): void;
}
const emit = defineEmits<UploadEmits>();
const handleHttpUpload = async (options: UploadRequestOptions) => {
let formData = new FormData();
formData.append('file', options.file);
formData.append('dir', props.dir);
try {
const { data } = await request({
url: props.uploadFileUrl,
method: 'post',
headers: {
'Content-Type': 'multipart/form-data',
'Enc-Flag': 'false',
},
data: formData,
});
emit('update:imageUrl', data.url);
emit('update:modelValue', data.url);
_imageUrl.value = data.url;
// 调用 el-form 内部的校验方法(可自动校验)
formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
} catch (error) {
options.onError(error as any);
}
};
/**
* @description 删除图片
* */
const deleteImg = () => {
emit('update:imageUrl', '');
emit('update:modelValue', '');
_imageUrl.value = '';
};
/**
* @description 编辑图片
* */
const editImg = () => {
const dom = document.querySelector(`#${uuid.value} .el-upload__input`);
dom && dom.dispatchEvent(new MouseEvent('click'));
};
/**
* @description 文件上传之前判断
* @param rawFile 选择的文件
* */
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
const imgType = props.fileType.includes(rawFile.type as ImageMimeType);
if (!imgType)
ElNotification({
title: '温馨提示',
message: '上传图片不符合所需的格式!',
type: 'warning',
});
if (!imgSize)
setTimeout(() => {
ElNotification({
title: '温馨提示',
message: `上传图片大小不能超过 ${props.fileSize}M`,
type: 'warning',
});
}, 0);
return imgType && imgSize;
};
/**
* @description 图片上传成功
* */
const uploadSuccess = () => {
ElNotification({
title: '温馨提示',
message: '图片上传成功!',
type: 'success',
});
};
/**
* @description 图片上传错误
* */
const uploadError = () => {
ElNotification({
title: '温馨提示',
message: '图片上传失败,请您重新上传!',
type: 'error',
});
};
</script>
<style scoped lang="scss">
.is-error {
.upload {
:deep(.el-upload),
:deep(.el-upload-dragger) {
border: 1px dashed var(--el-color-danger) !important;
&:hover {
border-color: var(--el-color-primary) !important;
}
}
}
}
:deep(.disabled) {
.el-upload,
.el-upload-dragger {
cursor: not-allowed !important;
background: var(--el-disabled-bg-color);
border: 1px dashed var(--el-border-color-darker) !important;
&:hover {
border: 1px dashed var(--el-border-color-darker) !important;
}
}
}
.upload-box {
.no-border {
:deep(.el-upload) {
border: none !important;
}
}
:deep(.upload) {
.el-upload {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: v-bind(width);
height: v-bind(height);
overflow: hidden;
border: 1px dashed var(--el-border-color-darker);
border-radius: v-bind(borderRadius);
transition: var(--el-transition-duration-fast);
&:hover {
border-color: var(--el-color-primary);
.upload-handle {
opacity: 1;
}
}
.el-upload-dragger {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 0;
overflow: hidden;
background-color: transparent;
border: 1px dashed var(--el-border-color-darker);
border-radius: v-bind(borderRadius);
&:hover {
border: 1px dashed var(--el-color-primary);
}
}
.el-upload-dragger.is-dragover {
background-color: var(--el-color-primary-light-9);
border: 2px dashed var(--el-color-primary) !important;
}
.upload-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.upload-empty {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 30px;
color: var(--el-color-info);
.el-icon {
font-size: 28px;
color: var(--el-text-color-secondary);
}
}
.upload-handle {
position: absolute;
top: 0;
right: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
cursor: pointer;
background: rgb(0 0 0 / 60%);
opacity: 0;
transition: var(--el-transition-duration-fast);
.handle-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 6%;
color: aliceblue;
.el-icon {
margin-bottom: 40%;
font-size: 130%;
line-height: 130%;
}
span {
font-size: 85%;
line-height: 85%;
}
}
}
}
}
.el-upload__tip {
line-height: 18px;
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,278 @@
<!-- 图片上传组件, 推荐使用 ImagePlus 组件后续将删除 Image组件-->
<template>
<div>
<el-upload
multiple
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
ref="imageUpload"
:on-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{ hide: fileList.length >= limit }"
:disabled="disabled"
>
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
{{ t('uploadTipPrefix') }}
<template v-if="fileSize">
{{ t('sizeLimitTip') }} <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
{{ t('formatTip') }} <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
</template>
{{ t('fileSuffix') }}
</div>
<el-image-viewer :teleported="true" v-if="imgViewVisible" @close="imgViewVisible = false" :url-list="previewImageList" />
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted } from 'vue';
import { Plus } from '@element-plus/icons-vue';
import { ElMessage, ElLoading, ElImageViewer } from 'element-plus';
import Sortable from 'sortablejs';
import { Session } from '/@/utils/storage';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
modelValue: {
type: [String, Object, Array],
default: '',
},
// 图片地址,用于向下兼容 Image 组件
imageUrl: {
type: String,
default: '',
},
// 上传接口地址
action: {
type: String,
default: '/admin/sys-file/upload',
},
// 上传携带的参数
data: {
type: Object,
default: () => ({}),
},
// 图片数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ['png', 'jpg', 'jpeg'],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true,
},
// 拖动排序
drag: {
type: Boolean,
default: true,
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'update:imageUrl']);
const number = ref(0);
const uploadList = ref([]);
const imgViewVisible = ref(false);
const imageUpload = ref(null);
const baseURL = import.meta.env.VITE_API_URL || '';
const uploadImgUrl = computed(() => baseURL + props.action);
const fileList = ref([]);
const previewImageList = ref([]);
const headers = computed(() => {
return {
Authorization: 'Bearer ' + Session.getToken(),
'TENANT-ID': Session.getTenant(),
};
});
// 是否显示提示
const showTip = computed(() => {
return props.isShowTip && (props.fileType || props.fileSize);
});
// 监听value变化
watch(
() => [props.modelValue, props.imageUrl],
([modelVal, imageVal]) => {
// 优先使用 modelValue如果没有则使用 imageUrl
const val = modelVal || imageVal;
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : val.split(',');
// 然后将数组转为对象数组
fileList.value = list.map((item) => {
if (typeof item === 'string') {
item = { name: item, url: item };
}
return item;
});
} else {
fileList.value = [];
}
},
{ deep: true, immediate: true }
);
// 拖拽排序
onMounted(() => {
if (props.drag) {
nextTick(() => {
const element = document.querySelector('.el-upload-list');
if (element) {
Sortable.create(element, {
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0];
fileList.value.splice(evt.newIndex, 0, movedItem);
const resultString = listToString(fileList.value);
emit('update:modelValue', resultString);
emit('update:imageUrl', resultString);
},
});
}
});
}
});
// 上传前loading加载
const handleBeforeUpload = (file) => {
let isImg = false;
if (props.fileType.length) {
let fileExtension = '';
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
}
isImg = props.fileType.some((type) => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf('image') > -1;
}
if (!isImg) {
ElMessage.error(t('invalidFormatError', { fileType: props.fileType.join('/') }));
return false;
}
if (file.name.includes(',')) {
ElMessage.error(t('invalidFilenameError'));
return false;
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
ElMessage.error(t('sizeLimitError', { fileSize: props.fileSize }));
return false;
}
}
ElLoading.service({ text: t('uploading') });
number.value++;
return true;
};
// 文件个数超出
const handleExceed = () => {
ElMessage.error(t('limitExceedError', { limit: props.limit }));
};
// 上传成功回调
const handleUploadSuccess = (res, file) => {
if (res.code === 0) {
uploadList.value.push({ name: res.data.fileName, url: baseURL + res.data.url });
uploadedSuccessfully();
} else {
number.value--;
ElLoading.service().close();
ElMessage.error(t('uploadFailRetry'));
imageUpload.value.handleRemove(file);
uploadedSuccessfully();
}
};
// 删除图片
const handleDelete = (file) => {
const findex = fileList.value.map((f) => f.name).indexOf(file.name);
if (findex > -1) {
fileList.value.splice(findex, 1);
const resultString = listToString(fileList.value);
emit('update:modelValue', resultString);
emit('update:imageUrl', resultString);
}
};
// 上传失败
const handleUploadError = () => {
ElMessage.error(t('uploadFail'));
ElLoading.service().close();
};
// 上传结束处理
const uploadedSuccessfully = () => {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.concat(uploadList.value);
uploadList.value = [];
number.value = 0;
const resultString = listToString(fileList.value);
emit('update:modelValue', resultString);
emit('update:imageUrl', resultString);
ElLoading.service().close();
}
};
// 预览
const handlePictureCardPreview = (file) => {
previewImageList.value = [file.url];
imgViewVisible.value = true;
};
// 对象转成指定字符串分隔
const listToString = (list, separator) => {
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) : '';
};
</script>
<style scoped>
/* 控制加号部分 */
:deep(.hide .el-upload--picture-card) {
display: none;
}
</style>

View File

@@ -0,0 +1,30 @@
export default {
excel: {
downloadTemplate: 'downloading the template',
fileFormat: 'only xls, xlsx format files are allowed',
operationNotice: 'Drag the file here and',
clickUpload: 'click upload',
lineNumbers: 'line numbers',
misDescription: 'misDescription',
validationFailureData: 'validation failure data',
pleaseUpload: 'please upload',
size: 'size not exceeding',
format: 'format',
file: 'file',
sizeErrorText: 'file size error, max ',
typeErrorText: 'file type error, upload ',
uploadLimit: 'Upload limit exceeded. Maximum',
files: 'files allowed',
},
uploadTipPrefix: 'Please upload',
sizeLimitTip: 'size less than',
formatTip: 'format is',
fileSuffix: 'files',
invalidFormatError: 'Incorrect file format, please upload {fileType} format pictures!',
invalidFilenameError: 'Incorrect file name, cannot contain commas!',
sizeLimitError: 'Upload image size cannot exceed {fileSize} MB!',
uploading: 'Uploading image, please wait...',
limitExceedError: 'Number of uploaded files cannot exceed {limit}!',
uploadFailRetry: 'Upload failed, please try again',
uploadFail: 'Image upload failed, please try again',
};

View File

@@ -0,0 +1,30 @@
export default {
excel: {
downloadTemplate: '下载模板',
fileFormat: '仅允许导入xls、xlsx格式文件。',
operationNotice: '将文件拖到此处,或',
clickUpload: '点击上传',
lineNumbers: '行号',
misDescription: '错误描述',
validationFailureData: '校验失败数据',
pleaseUpload: '请上传',
size: '大小不超过',
format: '格式为',
file: '的文件',
sizeErrorText: '文件大小不超过',
typeErrorText: '文件类型错误,请上传 ',
uploadLimit: '上传文件数量超出限制,最多允许上传',
files: '个文件',
},
uploadTipPrefix: '请上传',
sizeLimitTip: '大小不超过',
formatTip: '格式为',
fileSuffix: '的文件',
invalidFormatError: '文件格式不正确,请上传{fileType}图片格式文件!',
invalidFilenameError: '文件名不正确,不能包含英文逗号!',
sizeLimitError: '上传头像图片大小不能超过 {fileSize} MB!',
uploading: '正在上传图片,请稍候...',
limitExceedError: '上传文件数量不能超过 {limit} 个!',
uploadFailRetry: '上传失败,请重试',
uploadFail: '上传图片失败,请重试',
};

View File

@@ -0,0 +1,340 @@
<!--文件上传组件-->
<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>
<!-- 默认上传组件 -->
<el-upload
ref="fileUpload"
v-if="props.type === 'default' && !props.disabled"
:action="baseUrl + other.adaptationUrl(props.uploadFileUrl)"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:headers="headers"
:limit="limit"
:on-error="handleUploadError"
:on-remove="handleRemove"
:on-preview="handlePreview"
:on-exceed="handleExceed"
:data="formData"
:auto-upload="autoUpload"
:on-success="handleUploadSuccess"
:accept="fileAccept"
class="upload-file-uploader"
drag
multiple
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
{{ $t('excel.operationNotice') }}
<em>{{ $t('excel.clickUpload') }}</em>
</div>
<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>
<!-- 简单上传组件 -->
<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 } 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>