当前位置:   article > 正文

Vue3封装Upload(文件上传/文件预览)组件_vue upload组件

vue upload组件

本人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[]数组形式的,代码如下

  1. {
  2. field: 'field2',
  3. component: 'Input',
  4. label: '字段2',
  5. colProps: { span: 8 },
  6. },
  7. {
  8. field: 'field3',
  9. component: 'Select',
  10. label: '字段3',
  11. colProps: { span: 8 },
  12. },

从代码结构中可以看出Input就是一个输入框,Select是一个下拉框。而且框架文档中也给出了自定义组件的方法,具体请看官方文档https://doc.vvbin.cn/components/form.html根据文档中的方法修改后即可直接使用组件名称引入

  1. // 在 src/components/Form/src/componentMap.ts 内,添加需要的组件,并在上方 ComponentType 添加相应的类型 key
  2. componentMap.set('componentName', 组件);
  3. // ComponentType
  4. export type ComponentType = xxxx | 'componentName';

这一步最简单,但是实现需要先写出来自定义组件,最终自定义组件代码如下:

  1. {
  2. field: 'filed',
  3. label: '附件',
  4. component: 'UploadFile',
  5. componentProps: () => {
  6. return {
  7. uploadPrompt:
  8. '单个文件不超过20M,文件必须为.mp3或.wav格式文件,且文件命名为:XXXXXXXX',
  9. fileLength: 5,
  10. multiple: true,
  11. beforeUpload: (file) => beforeUploadMedia(file),
  12. };
  13. },
  14. }

二、封装Upload组件

首先业务需求是支持批量上传、支持文件预览、支持文件下载、支持文件长度限制、显示提示信息,根据以上Upload组件Html代码如下:

  1. <template>
  2. <Upload
  3. v-bind="getBindValue"
  4. v-if="showOperate"
  5. name="file"
  6. :showUploadList="false"
  7. :beforeUpload="(file, fileList) => beforeUpload(file, fileList)"
  8. :customRequest="(file) => handleUpload(file)"
  9. v-model:value="state"
  10. @change="handleChange"
  11. >
  12. <div>
  13. <a-button type="primary">
  14. <UploadOutlined />
  15. 选择文件
  16. </a-button>
  17. </div>
  18. </Upload>
  19. <span class="tip" v-if="uploadPrompt">{{ uploadPrompt }}</span>
  20. <div v-for="(item, index) in fileList" :key="index">
  21. <div class="button-container">
  22. <div :class="{ 'disabled-div': disabled }">
  23. <a @click="handlePreview(item)" :class="{ 'disabled-click': disabled }">
  24. {{ item.name }}
  25. </a>
  26. </div>
  27. <a-button type="link" @click="handlePreview(item)" :disabled="disabled">预览</a-button>
  28. <a-button type="link" @click="downLoadFile(item)">下载</a-button>
  29. <a-button type="link" @click="removeFile(index)" v-if="showOperate">删除</a-button>
  30. </div>
  31. </div>
  32. <FilePreviewModal @register="registerFilePreviewModal" />
  33. </template>
  34. <style lang="less" scoped>
  35. .disabled-click {
  36. pointer-events: none;
  37. }
  38. .disabled-div {
  39. cursor: not-allowed;
  40. }
  41. .tip {
  42. display: inline;
  43. margin-left: 10px;
  44. color: #fc6c54;
  45. }
  46. .button-container {
  47. display: flex;
  48. align-items: center;
  49. justify-content: space-around;
  50. width: 100%;
  51. border-bottom: 1px solid #d9ebf8;
  52. div {
  53. padding: 0 10px;
  54. border-radius: 5px;
  55. color: #0464cc;
  56. }
  57. & > div:nth-child(1) {
  58. flex: 1;
  59. margin-left: 0;
  60. overflow: hidden;
  61. text-overflow: ellipsis;
  62. white-space: nowrap;
  63. }
  64. }
  65. </style>

Upload的Script代码如下:

  1. <script lang="ts">
  2. import { Upload, message } from 'ant-design-vue';
  3. import { defineComponent, ref, computed, unref, watchEffect, PropType } from 'vue';
  4. import { UploadOutlined } from '@ant-design/icons-vue';
  5. import { useModal } from '/@/components/Modal';
  6. import FilePreviewModal from './components/FilePreviewModal.vue';
  7. import { uploadFile, downloadFiles, deleteFile, downloadFilesThen } from './upload.api';
  8. import { useAttrs } from '@vben/hooks';
  9. import { FileItem, getObjName, setFileType } from '@/utils/upload';
  10. import { useMessage } from '/@/hooks/web/useMessage';
  11. import { useRuleFormItem } from '/@/hooks/component/useFormItem';
  12. import { useI18n } from '/@/hooks/web/useI18n';
  13. export default defineComponent({
  14. name: 'UploadFile',
  15. components: { FilePreviewModal, UploadOutlined, Upload },
  16. props: {
  17. showOperate: { type: Boolean, default: true }, // 是否显示上传和删除按钮
  18. fileLength: { type: Number, default: 20 }, // 最大上传文件个数
  19. value: { type: String, default: '' }, // 绑定的文件字符串:以/分割
  20. uploadPrompt: { type: String }, // 上传提示
  21. beforeUpload: { type: Function as PropType<(...args) => any>, default: null }, // 上传前调用方法
  22. },
  23. emits: ['change'],
  24. setup(props, { emit }) {
  25. const attrs = useAttrs();
  26. const { t } = useI18n();
  27. const getBindValue = computed(() => ({ ...unref(attrs), ...props }));
  28. const [registerFilePreviewModal, { openModal: openFilePreviewModal }] = useModal();
  29. const { createConfirm } = useMessage();
  30. const fileList = ref<FileItem[]>([]);
  31. const disabled = ref(false);
  32. /**
  33. * 我的理解是:校验表单项以及在FormSchema中直接使用需要添加(必要)
  34. * 第一个参数为组件暴露的参数
  35. * 第二个参数为组件和Form项绑定的值
  36. * 第三个参数为什么时候校验
  37. */
  38. const [state] = useRuleFormItem(props, 'value', 'change');
  39. // 监听属性值改变,只要改变就执行以下代码
  40. watchEffect(() => {
  41. const { value } = props;
  42. if (value) {
  43. const list = value.split('/');
  44. fileList.value = [];
  45. list.map((item) => {
  46. if (item) {
  47. fileList.value.push({ name: item.split(':')[1], fileName: item });
  48. }
  49. });
  50. } else {
  51. fileList.value = [];
  52. }
  53. });
  54. // 更新value的值
  55. const updatePropByFileValue = async () => {
  56. let file = '';
  57. fileList.value.map((item) => {
  58. if (item.fileName != 'undefined') file += item.fileName + '/';
  59. });
  60. emit('change', file.substring(0, file.length - 1));
  61. };
  62. // 判断文件长度和属性长度
  63. const vaildFileListLength = async () => {
  64. const length = fileList.value.length;
  65. if (length > props.fileLength) {
  66. for (let i = 0; i <= length - props.fileLength; i++) {
  67. deleteFile(getObjName(fileList.value[0].fileName));
  68. fileList.value.splice(0, 1);
  69. }
  70. } else if (length === props.fileLength) {
  71. deleteFile(getObjName(fileList.value[0].fileName));
  72. fileList.value.splice(0, 1);
  73. }
  74. await updatePropByFileValue();
  75. };
  76. const handlePreview = async (fileItem) => {
  77. disabled.value = true;
  78. try {
  79. fileItem.type = setFileType(fileItem.name);
  80. if (!['audio', 'img', 'video'].includes(fileItem.type)) {
  81. message.info('当前文件暂不支持预览,请下载后查看');
  82. return;
  83. }
  84. /**
  85. * const downloadFilesThen = async (fileName: string) => {
  86. * const data = await defHttp.request(
  87. * { url: Api.downloadFile + '/' + getObjName(fileName), method: 'GET', responseType: 'blob' },
  88. * { isTransformResponse: false },
  89. * ).catch((error) => {
  90. * console.log(error);
  91. * });
  92. * const blobURL = window.URL.createObjectURL(new Blob([data]));
  93. * return blobURL;
  94. * };
  95. */
  96. const res = await downloadFilesThen(fileItem.fileName);
  97. fileItem.url = res;
  98. openFilePreviewModal(true, fileItem);
  99. } finally {
  100. disabled.value = false;
  101. }
  102. };
  103. const removeFile = (index) => {
  104. createConfirm({
  105. title: '是否确认删除文件?',
  106. content: '删除后请及时保存或提交,否则文件不可查看。',
  107. onOk: () => {
  108. // 自定义删除接口 如:const deleteFile = (objName: string) => defHttp.get({ url: '/file/delete' + '/' + objName });
  109. deleteFile(getObjName(fileList.value[index].fileName));
  110. fileList.value.splice(index, 1);
  111. updatePropByFileValue();
  112. message.success('删除成功');
  113. },
  114. });
  115. };
  116. const downLoadFile = (item) => {
  117. /**
  118. * 自定义下载接口
  119. * const downloadFiles = (params?: Recordable) => {
  120. * downloadFile({
  121. * url: Api.downloadFile + '/' + getObjName(params?.fileName),
  122. * fileName: getFileName(params?.name),
  123. * fileSuffix: '.' + getFileSuffix(params?.fileName),
  124. * });
  125. * };
  126. */
  127. downloadFiles({ name: item.name, fileName: item.fileName });
  128. };
  129. const handleUpload = async (files) => {
  130. if (files) {
  131. files.filename = files.file.name;
  132. // 自定义上传接口 如:const uploadFile = (params: any) => defHttp.uploadFile({ url: '/file/upload' }, params);
  133. const res = await uploadFile(files);
  134. await vaildFileListLength();
  135. let { value } = props;
  136. if (value) {
  137. value += '/' + res;
  138. } else {
  139. value = res;
  140. }
  141. emit('change', value);
  142. message.success('上传成功');
  143. }
  144. };
  145. // 组件更改时执行
  146. function handleChange() {
  147. let { value } = props;
  148. emit('change', value);
  149. }
  150. return {
  151. t,
  152. attrs,
  153. state,
  154. handleChange,
  155. registerFilePreviewModal,
  156. disabled,
  157. handlePreview,
  158. fileList,
  159. downLoadFile,
  160. getBindValue,
  161. removeFile,
  162. handleUpload,
  163. };
  164. },
  165. });
  166. </script>

文件预览FilePreviewModal组件的Html代码如下:

  1. <template>
  2. <BasicModal
  3. v-bind="$attrs"
  4. :title="title"
  5. :width="1200"
  6. :minHeight="height"
  7. :maskClosable="false"
  8. @register="registerModal"
  9. :footer="null"
  10. :centered="true"
  11. :destroyOnClose="true"
  12. >
  13. <div class="file-container">
  14. <Image v-if="fileObj.type === 'img'" :src="fileObj.url" />
  15. <audio ref="audio" v-if="fileObj.type === 'audio'" controls :src="fileObj.url"></audio>
  16. <video ref="video" v-if="fileObj.type === 'video'" controls :src="fileObj.url"></video>
  17. </div>
  18. </BasicModal>
  19. </template>
  20. <style lang="less" scoped>
  21. .file-container {
  22. align-items: center;
  23. font-size: 0;
  24. line-height: 1;
  25. audio {
  26. width: 100%;
  27. }
  28. video {
  29. width: 100%;
  30. }
  31. img {
  32. width: 100%;
  33. }
  34. }
  35. </style>

FilePreviewModal Script代码如下:

  1. <script lang="ts" setup>
  2. import { ref } from 'vue';
  3. import { Image } from 'ant-design-vue';
  4. import { BasicModal, useModalInner } from '/@/components/Modal';
  5. const title = ref('');
  6. const height = ref(50);
  7. const fileObj = {
  8. type: '',
  9. url: '',
  10. fileName: '',
  11. };
  12. const [registerModal] = useModalInner(async (data) => {
  13. title.value = '文件预览:' + data.name;
  14. fileObj.type = data.type;
  15. fileObj.fileName = data.fileName;
  16. fileObj.url = data.url;
  17. if (fileObj.type === 'img') {
  18. height.value = 700;
  19. }
  20. if (fileObj.type === 'audio') {
  21. height.value = 20;
  22. }
  23. if (fileObj.type === 'video') {
  24. height.value = 20;
  25. }
  26. });
  27. </script>

小结

第一次封装前端组件遇到很多不懂的地方,但也是提升自己的一种方法。

本片文章到此就结束啦,第一次写请多多包涵呀,不清楚的请评论或者私信哦。

下一篇准备前后端集成SocketIO,感兴趣的一键三连哦!

公众号同时发布,请搜索:PCode进阶。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/995768
推荐阅读
相关标签
  

闽ICP备14008679号