import { Platform } from 'react-native'; import { PhoneOS } from './enum'; import { SetStateAction, useRef } from 'react'; import { useMemoizedFn, useUpdate } from 'ahooks'; import { isPromise } from '../../core/helpers'; import mime from 'mime'; import { PlatformOS } from '../../utils'; import { UploadFileTypes, UploadTypeDetectMap, UploadTypeLabel } from './enum'; import { ImageInfo } from './interface'; // 安卓文件类型会为null,根据uri(android不准确,应取用path)来拼接下,path也不一定有(IOS无) export const formatPhotoType = (type: any, uri: string, path?: string) => { if (type) { return type; } const str = Platform.OS === PhoneOS.Ios ? uri : path || ''; const fileExt = getFileExtension(str); if (fileExt && ['jpg', 'jpeg', 'png'].includes(fileExt)) { return `image/${fileExt}`; } return null; }; type Options = { value?: T; defaultValue: T; onChange?: (v: T) => void; }; export function usePropsValue(options: Options) { const { value, defaultValue, onChange } = options; const update = useUpdate(); const stateRef = useRef(value !== undefined ? value : defaultValue); if (value !== undefined) { stateRef.current = value; } const setState = useMemoizedFn( (v: SetStateAction, forceTrigger = false) => { // `forceTrigger` means trigger `onChange` even if `v` is the same as `stateRef.current` const nextValue = typeof v === 'function' ? (v as (prevState: T) => T)(stateRef.current) : v; if (!forceTrigger && nextValue === stateRef.current) return; stateRef.current = nextValue; update(); return onChange?.(nextValue); } ); return [stateRef.current, setState] as const; } /** * 将结果情况统一为promise形式 * @param result 转换的结果 */ export async function waitResult(result: T) { return new Promise((resolve) => { if (isPromise(result)) { result .then((res) => { resolve(res ?? true); }) .catch(() => { resolve(false); }); } else { resolve(result); } }); } /** * 获取对应type的mime */ export function getMimeType(type?: string) { // 获取不到对应的mime直接返回type return mime.getType(type || '') || type; } /** * 文件路径处理 * 目前主要是针对鸿蒙的文件路径进行处理 * @param filePath 文件 */ export function filePathOS(filePath: string) { if (PlatformOS.isHarmony) { return filePath?.replace?.('file://', ''); } return filePath; } /** * 文件路径展示处理 * 目前主要是针对鸿蒙的文件路径进行处理 * @param filePath */ export function filePathOSDisplay(filePath: string) { if (PlatformOS.isHarmony && filePath?.startsWith('/')) { return `file://${filePath}`; } return filePath; } // 统一获取扩展名(不含点) export function getFileExtension(str?: string) { const s = (str || '').toLowerCase(); if (!s.includes('.')) return ''; return s.split('.').pop() || ''; } // 从文件名或 uri 获取扩展名 export function getExtFromFile(file: ImageInfo) { return getFileExtension(file?.fileName) || getFileExtension(file?.uri); } // 判断单个文件是否符合某一 UploadFileTypes 规则 export function isFileMatchType(file: ImageInfo, type: UploadFileTypes) { const rule = UploadTypeDetectMap[type] || {}; const fType = (file?.type || '').toLowerCase(); const ext = getExtFromFile(file); // 精确匹配 if (rule.mimes?.length && fType && rule.mimes.includes(fType)) return true; // MIME 前缀匹配(image/*, audio/*, video/* 等) if (rule.mimePrefix?.length && fType) { const ok = rule.mimePrefix.some((prefix) => fType.startsWith(prefix)); if (ok) return true; } // ios UTI 匹配 if (rule.utis?.length && fType && rule.utis.includes(fType)) return true; // 扩展名兜底 if (rule.extensions?.length && ext && rule.extensions.includes(ext)) return true; return false; } const getAcceptTokens = (accept?: string) => (accept || '') .split(',') .map((token) => token.trim().toLowerCase()) .filter(Boolean); const isAllAcceptToken = (token: string) => /^\*(\/\*)?$/.test(token); const mimeTypeAliases: Record = { 'image/jpg': 'image/jpeg', 'image/pjpeg': 'image/jpeg', 'image/x-png': 'image/png', }; const normalizeMimeType = (mimeType: string) => mimeTypeAliases[mimeType] || mimeType; const JPG_JPEG_ALIAS = ['jpg', 'jpeg']; const normalizeAcceptExtension = (extension: string) => { const extensionWithoutDot = extension.replace(/^\./, ''); return JPG_JPEG_ALIAS.includes(extensionWithoutDot) ? JPG_JPEG_ALIAS.map((item) => `.${item}`) : [`.${extensionWithoutDot}`]; }; const getExtensionsFromAcceptToken = (token: string): string[] | null => { if (token.startsWith('.') && token.length > 1) { return normalizeAcceptExtension(token); } if (token.endsWith('/*')) return null; if (!/^[^/]+\/[^/]+$/.test(token)) return []; const extension = mime.getExtension(normalizeMimeType(token)); return extension ? normalizeAcceptExtension(extension) : null; }; /** * 将 accept 转换为原生选择器可识别的 MIME 类型。 * 无限制或无法识别的配置返回空数组,由调用方沿用原选择器类型。 */ export function getAcceptMimeTypes(accept?: string) { const acceptTokens = getAcceptTokens(accept); const mimeTypes: string[] = []; for (const token of acceptTokens) { if (isAllAcceptToken(token)) return []; if (token.startsWith('.')) { const mimeType = getMimeType(`file${token}`); if (!mimeType || !/^[^/]+\/[^/]+$/.test(mimeType)) return []; mimeTypes.push(normalizeMimeType(mimeType.toLowerCase())); continue; } if (/^[^/]+\/[^/]+$/.test(token)) { mimeTypes.push(normalizeMimeType(token)); } } return [...new Set(mimeTypes)]; } const getAcceptExtensions = (accept?: string) => { const acceptTokens = getAcceptTokens(accept); if (acceptTokens.some(isAllAcceptToken)) return []; const extensionGroups = acceptTokens.map(getExtensionsFromAcceptToken); if (extensionGroups.some((extensions) => extensions === null)) return []; return [ ...new Set(extensionGroups.flatMap((extensions) => extensions || [])), ]; }; export function getAcceptDocumentTypes( accept: string | undefined, platform: string ) { if (platform === PhoneOS.Android) return getAcceptMimeTypes(accept); if (platform === PhoneOS.Harmony) return getAcceptExtensions(accept); return []; } const isAcceptTokenMatch = ( token: string, extension: string, mimeType: string ) => { if (token.startsWith('.')) { const acceptedExtensions = JPG_JPEG_ALIAS.includes(token.slice(1)) ? JPG_JPEG_ALIAS : [token.slice(1)]; return acceptedExtensions.includes(extension); } if (token.endsWith('/*')) { return mimeType.split('/')[0] === token.slice(0, -2); } if (/^[^/]+\/[^/]+$/.test(token)) { return mimeType === normalizeMimeType(token); } return false; }; /** * 根据 accept 配置的扩展名、完整 MIME 类型和 MIME 通配符判断文件是否匹配。 */ export function isFileMatchAccept(file: ImageInfo, accept?: string) { const acceptTokens = getAcceptTokens(accept); if (acceptTokens.length === 0) return true; if (acceptTokens.some(isAllAcceptToken)) return true; const extension = getExtFromFile(file); const declaredMime = (file.type || '').toLowerCase(); const inferredMime = ( extension ? getMimeType(`file.${extension}`) || '' : '' ).toLowerCase(); const mimeType = normalizeMimeType( declaredMime.includes('/') ? declaredMime : inferredMime ); if ( extension && declaredMime.includes('/') && inferredMime.includes('/') && normalizeMimeType(declaredMime) !== normalizeMimeType(inferredMime) ) { return false; } return acceptTokens.some((token) => isAcceptTokenMatch(token, extension, mimeType) ); } export function validateFilesByAccept( files: ImageInfo[], accept?: string ): { ok: boolean; allowedLabel: string } { const acceptTokens = getAcceptTokens(accept); if (acceptTokens.length === 0) { return { ok: true, allowedLabel: '' }; } return { ok: files?.length ? files.every((file) => isFileMatchAccept(file, accept)) : true, allowedLabel: accept || '', }; } // 校验一组文件是否都在允许的类型集合中;返回是否通过与拼接的允许类型标签 export function validateFilesByTypes( files: ImageInfo[], allowedTypes?: UploadFileTypes[] ): { ok: boolean; allowedLabel: string } { if ( !allowedTypes || !Array.isArray(allowedTypes) || allowedTypes.includes(UploadFileTypes.AllFile) ) { return { ok: true, allowedLabel: '' }; } const ok = files?.length ? files.every((f) => allowedTypes.some((t) => isFileMatchType(f, t))) : true; const allowedLabel = allowedTypes .filter((t) => t !== UploadFileTypes.AllFile) .map((t) => UploadTypeLabel[t] || '') .filter(Boolean) .join(', '); return { ok, allowedLabel }; }