feat: 增加 file 文件上传 50%

pull/75/MERGE
YunaiV 2025-04-17 23:30:34 +08:00
parent 54f9d0c10f
commit f6e2dc55ff
11 changed files with 390 additions and 2 deletions

View File

@ -21,6 +21,8 @@ import { $t } from '@vben/locales';
import { notification } from 'ant-design-vue';
import { FileUpload, ImageUpload } from '#/components/upload';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
@ -129,6 +131,8 @@ export type ComponentType =
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| 'FileUpload'
| 'ImageUpload'
| BaseFormComponentType;
async function initComponentAdapter() {
@ -183,6 +187,8 @@ async function initComponentAdapter() {
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
FileUpload,
ImageUpload,
};
// 将组件注册到全局共享状态中

View File

@ -1,5 +1,9 @@
import { requestClient } from '#/api/request';
import type { PageParam, PageResult } from '@vben/request';
import type { AxiosRequestConfig } from '@vben/request';
/** Axios 上传进度事件 */
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
export namespace InfraFileApi {
/** 文件信息 */
@ -46,7 +50,8 @@ export function createFile(data: InfraFileApi.InfraFile) {
return requestClient.post('/infra/file/create', data);
}
// TODO @芋艿:需要 data 自定义个类型;
/** 上传文件 */
export function uploadFile(data: any) {
return requestClient.upload('/infra/file/upload', data);
export function uploadFile(data: any, onUploadProgress?: AxiosProgressEvent) {
return requestClient.upload('/infra/file/upload', data, { onUploadProgress });
}

View File

@ -0,0 +1,53 @@
import { requestClient } from '#/api/request';
/** OAuth2.0 授权信息响应 */
export interface OAuth2OpenAuthorizeInfoRespVO {
client: {
name: string;
logo: string;
};
scopes: {
key: string;
value: boolean;
}[];
}
/** 获得授权信息 */
export function getAuthorize(clientId: string) {
return requestClient.get<OAuth2OpenAuthorizeInfoRespVO>(`/system/oauth2/authorize?clientId=${clientId}`);
}
/** 发起授权 */
export function authorize(
responseType: string,
clientId: string,
redirectUri: string,
state: string,
autoApprove: boolean,
checkedScopes: string[],
uncheckedScopes: string[]
) {
// 构建 scopes
const scopes: Record<string, boolean> = {};
for (const scope of checkedScopes) {
scopes[scope] = true;
}
for (const scope of uncheckedScopes) {
scopes[scope] = false;
}
// 发起请求
return requestClient.post<string>('/system/oauth2/authorize', null, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
params: {
response_type: responseType,
client_id: clientId,
redirect_uri: redirectUri,
state: state,
auto_approve: autoApprove,
scope: JSON.stringify(scopes)
}
});
}

View File

@ -0,0 +1,218 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosProgressEvent } from '#/api/infra/file';
import type { AxiosResponse } from '@vben/request';
import { CloudUpload } from '@vben/icons';
import { message, Upload, Button } from 'ant-design-vue';
import { $t } from '@vben/locales';
import { ref, toRefs, watch } from 'vue';
import { isFunction, isObject, isString } from '@vben/utils';
import { checkFileType } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
import { uploadFile } from '#/api/infra/file';
defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
//
accept?: string[];
api?: (
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
disabled?: boolean;
helpText?: string;
// Infinity
maxNumber?: number;
// MB
maxSize?: number;
//
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
//
showDescription?: boolean;
value?: string[] | string;
}>(),
{
value: () => [],
disabled: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => [],
multiple: false,
api: (file: Blob | File, onUploadProgress?: AxiosProgressEvent) => {
// TODO @
return uploadFile({ file }, onUploadProgress);
},
resultField: '',
showDescription: false,
},
);
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 (Array.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: UploadResultStatus.DONE,
url: item,
};
} else if (item && isObject(item)) {
return item;
}
return null;
}) as UploadProps['fileList'];
}
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 = async (file: File) => {
const { maxSize, accept } = props;
const isAct = checkFileType(file, accept);
if (!isAct) {
message.error($t('ui.upload.acceptUpload', [accept]));
isActMsg.value = false;
//
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('ui.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 progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
//
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) => {
debugger
if (item?.response && props?.resultField) {
return item?.response;
}
return item?.url || item?.response?.url || item?.response;
});
// add by String
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
}
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"
:progress="{ showInfo: true }"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber">
<Button>
<CloudUpload />
{{ $t('ui.upload.upload') }}
</Button>
</div>
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
</Upload>
</div>
</template>

View File

@ -0,0 +1,18 @@
export function checkFileType(file: File, accepts: string[]) {
let reg;
if (!accepts || accepts.length === 0) {
reg = /.(jpg|jpeg|png|gif|webp)$/i;
} else {
const newTypes = accepts.join('|');
reg = new RegExp('\\.(' + newTypes + ')$', 'i');
}
return reg.test(file.name);
}
export function checkImgType(file: File) {
return isImgTypeByName(file.name);
}
export function isImgTypeByName(name: string) {
return /\.(jpg|jpeg|png|gif|webp)$/i.test(name);
}

View File

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

View File

@ -0,0 +1,6 @@
export enum UploadResultStatus {
DONE = 'done',
ERROR = 'error',
SUCCESS = 'success',
UPLOADING = 'uploading',
}

View File

@ -0,0 +1,61 @@
import type { Ref } from 'vue';
import { computed, unref } from 'vue';
import { $t } from '@vben/locales';
export function useUploadType({
acceptRef,
helpTextRef,
maxNumberRef,
maxSizeRef,
}: {
acceptRef: Ref<string[]>;
helpTextRef: Ref<string>;
maxNumberRef: Ref<number>;
maxSizeRef: Ref<number>;
}) {
// 文件类型限制
const getAccept = computed(() => {
const accept = unref(acceptRef);
if (accept && accept.length > 0) {
return accept;
}
return [];
});
const getStringAccept = computed(() => {
return unref(getAccept)
.map((item) => {
return item.indexOf('/') > 0 || item.startsWith('.')
? item
: `.${item}`;
})
.join(',');
});
// 支持jpg、jpeg、png格式不超过2M最多可选择10张图片
const getHelpText = computed(() => {
const helpText = unref(helpTextRef);
if (helpText) {
return helpText;
}
const helpTexts: string[] = [];
const accept = unref(acceptRef);
if (accept.length > 0) {
helpTexts.push($t('ui.upload.accept', [accept.join(',')]));
}
const maxSize = unref(maxSizeRef);
if (maxSize) {
helpTexts.push($t('ui.upload.maxSize', [maxSize]));
}
const maxNumber = unref(maxNumberRef);
if (maxNumber && maxNumber !== Infinity) {
helpTexts.push($t('ui.upload.maxNumber', [maxNumber]));
}
return helpTexts.join('');
});
return { getAccept, getStringAccept, getHelpText };
}

View File

@ -67,5 +67,6 @@ export {
X,
Download,
Upload,
CloudUpload,
History,
} from 'lucide-vue-next';

View File

@ -106,5 +106,14 @@
"backToLogin": "Back to login",
"entry": "Enter the system"
}
},
"upload": {
"upload": "Upload",
"accept": "Support {0} format",
"acceptUpload": "Only upload files in {0} format",
"maxSize": "A single file does not exceed {0}MB",
"maxSizeMultiple": "Only upload files up to {0}MB!",
"maxNumber": "Only upload up to {0} files",
"uploadSuccess": "Upload successfully"
}
}

View File

@ -106,5 +106,14 @@
"backToLogin": "返回登录",
"entry": "进入系统"
}
},
"upload": {
"upload": "上传",
"accept": "支持{0}格式",
"acceptUpload": "只能上传{0}格式文件",
"maxSize": "单个文件不超过{0}MB",
"maxSizeMultiple": "只能上传不超过{0}MB的文件!",
"maxNumber": "最多只能上传{0}个文件",
"uploadSuccess": "上传成功"
}
}