feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const MAX_IMAGE_SIZE = 1024 * 1024 * 5;
|
||||
export const MAX_FILE_SIZE = 1024 * 1024 * 20;
|
||||
@@ -0,0 +1,36 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.container {
|
||||
&.hide-upload-area {
|
||||
:global {
|
||||
.semi-upload-drag-area {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-upload-drag-area {
|
||||
.semi-upload-drag-area-sub-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-upload-file-card-preview-placeholder {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.semi-upload.has-error .semi-upload-drag-area {
|
||||
border: 1px solid var(--Light-usage-danger---color-danger, #FF441E);
|
||||
}
|
||||
|
||||
.semi-upload-file-card-info-main {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.upload-error-text {
|
||||
color: var(--Light-usage-danger---color-danger, #FF441E);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ClickAwayListener from 'react-click-away-listener';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useUpdateEffect, useMemoizedFn } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Upload, Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { FileIcon, FileItemStatus } from '../file-icon';
|
||||
import { typeSafeJSONParse } from '../../utils';
|
||||
import { formatBytes } from './utils';
|
||||
import { useUpload } from './use-upload';
|
||||
import { type FileItem } from './types';
|
||||
|
||||
import styles from './file-upload.module.less';
|
||||
|
||||
export interface FileUploadProps {
|
||||
value: string;
|
||||
accept: string;
|
||||
multiple: boolean;
|
||||
disabled?: boolean;
|
||||
fileType: 'object' | 'image';
|
||||
validateStatus?: string;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
onChange?: (v?: string) => void;
|
||||
}
|
||||
|
||||
const TEST_RUN_FILE_NAME_KEY = 'x-wf-file_name';
|
||||
|
||||
const getFormatFileUrl = (curFile: FileItem) => {
|
||||
const originUrl = curFile?.url ?? '';
|
||||
const fileName = curFile?.name ?? '';
|
||||
try {
|
||||
const urlObj = new URL(originUrl);
|
||||
const params = new URLSearchParams(urlObj.search);
|
||||
|
||||
if (params.has(TEST_RUN_FILE_NAME_KEY)) {
|
||||
params.set(TEST_RUN_FILE_NAME_KEY, fileName);
|
||||
} else {
|
||||
params.append(TEST_RUN_FILE_NAME_KEY, fileName);
|
||||
}
|
||||
|
||||
urlObj.search = params.toString();
|
||||
|
||||
return urlObj.toString();
|
||||
} catch (e) {
|
||||
return originUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const getParsedFileInfo = (formatUrl: string) => {
|
||||
const url = new URL(formatUrl);
|
||||
const params = new URLSearchParams(url.search);
|
||||
const fileName =
|
||||
params.get(TEST_RUN_FILE_NAME_KEY) ?? I18n.t('plugin_file_unknown');
|
||||
|
||||
return {
|
||||
url: formatUrl,
|
||||
uid: nanoid(),
|
||||
name: fileName,
|
||||
};
|
||||
};
|
||||
|
||||
const getInitialValue = (val: string, multiple: boolean): FileItem[] => {
|
||||
if (multiple) {
|
||||
const multipleVal = typeSafeJSONParse(val);
|
||||
if (Array.isArray(multipleVal)) {
|
||||
return multipleVal.map(url => getParsedFileInfo(url)) as FileItem[];
|
||||
}
|
||||
}
|
||||
if (val) {
|
||||
return [getParsedFileInfo(val)] as FileItem[];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const FileUpload: React.FC<FileUploadProps> = props => {
|
||||
const {
|
||||
validateStatus,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
accept,
|
||||
multiple,
|
||||
disabled,
|
||||
fileType,
|
||||
} = props;
|
||||
|
||||
const focusRef = useRef(false);
|
||||
|
||||
const maxFileCount = multiple ? 20 : 1;
|
||||
|
||||
const { upload, fileList, isUploading, deleteFile, setFileList } = useUpload({
|
||||
multiple,
|
||||
fileType,
|
||||
accept,
|
||||
maxFileCount,
|
||||
});
|
||||
|
||||
const handleFocus = () => {
|
||||
if (!focusRef.current) {
|
||||
focusRef.current = true;
|
||||
onFocus?.();
|
||||
}
|
||||
};
|
||||
const handleBlur = () => {
|
||||
if (focusRef.current) {
|
||||
focusRef.current = false;
|
||||
onBlur?.();
|
||||
}
|
||||
};
|
||||
const handleClickAway = () => {
|
||||
if (!isUploading) {
|
||||
handleBlur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async file => {
|
||||
const { fileInstance } = file;
|
||||
await upload(fileInstance);
|
||||
};
|
||||
|
||||
const getSubmitValue = useMemoizedFn((): string | undefined => {
|
||||
let newVal: string | undefined;
|
||||
if (multiple) {
|
||||
const next = fileList
|
||||
.filter(item => item.url)
|
||||
.map(item => getFormatFileUrl(item));
|
||||
newVal = next.length ? JSON.stringify(next) : undefined;
|
||||
} else {
|
||||
const singleFile = fileList?.[0];
|
||||
newVal = singleFile ? getFormatFileUrl(singleFile) : undefined;
|
||||
}
|
||||
return newVal;
|
||||
});
|
||||
|
||||
const handleChange = useMemoizedFn(val => onChange?.(val));
|
||||
|
||||
// 当fileList更新时,触发onChange
|
||||
useUpdateEffect(() => {
|
||||
const newVal = getSubmitValue();
|
||||
handleChange?.(newVal);
|
||||
}, [fileList]);
|
||||
|
||||
// 当表单值更新时,同步到fileList
|
||||
useEffect(() => {
|
||||
const val = getSubmitValue();
|
||||
if (val !== value) {
|
||||
setFileList(getInitialValue(value, multiple));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleAcceptInvalid = () => {
|
||||
Toast.error(
|
||||
I18n.t('imageflow_upload_error_type', {
|
||||
type: accept,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const semiFileList: any[] = fileList.map(file => ({
|
||||
name: file.name,
|
||||
size: file.size !== undefined ? formatBytes(file.size) : '',
|
||||
uid: file.uid || nanoid(),
|
||||
status: file.status || FileItemStatus.Success,
|
||||
url: file.uil,
|
||||
validateMessage: file.validateMessage,
|
||||
percent: file.percent,
|
||||
preview: true,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<div
|
||||
className={classNames(styles.container, {
|
||||
[styles['hide-upload-area']]: fileList.length >= maxFileCount,
|
||||
})}
|
||||
>
|
||||
<Upload
|
||||
disabled={disabled}
|
||||
action=""
|
||||
limit={maxFileCount}
|
||||
fileList={semiFileList}
|
||||
data-testid={props['data-testid']}
|
||||
className={validateStatus === 'error' ? 'has-error' : ''}
|
||||
customRequest={handleUpload}
|
||||
draggable={true}
|
||||
dragMainText={I18n.t('imageflow_upload_action_common')}
|
||||
dragSubText={I18n.t('imageflow_upload_type', {
|
||||
type: accept,
|
||||
})}
|
||||
multiple={multiple}
|
||||
accept={accept}
|
||||
onDrop={handleFocus}
|
||||
onOpenFileDialog={handleFocus}
|
||||
onAcceptInvalid={handleAcceptInvalid}
|
||||
previewFile={file => {
|
||||
const { uid } = file;
|
||||
const fileItem = fileList.find(item => item.uid === uid);
|
||||
if (fileItem) {
|
||||
return <FileIcon file={fileItem} size={36} />;
|
||||
}
|
||||
}}
|
||||
onRemove={(currentFile, list, currentFileItem) => {
|
||||
deleteFile(currentFileItem.uid);
|
||||
}}
|
||||
onClear={() => setFileList([])}
|
||||
/>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FileUpload, type FileUploadProps } from './file-upload';
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FileItemStatus } from '../file-icon';
|
||||
|
||||
export interface FileItem extends File {
|
||||
// 唯一标识
|
||||
uid?: string;
|
||||
// 文件地址
|
||||
url?: string;
|
||||
// 上传进度
|
||||
percent?: number;
|
||||
// 校验信息
|
||||
validateMessage?: string;
|
||||
status?: FileItemStatus;
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-lines-per-function */
|
||||
import { useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { workflowApi, type ViewVariableType } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { upLoadFile } from '@coze-arch/bot-utils';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { FileItemStatus } from '../file-icon';
|
||||
import { validate } from './utils';
|
||||
import { type FileItem } from './types';
|
||||
import { MAX_IMAGE_SIZE, MAX_FILE_SIZE } from './constants';
|
||||
|
||||
export interface UploadConfig {
|
||||
initialValue?: FileItem[];
|
||||
customValidate?: (file: FileItem) => Promise<string | undefined>;
|
||||
timeout?: number;
|
||||
fileType?: 'object' | 'image';
|
||||
multiple?: boolean;
|
||||
maxSize?: number;
|
||||
inputType?: ViewVariableType;
|
||||
accept?: string;
|
||||
maxFileCount?: number;
|
||||
}
|
||||
|
||||
export const useUpload = (props?: UploadConfig) => {
|
||||
const {
|
||||
initialValue = [],
|
||||
customValidate,
|
||||
timeout,
|
||||
fileType,
|
||||
multiple = true,
|
||||
maxSize,
|
||||
accept,
|
||||
maxFileCount = 20,
|
||||
} = props || {};
|
||||
const [fileList, setFileList] = useState(initialValue);
|
||||
const isUploading = fileList.some(
|
||||
file => file.status === FileItemStatus.Uploading,
|
||||
);
|
||||
|
||||
const updateFileItemProps = (uid, fileItemProps) => {
|
||||
setFileList(prevList => {
|
||||
const newList = [...prevList];
|
||||
const index = newList.findIndex(item => item.uid === uid);
|
||||
if (index !== -1) {
|
||||
Object.keys(fileItemProps).forEach(key => {
|
||||
newList[index][key] = fileItemProps[key];
|
||||
});
|
||||
}
|
||||
|
||||
return newList;
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFileWithProgress = async file => {
|
||||
let progressTimer;
|
||||
|
||||
try {
|
||||
const doUpload = async () =>
|
||||
await upLoadFile({
|
||||
biz: 'workflow',
|
||||
fileType,
|
||||
file,
|
||||
getProgress: percent => {
|
||||
updateFileItemProps(file.uid, {
|
||||
percent,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (timeout) {
|
||||
progressTimer = setTimeout(() => {
|
||||
throw new Error('Upload timed out');
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
const uri = await doUpload();
|
||||
|
||||
if (!uri) {
|
||||
throw new CustomError('normal_error', 'no uri');
|
||||
}
|
||||
|
||||
// 上传完成,清空超时计时器
|
||||
clearTimeout(progressTimer);
|
||||
|
||||
// 加签uri,获得url
|
||||
const { url } = await workflowApi.SignImageURL(
|
||||
{
|
||||
uri,
|
||||
},
|
||||
{
|
||||
__disableErrorToast: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!url) {
|
||||
throw new Error(I18n.t('imageflow_upload_error'));
|
||||
}
|
||||
|
||||
updateFileItemProps(file.uid, {
|
||||
url,
|
||||
status: FileItemStatus.Success,
|
||||
});
|
||||
|
||||
return url;
|
||||
} catch (error) {
|
||||
updateFileItemProps(file.uid, {
|
||||
validateMessage: error.message || 'upload failed',
|
||||
status: FileItemStatus.ValidateFail,
|
||||
});
|
||||
clearTimeout(progressTimer);
|
||||
}
|
||||
};
|
||||
|
||||
const validateFile = async (file: FileItem): Promise<string | undefined> => {
|
||||
const validateMsg = await validate(file, {
|
||||
customValidate,
|
||||
maxSize:
|
||||
(maxSize ?? fileType === 'image') ? MAX_IMAGE_SIZE : MAX_FILE_SIZE,
|
||||
accept,
|
||||
});
|
||||
if (validateMsg) {
|
||||
return validateMsg;
|
||||
}
|
||||
};
|
||||
|
||||
const upload = async (file: FileItem) => {
|
||||
file.status = FileItemStatus.Uploading;
|
||||
if (!file.uid) {
|
||||
file.uid = nanoid();
|
||||
}
|
||||
|
||||
const errorInfo = await validateFile(file);
|
||||
|
||||
if (errorInfo) {
|
||||
Toast.error(errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!multiple && fileList[0]) {
|
||||
setFileList([]);
|
||||
}
|
||||
|
||||
let canUpload = true;
|
||||
|
||||
setFileList(prevList => {
|
||||
if (prevList.length >= maxFileCount) {
|
||||
Toast.warning(I18n.t('plugin_file_max'));
|
||||
canUpload = false;
|
||||
return prevList;
|
||||
}
|
||||
return [...prevList, file];
|
||||
});
|
||||
|
||||
if (canUpload) {
|
||||
await uploadFileWithProgress(file);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFile = (uid?: string) => {
|
||||
const index = fileList.findIndex(item => uid === item.uid);
|
||||
|
||||
if (index !== -1 && uid) {
|
||||
setFileList(prevList => {
|
||||
const newList = [...prevList];
|
||||
newList.splice(index, 1);
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fileList,
|
||||
upload,
|
||||
isUploading,
|
||||
deleteFile,
|
||||
setFileList: _fileList => setFileList(_fileList),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mime from 'mime-types';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { getFileExtension } from '../file-icon';
|
||||
import { type FileItem } from './types';
|
||||
import { MAX_FILE_SIZE } from './constants';
|
||||
|
||||
interface UploadValidateRule {
|
||||
maxSize?: number;
|
||||
imageSize?: ImageSizeRule;
|
||||
accept?: string;
|
||||
customValidate?: (file: FileItem) => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param bytes 文件大小
|
||||
* @param decimals 小数位数, 默认 2 位
|
||||
* @example
|
||||
* formatBytes(1024); // 1KB
|
||||
* formatBytes('1024'); // 1KB
|
||||
* formatBytes(1234); // 1.21KB
|
||||
* formatBytes(1234, 3); // 1.205KB
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
const k = 1024,
|
||||
dm = decimals,
|
||||
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`;
|
||||
}
|
||||
|
||||
/** 文件大小校验 */
|
||||
export const sizeValidate = (
|
||||
size: number,
|
||||
maxSize: number = MAX_FILE_SIZE,
|
||||
): string | undefined => {
|
||||
if (maxSize && size > maxSize) {
|
||||
return I18n.t('imageflow_upload_exceed', {
|
||||
size: formatBytes(maxSize),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export interface ImageSizeRule {
|
||||
maxWidth?: number;
|
||||
minWidth?: number;
|
||||
maxHeight?: number;
|
||||
minHeight?: number;
|
||||
aspectRatio?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片的宽高
|
||||
*/
|
||||
export async function getImageSize(
|
||||
file: FileItem,
|
||||
): Promise<{ width: number; height: number }> {
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new window.Image();
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
};
|
||||
img.onerror = e => {
|
||||
reject(e);
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/** 图像宽高校验 */
|
||||
// eslint-disable-next-line complexity
|
||||
export const imageSizeValidate = async (
|
||||
file: FileItem,
|
||||
rule?: ImageSizeRule,
|
||||
): Promise<string | undefined> => {
|
||||
const { maxWidth, minWidth, maxHeight, minHeight, aspectRatio } = rule || {};
|
||||
|
||||
// 未定义时不校验
|
||||
if (isNil(maxWidth || minWidth || maxHeight || minHeight || aspectRatio)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = await getImageSize(file);
|
||||
|
||||
if (maxWidth && width > maxWidth) {
|
||||
return I18n.t('imageflow_upload_error5', {
|
||||
value: `${maxWidth}px`,
|
||||
});
|
||||
}
|
||||
|
||||
if (minWidth && width < minWidth) {
|
||||
return I18n.t('imageflow_upload_error3', {
|
||||
value: `${minWidth}px`,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxHeight && height > maxHeight) {
|
||||
return I18n.t('imageflow_upload_error4', {
|
||||
value: `${maxHeight}px`,
|
||||
});
|
||||
}
|
||||
|
||||
if (minHeight && height < minHeight) {
|
||||
return I18n.t('imageflow_upload_error2', {
|
||||
value: `${minHeight}px`,
|
||||
});
|
||||
}
|
||||
if (aspectRatio && width / height > aspectRatio) {
|
||||
return I18n.t('imageflow_upload_error1');
|
||||
}
|
||||
};
|
||||
|
||||
export const acceptValidate = (fileName: string, accept?: string) => {
|
||||
if (!accept) {
|
||||
return;
|
||||
}
|
||||
const acceptList = accept.split(',');
|
||||
|
||||
const fileExtension = getFileExtension(fileName);
|
||||
const mimeType = mime.lookup(fileExtension);
|
||||
|
||||
// image/* 匹配所有的图片类型
|
||||
if (acceptList.includes('image/*') && mimeType?.startsWith?.('image/')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!acceptList.includes(`.${fileExtension}`)) {
|
||||
return I18n.t('imageflow_upload_error_type', {
|
||||
type: `${acceptList.filter(Boolean).join('/')}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validate = async (file: FileItem, rules?: UploadValidateRule) => {
|
||||
const { size, name } = file;
|
||||
|
||||
const { maxSize, imageSize, accept, customValidate } = rules || {};
|
||||
|
||||
const validators = [
|
||||
async () => await customValidate?.(file),
|
||||
() => sizeValidate(size, maxSize),
|
||||
async () => await imageSizeValidate(file, imageSize),
|
||||
() => acceptValidate(name, accept),
|
||||
];
|
||||
|
||||
for await (const validator of validators) {
|
||||
const errorMsg = await validator();
|
||||
if (errorMsg) {
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user