封装一个上传文件的组件
1.子组件
点击查看代码
<template>
<div class="upload-file">
<el-upload
ref="dragFileUploadRef"
drag
multiple
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:on-progress="handleFileUploadProgress"
:on-change="handleChange"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
:accept="fileType.join(',')"
:auto-upload="isAutoUpload"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-icon class="el-icon--upload"> <i-ep-upload-filled /> </el-icon>
<div class="el-upload__text">将文件拖到此处 或 <em>点击上传</em></div>
<template #tip>
<!-- 上传提示 -->
<div v-if="showTip" class="el-upload__tip">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join(' / ') }}</b>
</template>
的文件
</div>
</template>
</el-upload>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<template v-if="!isPic">
<li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
<el-link :href="`${file.url}`" :underline="false" target="_blank" style="display: inline-block; justify-content: flex-start; width: 80%">
<p class="el-icon-document truncate w-full">
{{ getFileName(file.name || file.originalName) }}
{{ file.size ? ` (${(file.size / 1024).toFixed(2)}kb) ` : '' }}
</p>
</el-link>
<div class="ele-upload-list__item-content-action w-[120px] flex justify-around items-center">
<el-button type="primary" link @click="handlePreview(index)">预览</el-button>
<el-button type="primary" link @click="downloadFile(file.url, getFileName(file.name || file.originalName))">下载</el-button>
<el-icon @click="handleDelete(index)" class="delete-btn" v-if="!disabled"><Close /></el-icon>
</div>
</li>
<div v-for="item in progressFile" :key="item.uid" class="mb-[4px]">
<span>{{ item.name }} </span>
<el-progress :stroke-width="6" :percentage="item.percent ? Number(item.percent.toFixed(0)) : 0" class="mt-[10px]" />
</div>
</template>
<template v-else>
<el-image
v-for="(file, index) in fileList"
:key="file.uid"
style="width: 100px; height: 100px"
:src="file.url"
:preview-src-list="fileList"
:initial-index="index"
fit="cover"
/>
</template>
</transition-group>
<preview-dialog v-model:show="previewVisible" :url="url"></preview-dialog>
<preview-img v-model:show="previewImgVisible" :url="urlList" :index="0"></preview-img>
</div>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
import { delOss, listByIds } from '@/api/system/oss/index';
import { globalHeaders } from '@/utils/request';
import { downloadFile, getFileTypeFromBase64, getFileExtensionFromMimeType, handlebase64PDF } from '@/utils/file';
import { UploadFiles } from 'element-plus';
const props = defineProps({
modelValue: {
type: [String, Object, Array],
default: () => []
},
// 数量限制
limit: propTypes.number.def(5),
// 大小限制(MB)
fileSize: propTypes.number.def(5),
// 文件类型 '.xls','.ppt',
fileType: propTypes.array.def(['.doc', '.docx', '.pdf', '.png', '.jpg', '.jpeg']),
// 是否显示提示
isShowTip: propTypes.bool.def(true),
isListToString: propTypes.bool.def(true),
isNameTofileName: propTypes.bool.def(true),
isAutoUpload: propTypes.bool.def(true),
isBase64: propTypes.bool.def(false),
info: propTypes.bool.def(),
disabled: propTypes.bool.def(false),
isPic: propTypes.bool.def(false),
defaultUrl: propTypes.string.def('/resource/oss/upload')
});
const { disabled, isAutoUpload } = toRefs(props);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue', 'delOssId', 'changeFile']);
const number = ref(0);
const uploadList = ref<any[]>([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(baseUrl + props.defaultUrl); // 上传文件服务器地址
const headers = ref(globalHeaders());
const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const dragFileUploadRef = ref<ElUploadInstance>();
watch(
() => props.modelValue,
async (val) => {
if (val) {
let temp = 1;
// 首先将值转为数组
let list: any[] = [];
if (Array.isArray(val)) {
list = val;
} else {
const res = await listByIds(val);
list = res.data.map((oss) => {
return {
name: oss.originalName,
url: oss.url,
ossId: oss.ossId,
size: oss.size || ''
};
});
}
if (list.length > 0) {
// 然后将数组转为对象数组
fileList.value = list.map((item) => {
item = { name: item.name || item.originalName, url: item.url, ossId: item.ossId, size: item.size };
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
}
} else {
fileList.value = [];
return [];
}
},
{ deep: true, immediate: true }
);
// 上传前校检格式和大小
const handleBeforeUpload = (file: any) => {
// 校检文件类型
if (props.fileType.length) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(`.${fileExt}`) >= 0;
if (!isTypeOk) {
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
return false;
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(',')) {
proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
// proxy?.$modal.loading('正在上传文件,请稍候...');
number.value++;
return true;
};
// 文件个数超出
const handleExceed = () => {
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
};
// 上传失败
const handleUploadError = () => {
proxy?.$modal.msgError('上传文件失败');
};
// 上传成功回调
const handleUploadSuccess = (res: any, file: UploadFile) => {
const idx = progressFile.value.findIndex((e) => e.uid === file.uid);
progressFile.value.splice(idx, 1);
if (res.code === 200) {
uploadList.value.push({
name: res.data.fileName,
url: res.data.url,
ossId: res.data.ossId,
size: file.size
});
uploadedSuccessfully();
} else {
number.value--;
// proxy?.$modal.closeLoading();
proxy?.$modal.msgError(res.msg);
dragFileUploadRef.value?.handleRemove(file);
uploadedSuccessfully();
}
};
// 上传中回调
const progressFile = ref([]);
const handleFileUploadProgress = (res: any, file: UploadFile) => {
const idx = progressFile.value.findIndex((e) => e.uid === file.uid);
const item = { ...res, name: file.name, uid: file.uid };
if (idx === -1) {
progressFile.value.push(item);
} else if (item.percent !== 100) {
progressFile.value.splice(idx, 1, item);
}
};
const handleChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
emit('changeFile', { uploadFile, uploadFiles });
};
// 删除文件
const handleDelete = async (index: number) => {
try {
await proxy?.$modal.confirm('是否确认删除文件?删除后不可恢复!');
if (isAutoUpload.value) {
emit('delOssId', fileList.value[index].ossId);
// delOss(ossId);
}
fileList.value.splice(index, 1);
returnFileList();
} catch (err) {
console.log('handleDelete', err);
}
};
/**
* 预览
*/
const handlePreview = (index: number) => {
const src = fileList.value[index].url;
if (props.isBase64) {
// 提取文件类型信息
const mimeType = getFileTypeFromBase64(src);
const fileExtension = getFileExtensionFromMimeType(mimeType || '');
if (fileExtension) {
if (['bmp', 'jpg', 'jpeg', 'png', 'tif', 'gif', 'svg', 'raw', 'WMF', 'webp', 'avif', 'apng'].includes(fileExtension)) {
viewImgFile([src]);
} else if (fileExtension === 'pdf') {
handlebase64PDF(src);
}
} else {
viewImgFile([src]);
}
} else {
const type = src.split('.').pop() || '';
if (['bmp', 'jpg', 'jpeg', 'png', 'tif', 'gif', 'svg', 'raw', 'WMF', 'webp', 'avif', 'apng'].includes(type)) {
viewImgFile([src]);
} else if (['doc'].includes(type)) {
window.open('https://view.officeapps.live.com/op/view.aspx?src=' + encodeURIComponent(src));
} else {
viewFile(src);
}
}
};
/** 预览文件 */
const previewVisible: any = ref(false);
const url: any = ref('');
const viewFile = (src: any) => {
url.value = src;
previewVisible.value = true;
};
/** 预览图片 */
const previewImgVisible: any = ref(false);
const urlList: any = ref([]);
const viewImgFile = (list: any) => {
urlList.value = list;
previewImgVisible.value = true;
};
// 上传结束处理
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;
returnFileList();
// proxy?.$modal.closeLoading();
}
};
// 获取文件名称
const getFileName = (name: string) => {
if (!name) {
return name;
}
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf('/') > -1) {
return name.slice(name.lastIndexOf('/') + 1);
}
return name;
};
const returnFileList = () => {
if (props.isListToString) {
emit('update:modelValue', listToString(fileList.value));
} else if (props.isNameTofileName) {
emit('update:modelValue', nameTofileName(fileList.value));
} else {
emit('update:modelValue', fileList.value);
}
};
// 对象转成指定字符串分隔
const listToString = (list: any[], separator?: string) => {
let strs = '';
separator = separator || ',';
list.forEach((item) => {
if (item.ossId) {
strs += item.ossId + separator;
}
});
return strs != '' ? strs.substring(0, strs.length - 1) : '';
};
const nameTofileName = (list: any[] | null) => {
return list.map((item) => ({
fileName: item.name,
...item
}));
};
const submitUpload = () => {
dragFileUploadRef.value!.submit();
};
/**
* 对外暴露子组件方法
*/
defineExpose({
submitUpload
});
</script>
<style scoped lang="scss">
.upload-file {
width: 100%;
}
.upload-file-uploader {
margin-bottom: 5px;
:deep(.el-upload-dragger) {
width: 240px;
}
:deep(.el-upload) {
--el-upload-dragger-padding-horizontal: 10px;
}
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
.delete-btn {
display: none;
}
}
.el-upload-list__item:hover {
.delete-btn {
display: block;
cursor: pointer;
}
}
::v-deep(.el-link__inner) {
width: 100%;
}
.upload-file-list .ele-upload-list__item-content {
padding: 0 10px;
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin: 0 10px;
}
</style>
预览组件
1)preview-dialog
点击查看代码
<template>
<el-dialog v-model="dialogVisible" title="预览" :before-close="handleClose" :fullscreen="true" destroy-on-close :append-to-body="true">
<div class="h-[73vh]">
<div class="error" v-if="errorDownload">
<p>文件预览失败,请点击下方链接下载查看</p>
<el-button type="primary" link @click.prevent="downloadFile(url)">点我下载</el-button>
</div>
<div class="preview-content h-full" v-if="dialogVisible && !errorDownload">
<vue-office-docx v-if="['docx'].includes(suffixtype)" :src="url" @rendered="renderedHandler" @error="errorHandler" />
<vue-office-pdf v-if="['pdf'].includes(suffixtype)" :src="url" @rendered="renderedHandler" @error="errorHandler" />
<vue-office-excel v-if="['xlx', 'xlsx'].includes(suffixtype)" :src="url" @rendered="renderedHandler" @error="errorHandler" />
<Ofdview v-if="['ofd'].includes(suffixtype)" :file="url" :mem="parser" :canClose="true" :canOpen="false"></Ofdview>
<!-- <iframe v-if="['doc'].includes(suffixtype)" :src="dialogImageUrl" frameborder="0" width="100%" class="h-[82vh]"></iframe> -->
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="handleClose">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ElLoading } from 'element-plus';
// 引入VueOfficeDocx组件
// import VueOfficeDocx from '@vue-office/docx';
import VueOfficeDocx from '@vue-office/docx/lib/v3/vue-office-docx.umd.js';
// 引入相关样式
// import '@vue-office/docx/lib/index.css';
import '@vue-office/docx/lib/v3/index.css';
// 引入VueOfficePdf组件
// import VueOfficePdf from '@vue-office/pdf';
import VueOfficePdf from '@vue-office/pdf/lib/v3/vue-office-pdf.umd.js';
// 引入VueOfficeExcel组件
// import VueOfficeExcel from '@vue-office/excel';
import VueOfficeExcel from '@vue-office/excel/lib/v3/vue-office-excel.umd.js';
// 引入相关样式
// import '@vue-office/excel/lib/index.css';
import '@vue-office/excel/lib/v3/index.css';
import { Ofdview } from 'ofdview-vue3';
import * as parser from 'parser_x.js';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
import { downloadFile } from '@/utils/file';
interface Props {
show: boolean;
url: string;
}
const props = withDefaults(defineProps<Props>(), {
show: false,
url: ''
});
const { url } = toRefs(props);
const emits = defineEmits(['update:show']);
const dialogVisible = ref(false);
watch(
() => props.show,
(val) => {
dialogVisible.value = val;
}
);
watch(
() => dialogVisible.value,
(val) => {
emits('update:show', val);
}
);
const errorDownload = ref(false);
let loadingInstance: LoadingInstance;
const suffixtype = ref();
const dialogImageUrl = ref();
watch(
() => url.value,
async (val) => {
if (val) {
loadingInstance = ElLoading.service({ fullscreen: true });
const type = props.url.split('.').pop();
suffixtype.value = type;
await nextTick();
if (type === 'doc') {
loadingInstance.close();
}
} else {
errorDownload.value = true;
loadingInstance.close();
console.log('特殊类型');
}
}
);
const renderedHandler = () => {
console.log('渲染完成');
loadingInstance.close();
};
const errorHandler = () => {
errorDownload.value = true;
console.log('渲染失败');
loadingInstance.close();
};
const handleClose = () => {
errorDownload.value = false;
dialogVisible.value = false;
console.log('关闭预览');
loadingInstance?.close();
};
</script>
<style lang="scss">
.body-class {
height: 84vh;
}
</style>
2)preview-img
点击查看代码
<template>
<el-dialog class="preview-img" v-model="dialogVisible" :before-close="handleClose" :fullscreen="true" destroy-on-close append-to-body>
<div class="h-[100%] flex items-center justify-between">
<el-icon size="40px" :style="{ color: idx === 0 ? '#606266' : '#fff' }" @click="pre()"><ArrowLeftBold /></el-icon>
<el-image :src="url[idx]" fit="fill" class="h-[90%]" />
<el-icon size="40px" :style="{ color: idx === url.length - 1 ? '#606266' : '#fff' }" @click="next()"><ArrowRightBold /></el-icon>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
interface Props {
show: boolean;
url: Array<string>;
index: number;
}
const props = withDefaults(defineProps<Props>(), {
show: false,
url: () => [],
index: 0
});
const { url, index } = toRefs(props);
const emits = defineEmits(['update:show']);
const dialogVisible = ref(false);
const idx = ref(0);
watch(
() => props.show,
(val) => {
dialogVisible.value = val;
idx.value = index.value;
}
);
watch(
() => dialogVisible.value,
(val) => {
emits('update:show', val);
}
);
const handleClose = () => {
dialogVisible.value = false;
};
const pre = () => {
if (idx.value > 0) {
idx.value--;
}
};
const next = () => {
if (idx.value < url.value.length - 1) {
idx.value++;
}
};
</script>
<style lang="scss">
.preview-img {
background: rgba(0, 0, 0, 0.5) !important;
.el-dialog__headerbtn {
font-size: 40px;
}
.el-dialog__body {
height: 90%;
}
.el-dialog__header {
border-bottom: none !important;
}
}
.upload-file {
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__header {
border-bottom: none !important;
}
}
</style>
组件中用到的方法:
1)下载文件(downloadFile)
export const downloadFile = (url: string, fileName: string = '') => {
// 参数检查
if (!url || url.trim() === '') return '';
if (!fileName || fileName.trim() === '') {
fileName = window.decodeURIComponent(url.split('/').pop());
fileName = fileName.split('.')[0];
}
// 定义一个函数来处理下载逻辑
const triggerDownload = (fileUrl: string, fileDownloadName: string) => {
const a = document.createElement('a');
a.href = fileUrl;
if (fileDownloadName) {
a.download = fileDownloadName;
}
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
// 如果 URL 是直接可访问的链接,直接下载
// if (url.startsWith('http://') || url.startsWith('https://')) {
if (!fileName) {
triggerDownload(url, fileName);
} else {
// 否则使用 XMLHttpRequest 下载文件
const x = new XMLHttpRequest();
x.open('GET', url, true);
x.responseType = 'blob';
x.onload = function (e) {
if (x.status === 200) {
const blobUrl = window.URL.createObjectURL(x.response);
triggerDownload(blobUrl, fileName);
} else {
console.error('下载文件失败:', x.statusText);
}
};
x.onerror = function () {
console.error('下载文件出错');
};
x.send();
}
};
2)从Base64数据中提取文件类型(getFileTypeFromBase64)
export const getFileTypeFromBase64 = (base64Data: string): string | null => {
if (!base64Data) return null;
const matches = base64Data.match(/^data:([A-Za-z-+\/]+);base64,/);
return matches ? matches[1] : null;
};
3)根据MIME类型获取文件扩展名(getFileExtensionFromMimeType)
export const getFileExtensionFromMimeType = (mimeType: string): string => {
const mimeTypes: Record<string, string> = {
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/png': 'png',
'application/pdf': 'pdf',
'image/gif': 'gif',
'application/msword': 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'application/vnd.ms-excel': 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx'
};
return mimeTypes[mimeType] || 'unknown';
};
4)预览pdf类型的base64文件(handlebase64PDF)
export const handlebase64PDF = (src) => {
if (src.startsWith('data:')) {
// 提取base64数据并转换为Blob
const base64Data = src.split(',')[1];
const byteCharacters = atob(base64Data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, '_blank');
} else {
// 非base64格式的PDF直接打开
window.open(src, '_blank');
}
};
2.父组件
点击查看代码
<template>
<div>
<el-form ref="formRef" :model="form" label-width="170px" label-position="left" class="w-[60%] m-auto">
<el-form-item label="文件" prop="baseBankVoucher">
<dragFileUpload
v-model="form.baseBankVoucher"
:fileSize="5"
:fileType="['.jpg', 'jpeg', '.png', '.pdf']"
:isListToString="false"
:isAutoUpload="false"
:limit="1"
:isBase64="true"
@changeFile="getFile"
@delOssId="deleteFile"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(formRef)">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { toBase64 } from '@/utils/file';
const formRef = ref<ElFormInstance>();
const form = ref({
baseBankVoucher: null
});
const getFile = async ({ uploadFile, uploadFiles }) => {
form.value.baseBankVoucher = uploadFiles;
if (form.value.baseBankVoucher && form.value.baseBankVoucher.length > 0) {
form.value.baseBankVoucher.forEach(async (item) => (item.url = await toBase64(item.raw)));
}
};
/** 删除文件 */
const delOssIdList = ref<(string | number)[]>([]);
const deleteFile = (ossId: string | number) => {
delOssIdList.value.push(ossId);
form.value.baseBankVoucher = [];
};
/** 提交文件 */
const submitForm = async () => {
const data = { ...form.value };
if (form.value.baseBankVoucher && form.value.baseBankVoucher.length > 0) {
data.baseBankVoucher = await toBase64(form.value.baseBankVoucher[0].raw);
}
// 使用的API
xxxx(data).then(() => {
ElMessage({
type: 'success',
message: '提交成功'
});
});
};
</script>
父组件中用到的方法:
1)文件流转成base64(toBase64)
export const toBase64 = (raw) => {
return new Promise((resolve, reject) => {
if (!raw) {
resolve(null);
return;
}
const rd = new FileReader(); // 创建文件读取对象
rd.readAsDataURL(raw); // 文件读取装换为base64类型
rd.onloadend = function (e: any) {
resolve(e.target.result); // 成功时解析 Promise
};
rd.onerror = function () {
reject('Error reading file'); // 失败时拒绝 Promise
};
});
};

浙公网安备 33010602011771号