Vue3封装Upload(文件上传/文件预览)组件
vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
项目地址:https://gitcode.com/gh_mirrors/vu/vue
免费下载资源
·
本人23应届菜鸟,2月份入职一直工作到现在,工作时间也半年了,记录一下成长过程。
由于公司前端使用的是Vue3+Ts+AntdVue封装的Vben-Admin框架,只需看源码,就可分析处理整个的Form表单引用流程,封装只需根据业务需求实现就好。下面就由这两部分组成本文章。效果图:
组件整体结构
- 一个Upload组件右边一个span标签,下面是for循环渲染出来的列表
- 由a标签和三个按钮组成
- a标签显示文件名支持点击预览,也可以点击按钮预览
- 预览是打开一个新的Model(FilePreviewModal)目前仅支持图片、音视频
- 实现思路是下载文件到浏览器创建一个Blob对象,src指向这个对象的内存地址
- 上传和下载以及删除就是发送请求调用oss的Api
组件参数说明(对应组件属性)
- 文件长度(fileLength):如只需要上传一个文件,通过参数设置上传文件会替换当前文件,如果是多个文件则会替换最早上传的文件
- 隐藏关键操作按钮(showOperate):true/false
- 提示信息(uploadPrompt):字符串
- 文件保存格式(value):格式为:(oss存储中的名称:文件实际名称)/分割,代表多个
- 上传前调用方法(beforeUpload):用于校验文件,上传之前调用
- 上传下载删除接口需要自行修改,不支持传参形式,文章最后放出代码
- 其他属性也支持Antd的Upload组件
由于本人对前端技术了解有限,目前只可以模仿前辈代码,才可以把需求实现的差不多,而且其中很多底层意义不了解,这也是我写这篇文章的原因,来加深学习。
技术细节
一、框架中的引用
在VbenAdmin中Form封装成了FormSchema[]数组形式的,代码如下
{
field: 'field2',
component: 'Input',
label: '字段2',
colProps: { span: 8 },
},
{
field: 'field3',
component: 'Select',
label: '字段3',
colProps: { span: 8 },
},
从代码结构中可以看出Input就是一个输入框,Select是一个下拉框。而且框架文档中也给出了自定义组件的方法,具体请看官方文档https://doc.vvbin.cn/components/form.html根据文档中的方法修改后即可直接使用组件名称引入
// 在 src/components/Form/src/componentMap.ts 内,添加需要的组件,并在上方 ComponentType 添加相应的类型 key
componentMap.set('componentName', 组件);
// ComponentType
export type ComponentType = xxxx | 'componentName';
这一步最简单,但是实现需要先写出来自定义组件,最终自定义组件代码如下:
{
field: 'filed',
label: '附件',
component: 'UploadFile',
componentProps: () => {
return {
uploadPrompt:
'单个文件不超过20M,文件必须为.mp3或.wav格式文件,且文件命名为:XXXXXXXX',
fileLength: 5,
multiple: true,
beforeUpload: (file) => beforeUploadMedia(file),
};
},
}
二、封装Upload组件
首先业务需求是支持批量上传、支持文件预览、支持文件下载、支持文件长度限制、显示提示信息,根据以上Upload组件Html代码如下:
<template>
<Upload
v-bind="getBindValue"
v-if="showOperate"
name="file"
:showUploadList="false"
:beforeUpload="(file, fileList) => beforeUpload(file, fileList)"
:customRequest="(file) => handleUpload(file)"
v-model:value="state"
@change="handleChange"
>
<div>
<a-button type="primary">
<UploadOutlined />
选择文件
</a-button>
</div>
</Upload>
<span class="tip" v-if="uploadPrompt">{{ uploadPrompt }}</span>
<div v-for="(item, index) in fileList" :key="index">
<div class="button-container">
<div :class="{ 'disabled-div': disabled }">
<a @click="handlePreview(item)" :class="{ 'disabled-click': disabled }">
{{ item.name }}
</a>
</div>
<a-button type="link" @click="handlePreview(item)" :disabled="disabled">预览</a-button>
<a-button type="link" @click="downLoadFile(item)">下载</a-button>
<a-button type="link" @click="removeFile(index)" v-if="showOperate">删除</a-button>
</div>
</div>
<FilePreviewModal @register="registerFilePreviewModal" />
</template>
<style lang="less" scoped>
.disabled-click {
pointer-events: none;
}
.disabled-div {
cursor: not-allowed;
}
.tip {
display: inline;
margin-left: 10px;
color: #fc6c54;
}
.button-container {
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
border-bottom: 1px solid #d9ebf8;
div {
padding: 0 10px;
border-radius: 5px;
color: #0464cc;
}
& > div:nth-child(1) {
flex: 1;
margin-left: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
Upload的Script代码如下:
<script lang="ts">
import { Upload, message } from 'ant-design-vue';
import { defineComponent, ref, computed, unref, watchEffect, PropType } from 'vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import { useModal } from '/@/components/Modal';
import FilePreviewModal from './components/FilePreviewModal.vue';
import { uploadFile, downloadFiles, deleteFile, downloadFilesThen } from './upload.api';
import { useAttrs } from '@vben/hooks';
import { FileItem, getObjName, setFileType } from '@/utils/upload';
import { useMessage } from '/@/hooks/web/useMessage';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
name: 'UploadFile',
components: { FilePreviewModal, UploadOutlined, Upload },
props: {
showOperate: { type: Boolean, default: true }, // 是否显示上传和删除按钮
fileLength: { type: Number, default: 20 }, // 最大上传文件个数
value: { type: String, default: '' }, // 绑定的文件字符串:以/分割
uploadPrompt: { type: String }, // 上传提示
beforeUpload: { type: Function as PropType<(...args) => any>, default: null }, // 上传前调用方法
},
emits: ['change'],
setup(props, { emit }) {
const attrs = useAttrs();
const { t } = useI18n();
const getBindValue = computed(() => ({ ...unref(attrs), ...props }));
const [registerFilePreviewModal, { openModal: openFilePreviewModal }] = useModal();
const { createConfirm } = useMessage();
const fileList = ref<FileItem[]>([]);
const disabled = ref(false);
/**
* 我的理解是:校验表单项以及在FormSchema中直接使用需要添加(必要)
* 第一个参数为组件暴露的参数
* 第二个参数为组件和Form项绑定的值
* 第三个参数为什么时候校验
*/
const [state] = useRuleFormItem(props, 'value', 'change');
// 监听属性值改变,只要改变就执行以下代码
watchEffect(() => {
const { value } = props;
if (value) {
const list = value.split('/');
fileList.value = [];
list.map((item) => {
if (item) {
fileList.value.push({ name: item.split(':')[1], fileName: item });
}
});
} else {
fileList.value = [];
}
});
// 更新value的值
const updatePropByFileValue = async () => {
let file = '';
fileList.value.map((item) => {
if (item.fileName != 'undefined') file += item.fileName + '/';
});
emit('change', file.substring(0, file.length - 1));
};
// 判断文件长度和属性长度
const vaildFileListLength = async () => {
const length = fileList.value.length;
if (length > props.fileLength) {
for (let i = 0; i <= length - props.fileLength; i++) {
deleteFile(getObjName(fileList.value[0].fileName));
fileList.value.splice(0, 1);
}
} else if (length === props.fileLength) {
deleteFile(getObjName(fileList.value[0].fileName));
fileList.value.splice(0, 1);
}
await updatePropByFileValue();
};
const handlePreview = async (fileItem) => {
disabled.value = true;
try {
fileItem.type = setFileType(fileItem.name);
if (!['audio', 'img', 'video'].includes(fileItem.type)) {
message.info('当前文件暂不支持预览,请下载后查看');
return;
}
/**
* const downloadFilesThen = async (fileName: string) => {
* const data = await defHttp.request(
* { url: Api.downloadFile + '/' + getObjName(fileName), method: 'GET', responseType: 'blob' },
* { isTransformResponse: false },
* ).catch((error) => {
* console.log(error);
* });
* const blobURL = window.URL.createObjectURL(new Blob([data]));
* return blobURL;
* };
*/
const res = await downloadFilesThen(fileItem.fileName);
fileItem.url = res;
openFilePreviewModal(true, fileItem);
} finally {
disabled.value = false;
}
};
const removeFile = (index) => {
createConfirm({
title: '是否确认删除文件?',
content: '删除后请及时保存或提交,否则文件不可查看。',
onOk: () => {
// 自定义删除接口 如:const deleteFile = (objName: string) => defHttp.get({ url: '/file/delete' + '/' + objName });
deleteFile(getObjName(fileList.value[index].fileName));
fileList.value.splice(index, 1);
updatePropByFileValue();
message.success('删除成功');
},
});
};
const downLoadFile = (item) => {
/**
* 自定义下载接口
* const downloadFiles = (params?: Recordable) => {
* downloadFile({
* url: Api.downloadFile + '/' + getObjName(params?.fileName),
* fileName: getFileName(params?.name),
* fileSuffix: '.' + getFileSuffix(params?.fileName),
* });
* };
*/
downloadFiles({ name: item.name, fileName: item.fileName });
};
const handleUpload = async (files) => {
if (files) {
files.filename = files.file.name;
// 自定义上传接口 如:const uploadFile = (params: any) => defHttp.uploadFile({ url: '/file/upload' }, params);
const res = await uploadFile(files);
await vaildFileListLength();
let { value } = props;
if (value) {
value += '/' + res;
} else {
value = res;
}
emit('change', value);
message.success('上传成功');
}
};
// 组件更改时执行
function handleChange() {
let { value } = props;
emit('change', value);
}
return {
t,
attrs,
state,
handleChange,
registerFilePreviewModal,
disabled,
handlePreview,
fileList,
downLoadFile,
getBindValue,
removeFile,
handleUpload,
};
},
});
</script>
文件预览FilePreviewModal组件的Html代码如下:
<template>
<BasicModal
v-bind="$attrs"
:title="title"
:width="1200"
:minHeight="height"
:maskClosable="false"
@register="registerModal"
:footer="null"
:centered="true"
:destroyOnClose="true"
>
<div class="file-container">
<Image v-if="fileObj.type === 'img'" :src="fileObj.url" />
<audio ref="audio" v-if="fileObj.type === 'audio'" controls :src="fileObj.url"></audio>
<video ref="video" v-if="fileObj.type === 'video'" controls :src="fileObj.url"></video>
</div>
</BasicModal>
</template>
<style lang="less" scoped>
.file-container {
align-items: center;
font-size: 0;
line-height: 1;
audio {
width: 100%;
}
video {
width: 100%;
}
img {
width: 100%;
}
}
</style>
FilePreviewModal Script代码如下:
<script lang="ts" setup>
import { ref } from 'vue';
import { Image } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
const title = ref('');
const height = ref(50);
const fileObj = {
type: '',
url: '',
fileName: '',
};
const [registerModal] = useModalInner(async (data) => {
title.value = '文件预览:' + data.name;
fileObj.type = data.type;
fileObj.fileName = data.fileName;
fileObj.url = data.url;
if (fileObj.type === 'img') {
height.value = 700;
}
if (fileObj.type === 'audio') {
height.value = 20;
}
if (fileObj.type === 'video') {
height.value = 20;
}
});
</script>
小结
第一次封装前端组件遇到很多不懂的地方,但也是提升自己的一种方法。
本片文章到此就结束啦,第一次写请多多包涵呀,不清楚的请评论或者私信哦。
下一篇准备前后端集成SocketIO,感兴趣的一键三连哦!
公众号同时发布,请搜索:PCode进阶。
GitHub 加速计划 / vu / vue
207.54 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
73486cb5
* chore: fix link broken
Signed-off-by: snoppy <michaleli@foxmail.com>
* Update packages/template-compiler/README.md [skip ci]
---------
Signed-off-by: snoppy <michaleli@foxmail.com>
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 4 个月前
e428d891
Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
更多推荐
已为社区贡献2条内容
所有评论(0)