赞
踩
本人23应届菜鸟,2月份入职一直工作到现在,工作时间也半年了,记录一下成长过程。
由于公司前端使用的是Vue3+Ts+AntdVue封装的Vben-Admin框架,只需看源码,就可分析处理整个的Form表单引用流程,封装只需根据业务需求实现就好。下面就由这两部分组成本文章。效果图:
组件整体结构
组件参数说明(对应组件属性)
由于本人对前端技术了解有限,目前只可以模仿前辈代码,才可以把需求实现的差不多,而且其中很多底层意义不了解,这也是我写这篇文章的原因,来加深学习。
技术细节
一、框架中的引用
在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进阶。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。