Commit 746d4a745d06ff1f0eb42a2c2d09c539202bc91e

Authored by jq
1 parent 2b95be80

wip: add upload component

.env.development
... ... @@ -5,7 +5,7 @@ VITE_USE_MOCK = true
5 5 VITE_PUBLIC_PATH = /
6 6  
7 7 # Cross-domain proxy, you can configure multiple
8   -VITE_PROXY=[["/api","http://localhost:3000"]]
  8 +VITE_PROXY=[["/api","http://localhost:3000"],["/upload","http://localhost:3001/upload"]]
9 9 # VITE_PROXY=[["/api","https://vvbin.cn/test"]]
10 10  
11 11 # Delete console
... ...
src/api/demo/model/uploadModel.ts 0 → 100644
  1 +export interface UploadApiResult {
  2 + message: string;
  3 + code: number;
  4 + url: string;
  5 +}
... ...
src/api/demo/upload.ts 0 → 100644
  1 +import { UploadApiResult } from './model/uploadModel';
  2 +import { defHttp } from '/@/utils/http/axios';
  3 +import { UploadFileParams } from '/@/utils/http/axios/types';
  4 +
  5 +enum Api {
  6 + UPLOAD_URL = '/upload',
  7 +}
  8 +
  9 +/**
  10 + * @description: 上传接口
  11 + */
  12 +export function uploadApi(
  13 + params: UploadFileParams,
  14 + onUploadProgress: (progressEvent: ProgressEvent) => void
  15 +) {
  16 + return defHttp.uploadFile<UploadApiResult>(
  17 + {
  18 + url: Api.UPLOAD_URL,
  19 + onUploadProgress,
  20 + },
  21 + params
  22 + );
  23 +}
... ...
src/components/Table/src/types/tableAction.ts
1 1 export interface ActionItem {
2 2 on?: any;
  3 + onClick?: any;
3 4 label: string;
4 5 disabled?: boolean;
5 6 color?: 'success' | 'error' | 'warning';
... ...
src/components/Upload/index.ts 0 → 100644
  1 +export { default as UploadContainer } from './src/UploadContainer.vue';
  2 +// export * from './src/types';
... ...
src/components/Upload/src/ThumnUrl.vue 0 → 100644
  1 +<template>
  2 + <span>
  3 + <img v-if="fileUrl" :src="fileUrl" />
  4 + <span v-else>{{ fileType }}</span>
  5 + </span>
  6 +</template>
  7 +<script lang="ts">
  8 + import { defineComponent } from 'vue';
  9 +
  10 + export default defineComponent({
  11 + props: {
  12 + fileUrl: {
  13 + type: String,
  14 + default: '',
  15 + },
  16 + fileType: {
  17 + type: String,
  18 + default: '',
  19 + },
  20 + fileName: {
  21 + type: String,
  22 + default: '',
  23 + },
  24 + },
  25 + setup() {
  26 + return {};
  27 + },
  28 + });
  29 +</script>
... ...
src/components/Upload/src/UploadContainer.vue 0 → 100644
  1 +<template>
  2 + <div>
  3 + <a-button-group>
  4 + <a-button type="primary" @click="openUploadModal">上传</a-button>
  5 + <a-button @click="openPreviewModal">
  6 + <Icon icon="ant-design:eye-outlined" />
  7 + </a-button>
  8 + </a-button-group>
  9 + <UploadModal v-bind="$props" @register="registerUploadModal" @change="handleChange" />
  10 + <UploadPreviewModal
  11 + :value="fileListRef"
  12 + @register="registerPreviewModal"
  13 + @change="handlePreviewChange"
  14 + />
  15 + </div>
  16 +</template>
  17 +<script lang="ts">
  18 + import { defineComponent, ref, watch, unref } from 'vue';
  19 + import { useModal } from '/@/components/Modal';
  20 + import UploadModal from './UploadModal.vue';
  21 + import { uploadContainerProps } from './props';
  22 + import UploadPreviewModal from './UploadPreviewModal.vue';
  23 + import Icon from '/@/components/Icon/index';
  24 + export default defineComponent({
  25 + components: { UploadModal, UploadPreviewModal, Icon },
  26 + props: uploadContainerProps,
  27 + setup(props, { emit }) {
  28 + // 上传modal
  29 + const [registerUploadModal, { openModal: openUploadModal }] = useModal();
  30 + // 预览modal
  31 + const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
  32 +
  33 + const fileListRef = ref<string[]>([]);
  34 + watch(
  35 + () => props.value,
  36 + (value) => {
  37 + fileListRef.value = [...(value || [])];
  38 + },
  39 + { immediate: true }
  40 + );
  41 + // 上传modal保存操作
  42 + function handleChange(urls: string[]) {
  43 + fileListRef.value = [...unref(fileListRef), ...(urls || [])];
  44 + emit('change', fileListRef.value);
  45 + }
  46 + // 预览modal保存操作
  47 + function handlePreviewChange(urls: string[]) {
  48 + fileListRef.value = [...(urls || [])];
  49 + emit('change', fileListRef.value);
  50 + }
  51 + return {
  52 + registerUploadModal,
  53 + openUploadModal,
  54 + handleChange,
  55 + handlePreviewChange,
  56 + registerPreviewModal,
  57 + openPreviewModal,
  58 + fileListRef,
  59 + };
  60 + },
  61 + });
  62 +</script>
... ...
src/components/Upload/src/UploadModal.vue 0 → 100644
  1 +<template>
  2 + <BasicModal
  3 + v-bind="$attrs"
  4 + @register="register"
  5 + @ok="handleOk"
  6 + :closeFunc="handleCloseFunc"
  7 + :maskClosable="false"
  8 + width="800px"
  9 + title="上传组件"
  10 + wrapClassName="upload-modal"
  11 + :okButtonProps="{ disabled: isUploadingRef }"
  12 + :cancelButtonProps="{ disabled: isUploadingRef }"
  13 + >
  14 + <template #centerdFooter>
  15 + <a-button @click="handleStartUpload" color="success" :loading="isUploadingRef">
  16 + {{ isUploadingRef ? '上传中' : '开始上传' }}
  17 + </a-button>
  18 + </template>
  19 + <Upload :accept="getStringAccept" :multiple="multiple" :before-upload="beforeUpload">
  20 + <a-button type="primary"> 选择文件 </a-button>
  21 + <span class="px-2">{{ getHelpText }}</span>
  22 + </Upload>
  23 + <BasicTable @register="registerTable" :dataSource="fileListRef" />
  24 + </BasicModal>
  25 +</template>
  26 +<script lang="ts">
  27 + import { defineComponent, reactive, ref, toRef, unref } from 'vue';
  28 + import { Upload } from 'ant-design-vue';
  29 + import { BasicModal, useModalInner } from '/@/components/Modal';
  30 + import { BasicTable, useTable } from '/@/components/Table';
  31 + // hooks
  32 + import { useUploadType } from './useUpload';
  33 + import { useMessage } from '/@/hooks/web/useMessage';
  34 + // types
  35 + import { FileItem, UploadResultStatus } from './types';
  36 + import { basicProps } from './props';
  37 + import { createTableColumns, createActionColumn } from './data';
  38 + // utils
  39 + import { checkFileType, checkImgType, getBase64WithFile } from './utils';
  40 + import { buildUUID } from '/@/utils/uuid';
  41 + import { createImgPreview } from '/@/components/Preview/index';
  42 + import { uploadApi } from '/@/api/demo/upload';
  43 +
  44 + export default defineComponent({
  45 + components: { BasicModal, Upload, BasicTable },
  46 + props: basicProps,
  47 + setup(props, { emit }) {
  48 + const [register, { closeModal }] = useModalInner();
  49 + const { getAccept, getStringAccept, getHelpText } = useUploadType({
  50 + acceptRef: toRef(props, 'accept'),
  51 + helpTextRef: toRef(props, 'helpText'),
  52 + maxNumberRef: toRef(props, 'maxNumber'),
  53 + maxSizeRef: toRef(props, 'maxSize'),
  54 + });
  55 +
  56 + const fileListRef = ref<FileItem[]>([]);
  57 + const state = reactive<{ fileList: FileItem[] }>({ fileList: [] });
  58 + const { createMessage } = useMessage();
  59 + // 上传前校验
  60 + function beforeUpload(file: File) {
  61 + const { size, name } = file;
  62 + const { maxSize } = props;
  63 + const accept = unref(getAccept);
  64 +
  65 + // 设置最大值,则判断
  66 + if (maxSize && file.size / 1024 / 1024 >= maxSize) {
  67 + createMessage.error(`只能上传不超过${maxSize}MB的文件!`);
  68 + return false;
  69 + }
  70 +
  71 + // 设置类型,则判断
  72 + if (accept.length > 0 && !checkFileType(file, accept)) {
  73 + createMessage.error!(`只能上传${accept.join(',')}格式文件`);
  74 + return false;
  75 + }
  76 + // 生成图片缩略图
  77 + if (checkImgType(file)) {
  78 + // beforeUpload,如果异步会调用自带上传方法
  79 + // file.thumbUrl = await getBase64(file);
  80 + getBase64WithFile(file).then(({ result: thumbUrl }) => {
  81 + fileListRef.value = [
  82 + ...unref(fileListRef),
  83 + {
  84 + uuid: buildUUID(),
  85 + file,
  86 + thumbUrl,
  87 + size,
  88 + name,
  89 + percent: 0,
  90 + type: name.split('.').pop(),
  91 + },
  92 + ];
  93 + });
  94 + } else {
  95 + fileListRef.value = [
  96 + ...unref(fileListRef),
  97 + {
  98 + uuid: buildUUID(),
  99 +
  100 + file,
  101 + size,
  102 + name,
  103 + percent: 0,
  104 + type: name.split('.').pop(),
  105 + },
  106 + ];
  107 + }
  108 + return false;
  109 + }
  110 + // 删除
  111 + function handleRemove(record: FileItem) {
  112 + const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
  113 + index !== -1 && fileListRef.value.splice(index, 1);
  114 + }
  115 + // 预览
  116 + function handlePreview(record: FileItem) {
  117 + const { thumbUrl = '' } = record;
  118 + createImgPreview({
  119 + imageList: [thumbUrl],
  120 + });
  121 + }
  122 + const [registerTable] = useTable({
  123 + columns: createTableColumns(),
  124 + actionColumn: createActionColumn(handleRemove, handlePreview),
  125 + pagination: false,
  126 + });
  127 + // 是否正在上传
  128 + const isUploadingRef = ref(false);
  129 + async function uploadApiByItem(item: FileItem) {
  130 + try {
  131 + item.status = UploadResultStatus.UPLOADING;
  132 +
  133 + const { data } = await uploadApi(
  134 + {
  135 + file: item.file,
  136 + },
  137 + function onUploadProgress(progressEvent: ProgressEvent) {
  138 + const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
  139 + item.percent = complete;
  140 + }
  141 + );
  142 + item.status = UploadResultStatus.SUCCESS;
  143 + item.responseData = data;
  144 + return {
  145 + success: true,
  146 + error: null,
  147 + };
  148 + } catch (e) {
  149 + console.log(e);
  150 + item.status = UploadResultStatus.ERROR;
  151 + return {
  152 + success: false,
  153 + error: e,
  154 + };
  155 + }
  156 + }
  157 + // 点击开始上传
  158 + async function handleStartUpload() {
  159 + try {
  160 + isUploadingRef.value = true;
  161 + const data = await Promise.all(
  162 + unref(fileListRef).map((item) => {
  163 + return uploadApiByItem(item);
  164 + })
  165 + );
  166 + isUploadingRef.value = false;
  167 + // 生产环境:抛出错误
  168 + const errorList = data.filter((item) => !item.success);
  169 + if (errorList.length > 0) {
  170 + throw errorList;
  171 + }
  172 + } catch (e) {
  173 + isUploadingRef.value = false;
  174 + throw e;
  175 + }
  176 + }
  177 + // 点击保存
  178 + function handleOk() {
  179 + // TODO: 没起作用:okButtonProps={{ disabled: state.isUploading }}
  180 + if (isUploadingRef.value) {
  181 + createMessage.warning('请等待文件上传后,保存');
  182 + return;
  183 + }
  184 + const fileList: string[] = [];
  185 +
  186 + for (const item of fileListRef.value) {
  187 + const { status, responseData } = item;
  188 + if (status === UploadResultStatus.SUCCESS && responseData) {
  189 + fileList.push(responseData.url);
  190 + }
  191 + }
  192 +
  193 + // 存在一个上传成功的即可保存
  194 +
  195 + if (fileList.length <= 0) {
  196 + createMessage.warning('没有上传成功的文件,无法保存');
  197 + return;
  198 + }
  199 + console.log(fileList);
  200 + emit('change', fileList);
  201 + fileListRef.value = [];
  202 + closeModal();
  203 + }
  204 + // 点击关闭:则所有操作不保存,包括上传的
  205 + function handleCloseFunc() {
  206 + if (!isUploadingRef.value) {
  207 + fileListRef.value = [];
  208 + return true;
  209 + } else {
  210 + createMessage.warning('请等待文件上传结束后操作');
  211 + return false;
  212 + }
  213 + }
  214 + return {
  215 + register,
  216 + closeModal,
  217 + getHelpText,
  218 + getStringAccept,
  219 + beforeUpload,
  220 + registerTable,
  221 + fileListRef,
  222 + state,
  223 + isUploadingRef,
  224 + handleStartUpload,
  225 + handleOk,
  226 + handleCloseFunc,
  227 + };
  228 + },
  229 + });
  230 +</script>
  231 +<style lang="less">
  232 + // /deep/ .ant-upload-list {
  233 + // display: none;
  234 + // }
  235 + .upload-modal {
  236 + .ant-upload-list {
  237 + display: none;
  238 + }
  239 +
  240 + .ant-table-wrapper .ant-spin-nested-loading {
  241 + padding: 0;
  242 + }
  243 + }
  244 +</style>
... ...
src/components/Upload/src/UploadPreviewModal.vue 0 → 100644
  1 +<template>
  2 + <BasicModal
  3 + wrapClassName="upload-preview-modal"
  4 + v-bind="$attrs"
  5 + width="800px"
  6 + @register="register"
  7 + title="预览"
  8 + :showOkBtn="false"
  9 + >
  10 + <BasicTable @register="registerTable" :dataSource="fileListRef" />
  11 + </BasicModal>
  12 +</template>
  13 +<script lang="ts">
  14 + import { defineComponent, watch, ref, unref } from 'vue';
  15 + import { BasicTable, useTable } from '/@/components/Table';
  16 + import { createPreviewColumns, createPreviewActionColumn } from './data';
  17 + import { BasicModal, useModalInner } from '/@/components/Modal';
  18 + import { priviewProps } from './props';
  19 + import { PreviewFileItem } from './types';
  20 + import { createImgPreview } from '/@/components/Preview/index';
  21 + import { downloadByUrl } from '/@/utils/file/FileDownload';
  22 +
  23 + export default defineComponent({
  24 + components: { BasicModal, BasicTable },
  25 + props: priviewProps,
  26 + setup(props, { emit }) {
  27 + const [register, { closeModal }] = useModalInner();
  28 + const fileListRef = ref<PreviewFileItem[]>([]);
  29 + watch(
  30 + () => props.value,
  31 + (value) => {
  32 + fileListRef.value = [];
  33 + value.forEach((item) => {
  34 + fileListRef.value = [
  35 + ...unref(fileListRef),
  36 + {
  37 + url: item,
  38 + type: item.split('.').pop() || '',
  39 + name: item.split('/').pop() || '',
  40 + },
  41 + ];
  42 + });
  43 + },
  44 + { immediate: true }
  45 + );
  46 + // 删除
  47 + function handleRemove(record: PreviewFileItem) {
  48 + const index = fileListRef.value.findIndex((item) => item.url === record.url);
  49 + if (index !== -1) {
  50 + fileListRef.value.splice(index, 1);
  51 + emit(
  52 + 'change',
  53 + fileListRef.value.map((item) => item.url)
  54 + );
  55 + }
  56 + }
  57 + // 预览
  58 + function handlePreview(record: PreviewFileItem) {
  59 + const { url = '' } = record;
  60 + createImgPreview({
  61 + imageList: [url],
  62 + });
  63 + }
  64 + // 下载
  65 + function handleDownload(record: PreviewFileItem) {
  66 + const { url = '' } = record;
  67 + downloadByUrl({ url });
  68 + }
  69 + const [registerTable] = useTable({
  70 + columns: createPreviewColumns(),
  71 + pagination: false,
  72 + actionColumn: createPreviewActionColumn({ handleRemove, handlePreview, handleDownload }),
  73 + });
  74 + return {
  75 + register,
  76 + closeModal,
  77 + fileListRef,
  78 + registerTable,
  79 + };
  80 + },
  81 + });
  82 +</script>
  83 +<style lang="less">
  84 + .upload-preview-modal {
  85 + .ant-upload-list {
  86 + display: none;
  87 + }
  88 +
  89 + .ant-table-wrapper .ant-spin-nested-loading {
  90 + padding: 0;
  91 + }
  92 + }
  93 +</style>
... ...
src/components/Upload/src/data.tsx 0 → 100644
  1 +// import { BasicColumn, TableAction, ActionItem } from '@/components/table';
  2 +import { checkImgType, isImgTypeByName } from './utils';
  3 +// import ThumnUrl from './ThumbUrl.vue';
  4 +import { Progress } from 'ant-design-vue';
  5 +import { FileItem, PreviewFileItem, UploadResultStatus } from './types';
  6 +// import { ElecArchivesSaveResult } from '@/api/biz/file/model/fileModel';
  7 +// import { quryFile } from '@/api/biz/file/file';
  8 +import { BasicColumn, ActionItem, TableAction } from '/@/components/Table/index';
  9 +
  10 +// 文件上传列表
  11 +export function createTableColumns(): BasicColumn[] {
  12 + return [
  13 + {
  14 + dataIndex: 'thumbUrl',
  15 + title: '图例',
  16 + width: 100,
  17 + customRender: ({ record }) => {
  18 + const { thumbUrl, type } = (record as FileItem) || {};
  19 + return <span>{thumbUrl ? <img src={thumbUrl} style={{ width: '50px' }} /> : type}</span>;
  20 + // return <ThumnUrl fileUrl={thumbUrl} fileType={type} fileName={type} />;
  21 + },
  22 + },
  23 + {
  24 + dataIndex: 'name',
  25 + title: '文件名',
  26 + align: 'left',
  27 + customRender: ({ text, record }) => {
  28 + const { percent, status: uploadStatus } = (record as FileItem) || {};
  29 + let status = 'normal';
  30 + if (uploadStatus === UploadResultStatus.ERROR) {
  31 + status = 'exception';
  32 + } else if (uploadStatus === UploadResultStatus.UPLOADING) {
  33 + status = 'active';
  34 + } else if (uploadStatus === UploadResultStatus.SUCCESS) {
  35 + status = 'success';
  36 + }
  37 + return (
  38 + <span>
  39 + <p class="ellipsis mb-1" title={text}>
  40 + {text}
  41 + </p>
  42 + <Progress percent={percent} size="small" status={status} />
  43 + </span>
  44 + );
  45 + },
  46 + },
  47 + {
  48 + dataIndex: 'size',
  49 + title: '文件大小',
  50 + width: 100,
  51 + customRender: ({ text = 0 }) => {
  52 + return text && (text / 1024).toFixed(2) + 'KB';
  53 + },
  54 + },
  55 + // {
  56 + // dataIndex: 'type',
  57 + // title: '文件类型',
  58 + // width: 100,
  59 + // },
  60 + {
  61 + dataIndex: 'status',
  62 + title: '状态',
  63 + width: 100,
  64 + customRender: ({ text }) => {
  65 + if (text === UploadResultStatus.SUCCESS) {
  66 + return '上传成功';
  67 + } else if (text === UploadResultStatus.ERROR) {
  68 + return '上传失败';
  69 + } else if (text === UploadResultStatus.UPLOADING) {
  70 + return '上传中';
  71 + }
  72 +
  73 + return text;
  74 + },
  75 + },
  76 + ];
  77 +}
  78 +export function createActionColumn(handleRemove: Function, handlePreview: Function): BasicColumn {
  79 + return {
  80 + width: 120,
  81 + title: '操作',
  82 + dataIndex: 'action',
  83 + fixed: false,
  84 + customRender: ({ record }) => {
  85 + const actions: ActionItem[] = [
  86 + {
  87 + label: '删除',
  88 + onClick: handleRemove.bind(null, record),
  89 + },
  90 + ];
  91 + if (checkImgType(record)) {
  92 + actions.unshift({
  93 + label: '预览',
  94 + onClick: handlePreview.bind(null, record),
  95 + });
  96 + }
  97 + return <TableAction actions={actions} />;
  98 + },
  99 + };
  100 +}
  101 +// 文件预览列表
  102 +export function createPreviewColumns(): BasicColumn[] {
  103 + return [
  104 + {
  105 + dataIndex: 'url',
  106 + title: '图例',
  107 + width: 100,
  108 + customRender: ({ record }) => {
  109 + const { url, type } = (record as PreviewFileItem) || {};
  110 + return (
  111 + <span>{isImgTypeByName(url) ? <img src={url} style={{ width: '50px' }} /> : type}</span>
  112 + );
  113 + },
  114 + },
  115 + {
  116 + dataIndex: 'name',
  117 + title: '文件名',
  118 + align: 'left',
  119 + },
  120 + ];
  121 +}
  122 +
  123 +export function createPreviewActionColumn({
  124 + handleRemove,
  125 + handlePreview,
  126 + handleDownload,
  127 +}: {
  128 + handleRemove: Function;
  129 + handlePreview: Function;
  130 + handleDownload: Function;
  131 +}): BasicColumn {
  132 + return {
  133 + width: 160,
  134 + title: '操作',
  135 + dataIndex: 'action',
  136 + fixed: false,
  137 + customRender: ({ record }) => {
  138 + const { url } = (record as PreviewFileItem) || {};
  139 +
  140 + const actions: ActionItem[] = [
  141 + {
  142 + label: '删除',
  143 + onClick: handleRemove.bind(null, record),
  144 + },
  145 + {
  146 + label: '下载',
  147 + onClick: handleDownload.bind(null, record),
  148 + },
  149 + ];
  150 + if (isImgTypeByName(url)) {
  151 + actions.unshift({
  152 + label: '预览',
  153 + onClick: handlePreview.bind(null, record),
  154 + });
  155 + }
  156 + return <TableAction actions={actions} />;
  157 + },
  158 + };
  159 +}
... ...
src/components/Upload/src/props.ts 0 → 100644
  1 +import type { PropType } from 'vue';
  2 +
  3 +export const basicProps = {
  4 + helpText: {
  5 + type: String as PropType<string>,
  6 + default: '',
  7 + },
  8 + // 文件最大多少MB
  9 + maxSize: {
  10 + type: Number as PropType<number>,
  11 + default: 2,
  12 + },
  13 + // 最大数量的文件,0不限制
  14 + maxNumber: {
  15 + type: Number as PropType<number>,
  16 + default: 0,
  17 + },
  18 + // 根据后缀,或者其他
  19 + accept: {
  20 + type: Array as PropType<string[]>,
  21 + default: () => [],
  22 + },
  23 + multiple: {
  24 + type: Boolean,
  25 + default: true,
  26 + },
  27 +};
  28 +
  29 +export const uploadContainerProps = {
  30 + value: {
  31 + type: Array as PropType<string[]>,
  32 + default: () => [],
  33 + },
  34 + ...basicProps,
  35 +};
  36 +
  37 +export const priviewProps = {
  38 + value: {
  39 + type: Array as PropType<string[]>,
  40 + default: () => [],
  41 + },
  42 +};
... ...
src/components/Upload/src/types.ts 0 → 100644
  1 +import { UploadApiResult } from '/@/api/demo/model/uploadModel';
  2 +
  3 +export enum UploadResultStatus {
  4 + SUCCESS = 'success',
  5 + ERROR = 'error',
  6 + UPLOADING = 'uploading',
  7 +}
  8 +
  9 +export interface FileItem {
  10 + thumbUrl?: string;
  11 + name: string;
  12 + size: string | number;
  13 + type?: string;
  14 + percent: number;
  15 + file: File;
  16 + status?: UploadResultStatus;
  17 + responseData?: UploadApiResult;
  18 + uuid: string;
  19 +}
  20 +
  21 +export interface PreviewFileItem {
  22 + url: string;
  23 + name: string;
  24 + type: string;
  25 +}
... ...
src/components/Upload/src/useUpload.ts 0 → 100644
  1 +import { Ref, unref, computed } from 'vue';
  2 +
  3 +export function useUploadType({
  4 + acceptRef,
  5 + // uploadTypeRef,
  6 + helpTextRef,
  7 + maxNumberRef,
  8 + maxSizeRef,
  9 +}: {
  10 + acceptRef: Ref<string[]>;
  11 + // uploadTypeRef: Ref<UploadTypeEnum>;
  12 + helpTextRef: Ref<string>;
  13 + maxNumberRef: Ref<number>;
  14 + maxSizeRef: Ref<number>;
  15 +}) {
  16 + // 文件类型限制
  17 + const getAccept = computed(() => {
  18 + // const uploadType = unref(uploadTypeRef);
  19 + const accept = unref(acceptRef);
  20 + if (accept && accept.length > 0) {
  21 + return accept;
  22 + }
  23 + return [];
  24 + });
  25 + const getStringAccept = computed(() => {
  26 + return unref(getAccept)
  27 + .map((item) => `.${item}`)
  28 + .join(',');
  29 + });
  30 + // 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。
  31 + const getHelpText = computed(() => {
  32 + const helpText = unref(helpTextRef);
  33 + if (helpText) {
  34 + return helpText;
  35 + }
  36 + const helpTexts: string[] = [];
  37 +
  38 + const accept = unref(acceptRef);
  39 + if (accept.length > 0) {
  40 + helpTexts.push(`支持${accept.join(',')}格式`);
  41 + }
  42 +
  43 + const maxSize = unref(maxSizeRef);
  44 + if (maxSize) {
  45 + helpTexts.push(`不超过${maxSize}MB`);
  46 + }
  47 +
  48 + const maxNumber = unref(maxNumberRef);
  49 + if (maxNumber) {
  50 + helpTexts.push(`最多可选择${maxNumber}个文件`);
  51 + }
  52 + return helpTexts.join(',');
  53 + });
  54 + return { getAccept, getStringAccept, getHelpText };
  55 +}
... ...
src/components/Upload/src/utils.ts 0 → 100644
  1 +export function checkFileType(file: File, accepts: string[]) {
  2 + const newTypes = accepts.join('|');
  3 + // const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
  4 + const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
  5 +
  6 + if (!reg.test(file.name)) {
  7 + return false;
  8 + } else {
  9 + return true;
  10 + }
  11 +}
  12 +export function checkImgType(file: File) {
  13 + return /\.(jpg|jpeg|png|gif)$/i.test(file.name);
  14 +}
  15 +export function isImgTypeByName(name: string) {
  16 + return /\.(jpg|jpeg|png|gif)$/i.test(name);
  17 +}
  18 +export function getBase64WithFile(file: File) {
  19 + return new Promise<{
  20 + result: string;
  21 + file: File;
  22 + }>((resolve, reject) => {
  23 + const reader = new FileReader();
  24 + reader.readAsDataURL(file);
  25 + reader.onload = () => resolve({ result: reader.result as string, file });
  26 + reader.onerror = (error) => reject(error);
  27 + });
  28 +}
... ...
src/router/menus/modules/demo/comp.ts
... ... @@ -39,6 +39,10 @@ const menu: MenuModule = {
39 39 name: '密码强度组件',
40 40 },
41 41 {
  42 + path: 'upload',
  43 + name: '上传组件',
  44 + },
  45 + {
42 46 path: 'scroll',
43 47 name: '滚动组件',
44 48 children: [
... ...
src/router/routes/modules/demo/comp.ts
... ... @@ -170,5 +170,13 @@ export default {
170 170 title: '密码强度组件',
171 171 },
172 172 },
  173 + {
  174 + path: '/upload',
  175 + name: 'UploadDemo',
  176 + component: () => import('/@/views/demo/comp/upload/index.vue'),
  177 + meta: {
  178 + title: '上传组件',
  179 + },
  180 + },
173 181 ],
174 182 } as AppRouteModule;
... ...
src/utils/http/axios/Axios.ts
... ... @@ -5,9 +5,10 @@ import { AxiosCanceler } from &#39;./axiosCancel&#39;;
5 5 import { isFunction } from '/@/utils/is';
6 6 import { cloneDeep } from 'lodash-es';
7 7  
8   -import type { RequestOptions, CreateAxiosOptions, Result } from './types';
  8 +import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
9 9 // import { ContentTypeEnum } from '/@/enums/httpEnum';
10 10 import { errorResult } from './const';
  11 +import { ContentTypeEnum } from '/@/enums/httpEnum';
11 12  
12 13 export * from './axiosTransform';
13 14  
... ... @@ -107,25 +108,42 @@ export class VAxios {
107 108 this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
108 109 }
109 110  
110   - // /**
111   - // * @description: 文件上传
112   - // */
113   - // uploadFiles(config: AxiosRequestConfig, params: File[]) {
114   - // const formData = new FormData();
115   -
116   - // Object.keys(params).forEach((key) => {
117   - // formData.append(key, params[key as any]);
118   - // });
119   -
120   - // return this.request({
121   - // ...config,
122   - // method: 'POST',
123   - // data: formData,
124   - // headers: {
125   - // 'Content-type': ContentTypeEnum.FORM_DATA,
126   - // },
127   - // });
128   - // }
  111 + /**
  112 + * @description: 文件上传
  113 + */
  114 + uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
  115 + const formData = new window.FormData();
  116 +
  117 + if (params.data) {
  118 + Object.keys(params.data).forEach((key) => {
  119 + if (!params.data) return;
  120 + const value = params.data[key];
  121 + // support key-value array data
  122 + if (Array.isArray(value)) {
  123 + value.forEach((item) => {
  124 + // { list: [ 11, 22 ] }
  125 + // formData.append('list[]', 11);
  126 + formData.append(`${key}[]`, item);
  127 + });
  128 + return;
  129 + }
  130 +
  131 + formData.append(key, params.data[key]);
  132 + });
  133 + }
  134 +
  135 + formData.append(params.name || 'file', params.file, params.filename);
  136 +
  137 + return this.axiosInstance.request<T>({
  138 + ...config,
  139 + method: 'POST',
  140 + data: formData,
  141 + headers: {
  142 + 'Content-type': ContentTypeEnum.FORM_DATA,
  143 + ignoreCancelToken: true,
  144 + },
  145 + });
  146 + }
129 147  
130 148 /**
131 149 * @description: 请求方法
... ...
src/utils/http/axios/types.ts
... ... @@ -28,3 +28,14 @@ export interface Result&lt;T = any&gt; {
28 28 message: string;
29 29 result: T;
30 30 }
  31 +// multipart/form-data:上传文件
  32 +export interface UploadFileParams {
  33 + // 其他参数
  34 + data?: { [key: string]: any };
  35 + // 文件参数的接口字段名
  36 + name?: string;
  37 + // 文件
  38 + file: File | Blob;
  39 + // 文件名
  40 + filename?: string;
  41 +}
... ...
src/views/demo/comp/upload/index.vue 0 → 100644
  1 +<template>
  2 + <div class="p-4">
  3 + <UploadContainer :maxSize="5" />
  4 + </div>
  5 +</template>
  6 +<script lang="ts">
  7 + import { defineComponent } from 'vue';
  8 + import { UploadContainer } from '/@/components/Upload/index';
  9 +
  10 + // import { Alert } from 'ant-design-vue';
  11 + export default defineComponent({
  12 + components: { UploadContainer },
  13 + setup() {
  14 + return {};
  15 + },
  16 + });
  17 +</script>
... ...