feat: file upload

This commit is contained in:
dap 2024-10-08 13:37:14 +08:00
parent b696456350
commit 621baef3eb
7 changed files with 304 additions and 5 deletions

View File

@ -36,7 +36,7 @@ import {
import { isArray } from 'lodash-es';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload } from '#/components/upload';
import { FileUpload, ImageUpload } from '#/components/upload';
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type FormComponentType =
@ -45,6 +45,7 @@ export type FormComponentType =
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'FileUpload'
| 'ImageUpload'
| 'Input'
| 'InputNumber'
@ -107,6 +108,7 @@ setupVbenForm<FormComponentType>({
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
ImageUpload,
FileUpload,
},
config: {
// ant design vue组件库默认都是 v-model:value

View File

@ -1 +1,2 @@
export { default as FileUpload } from './src/file-upload.vue';
export { default as ImageUpload } from './src/image-upload.vue';

View File

@ -0,0 +1,213 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import { UploadOutlined } from '@ant-design/icons-vue';
import { message, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString } from 'lodash-es';
import { uploadApi } from '#/api';
import { checkFileType } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
defineOptions({ name: 'FileUpload' });
const props = withDefaults(
defineProps<{
/**
* 建议使用拓展名(不带.)
* 或者文件头 image/png等(测试判断不准确) 不支持image/*类似的写法
* 需自行改造 ./helper/checkFileType方法
*/
accept?: string[];
api?: (...args: any[]) => Promise<any>;
disabled?: boolean;
helpText?: string;
// Infinity
maxNumber?: number;
// MB
maxSize?: number;
//
multiple?: boolean;
// support xxx.xxx.xx
// url
resultField?: 'fileName' | 'ossId' | 'url' | string;
value?: string[];
}>(),
{
value: () => [],
disabled: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => [],
multiple: false,
api: uploadApi,
resultField: '',
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
const isFirstRender = ref<boolean>(true);
watch(
() => props.value,
(v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string[] = [];
if (v) {
if (isArray(v)) {
value = v;
} else {
value.push(v);
}
fileList.value = value.map((item, i) => {
if (item && isString(item)) {
return {
uid: `${-i}`,
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
};
} else if (item && isObject(item)) {
return item;
}
return null;
}) as UploadProps['fileList'];
}
emit('update:value', value);
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
emit('delete', file);
}
};
const beforeUpload = (file: File) => {
const { maxSize, accept } = props;
const isAct = checkFileType(file, accept);
if (!isAct) {
message.error($t('component.upload.acceptUpload', [accept]));
isActMsg.value = false;
//
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
//
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
const res = await api?.(info.file);
/**
* 由getValue处理 传对象过去
* 直接传string(id)会被转为Number
*/
info.onSuccess!(res);
//
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response?.[props.resultField];
}
// key url
return item?.response?.url;
});
return list;
}
</script>
<template>
<div>
<Upload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:custom-request="customRequest"
:disabled="disabled"
:max-count="maxNumber"
:multiple="multiple"
list-type="text"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber">
<a-button>
<UploadOutlined />
{{ $t('component.upload.upload') }}
</a-button>
</div>
</Upload>
</div>
</template>
<style lang="less">
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>

View File

@ -1,4 +1,5 @@
export function checkFileType(file: File, accepts: string[]) {
console.log(file.name, accepts);
let reg;
if (!accepts || accepts.length === 0) {
reg = /.(?:jpg|jpeg|png|gif|webp)$/i;

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Alert } from 'ant-design-vue';
import { FileUpload } from '#/components/upload';
const emit = defineEmits<{ reload: [] }>();
const fileList = ref<string[]>([]);
const [BasicModal, modalApi] = useVbenModal({
onOpenChange: (isOpen) => {
if (isOpen) {
return null;
}
if (fileList.value.length > 0) {
fileList.value = [];
emit('reload');
modalApi.close();
return null;
}
},
});
const accept = ref(['txt', 'excel', 'word', 'pdf']);
const maxNumber = ref(3);
const message = computed(() => {
return `支持 [${accept.value.join(', ')}] 格式,最多上传 ${maxNumber.value} 个文件`;
});
</script>
<template>
<BasicModal
:close-on-click-modal="false"
:footer="false"
:fullscreen-button="false"
title="文件上传"
>
<div class="flex flex-col gap-4">
<Alert :message="message" show-icon type="info">aaa</Alert>
<FileUpload
v-model:value="fileList"
:accept="accept"
:max-number="maxNumber"
/>
</div>
</BasicModal>
</template>

View File

@ -26,6 +26,7 @@ import { ossDownload, ossList, ossRemove } from '#/api/system/oss';
import { downloadByData } from '#/utils/file/download';
import { columns, querySchema } from './data';
import fileUploadModal from './file-upload-modal.vue';
import imageUploadModal from './image-upload-modal.vue';
const formOptions: VbenFormProps = {
@ -157,6 +158,10 @@ function isImageFile(ext: string) {
const [ImageUploadModal, imageUploadApi] = useVbenModal({
connectedComponent: imageUploadModal,
});
const [FileUploadModal, fileUploadApi] = useVbenModal({
connectedComponent: fileUploadModal,
});
</script>
<template>
@ -185,6 +190,12 @@ const [ImageUploadModal, imageUploadApi] = useVbenModal({
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
v-access:code="['system:oss:upload']"
@click="fileUploadApi.open"
>
文件上传
</a-button>
<a-button
v-access:code="['system:oss:upload']"
@click="imageUploadApi.open"
@ -227,5 +238,6 @@ const [ImageUploadModal, imageUploadApi] = useVbenModal({
</template>
</BasicTable>
<ImageUploadModal @reload="tableApi.query" />
<FileUploadModal @reload="tableApi.query" />
</Page>
</template>

View File

@ -3,27 +3,46 @@ import { ref } from 'vue';
import { JsonPreview, Page } from '@vben/common-ui';
import { RadioGroup } from 'ant-design-vue';
import { Alert, RadioGroup } from 'ant-design-vue';
import { ImageUpload } from '#/components/upload';
import { FileUpload, ImageUpload } from '#/components/upload';
const resultField = ref<'ossId' | 'url'>('ossId');
const imageList = ref([]);
const fileList = ref([]);
const fieldOptions = [
{ label: 'ossId', value: 'ossId' },
{ label: '链接地址', value: 'url' },
];
const fileAccept = ['txt', 'excel', 'word', 'pdf'];
</script>
<template>
<Page class="flex flex-col gap-[8px]">
<Page content-class="flex flex-col gap-[12px]">
<div class="bg-background flex flex-col gap-[12px] rounded-lg p-6">
<div class="flex gap-[8px]">
<span>返回字段: </span>
<RadioGroup v-model:value="resultField" :options="fieldOptions" />
</div>
<ImageUpload v-model:value="fileList" :result-field="resultField" />
<ImageUpload v-model:value="imageList" :result-field="resultField" />
<JsonPreview :data="imageList" />
</div>
<div class="bg-background flex flex-col gap-[12px] rounded-lg p-6">
<div class="flex gap-[8px]">
<span>返回字段: </span>
<RadioGroup v-model:value="resultField" :options="fieldOptions" />
</div>
<Alert
:message="`支持的文件类型:${fileAccept.join(', ')}`"
:show-icon="true"
type="info"
/>
<FileUpload
v-model:value="fileList"
:accept="fileAccept"
:result-field="resultField"
/>
<JsonPreview :data="fileList" />
</div>
</Page>