feat: file upload
This commit is contained in:
parent
b696456350
commit
621baef3eb
@ -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
|
||||
|
@ -1 +1,2 @@
|
||||
export { default as FileUpload } from './src/file-upload.vue';
|
||||
export { default as ImageUpload } from './src/image-upload.vue';
|
||||
|
213
apps/web-antd/src/components/upload/src/file-upload.vue
Normal file
213
apps/web-antd/src/components/upload/src/file-upload.vue
Normal 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>
|
@ -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;
|
||||
|
51
apps/web-antd/src/views/system/oss/file-upload-modal.vue
Normal file
51
apps/web-antd/src/views/system/oss/file-upload-modal.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user