diff --git a/packages/uni-api/src/helpers/api/index.ts b/packages/uni-api/src/helpers/api/index.ts index 14ea1407d8a4496d979766586850d97120753d44..b26783860ceb94304eeeefd361a5ab0e15ff93fe 100644 --- a/packages/uni-api/src/helpers/api/index.ts +++ b/packages/uni-api/src/helpers/api/index.ts @@ -189,7 +189,7 @@ export function defineOffApi( export function defineTaskApi>( name: string, fn: ( - args: Omit, + args: Omit, res: { resolve: (res?: AsyncApiRes

) => void reject: (err?: string) => void diff --git a/packages/uni-api/src/protocols/media/chooseFile.ts b/packages/uni-api/src/protocols/media/chooseFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..135607f295b9a6754ecfcbab02e09a0ab69f9e81 --- /dev/null +++ b/packages/uni-api/src/protocols/media/chooseFile.ts @@ -0,0 +1,42 @@ +import { + CHOOSE_SOURCE_TYPES, + elemsInArray, + elemInArray, +} from '../../helpers/protocol' +export const API_CHOOSE_FILE = 'chooseFile' +export type API_TYPE_CHOOSE_FILE = typeof uni.chooseFile +export type API_TYPE_CHOOSE_FILE_OPTIONS = AsyncApiOptions +const CHOOSE_MEDIA_TYPE: API_TYPE_CHOOSE_FILE_OPTIONS['type'][] = [ + 'all', + 'image', + 'video', +] + +export const ChooseFileOptions: ApiOptions = { + formatArgs: { + count(count, params) { + if (count! <= 0) { + params.count = 100 + } + }, + sourceType(sourceType, params) { + params.sourceType = elemsInArray(sourceType, CHOOSE_SOURCE_TYPES) + }, + type(type, params) { + params.type = elemInArray(type, CHOOSE_MEDIA_TYPE) + }, + extension(extension, params) { + if (extension instanceof Array && extension.length === 0) { + return 'param extension should not be empty.' + } + if (!extension) params.extension = [''] + }, + }, +} + +export const ChooseFileProtocol: ApiProtocol = { + count: Number, + sourceType: Array, + type: String as any, + extension: Array, +} diff --git a/packages/uni-h5/src/service/api/media/MIMEType.ts b/packages/uni-h5/src/service/api/media/MIMEType.ts new file mode 100644 index 0000000000000000000000000000000000000000..c998a41df91a5da90a76938d0c1f5b69ce8347ab --- /dev/null +++ b/packages/uni-h5/src/service/api/media/MIMEType.ts @@ -0,0 +1,58 @@ +const MIMEType: { + image: Record + video: Record +} = { + /** + * 关于图片常见的MIME类型 + */ + image: { + jpg: 'jpeg', + jpe: 'jpeg', + pbm: 'x-portable-bitmap', + pgm: 'x-portable-graymap', + pnm: 'x-portable-anymap', + ppm: 'x-portable-pixmap', + psd: 'vnd.adobe.photoshop', + pic: 'x-pict', + rgb: 'x-rgb', + svg: 'svg+xml', + svgz: 'svg+xml', + tif: 'tiff', + xif: 'vnd.xiff', + wbmp: 'vnd.wap.wbmp', + wdp: 'vnd.ms-photo', + xbm: 'x-xbitmap', + ico: 'x-icon', + }, + /** + * 关于视频常见的MIME类型 + */ + video: { + '3g2': '3gpp2', + '3gp': '3gpp', + avi: 'x-msvideo', + f4v: 'x-f4v', + flv: 'x-flv', + jpgm: 'jpm', + jpgv: 'jpeg', + m1v: 'mpeg', + m2v: 'mpeg', + mpe: 'mpeg', + mpg: 'mpeg', + mpg4: 'mpeg', + m4v: 'x-m4v', + mkv: 'x-matroska', + mov: 'quicktime', + qt: 'quicktime', + movie: 'x-sgi-movie', + mp4v: 'mp4', + ogv: 'ogg', + smv: 'x-smv', + wm: 'x-ms-wm', + wmv: 'x-ms-wmv', + wmx: 'x-ms-wmx', + wvx: 'x-ms-wvx', + }, +} + +export default MIMEType diff --git a/packages/uni-h5/src/service/api/media/chooseFile.ts b/packages/uni-h5/src/service/api/media/chooseFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8964ec9c78e04eb176c87500e57ce1b496d69c6 --- /dev/null +++ b/packages/uni-h5/src/service/api/media/chooseFile.ts @@ -0,0 +1,82 @@ +//#region imp functions +import { + API_CHOOSE_FILE, + ChooseFileOptions, + ChooseFileProtocol, + defineAsyncApi, +} from '@dcloudio/uni-api' +import { fileToUrl } from '../../../helpers/file' +import _createInput from './createInput' +//#endregion + +//#region types +import type { API_TYPE_CHOOSE_FILE } from '@dcloudio/uni-api' +type TempFile = UniApp.ChooseFileSuccessCallbackResultFile +//#endregion + +let fileInput: HTMLInputElement = null as any + +export const chooseFile = defineAsyncApi( + API_CHOOSE_FILE, + ( + { + // sizeType, + count, + sourceType, + type, + extension, + }, + { resolve, reject } + ) => { + // TODO handle sizeType 尝试通过 canvas 压缩 + if (fileInput) { + document.body.removeChild(fileInput) + fileInput = null as any + } + + fileInput = _createInput({ + count, + sourceType, + type, + extension, + }) + document.body.appendChild(fileInput) + + fileInput.addEventListener('change', function (event: Event) { + const eventTarget = event.target as HTMLInputElement + + const tempFiles: TempFile[] = [] + + if (eventTarget && eventTarget.files) { + const fileCount = eventTarget.files.length + for (let i = 0; i < fileCount; i++) { + const file = eventTarget.files[i] + let filePath: string + Object.defineProperty(file, 'path', { + get() { + filePath = filePath || fileToUrl(file) + return filePath + }, + }) + if (i < count!) tempFiles.push(file as any) + } + } + + const res = { + errMsg: 'chooseFile:ok', + get tempFilePaths() { + return tempFiles.map(({ path }) => path) + }, + tempFiles: tempFiles, + } + + resolve(res) + + // TODO 用户取消选择时,触发 fail,目前尚未找到合适的方法。 + }) + + fileInput.click() + }, + ChooseFileProtocol, + ChooseFileOptions +) diff --git a/packages/uni-h5/src/service/api/media/createInput.ts b/packages/uni-h5/src/service/api/media/createInput.ts new file mode 100644 index 0000000000000000000000000000000000000000..932af17e1a9005833a13647e6817def456116557 --- /dev/null +++ b/packages/uni-h5/src/service/api/media/createInput.ts @@ -0,0 +1,71 @@ +import { updateElementStyle } from '@dcloudio/uni-shared' +import MIMEType from './MIMEType' + +export type createInputOptions = Pick< + UniApp.ChooseFileOptions, + 'count' | 'sourceType' | 'type' | 'extension' +> + +const ALL = 'all' + +function isWXEnv(): boolean { + const ua = window.navigator.userAgent.toLowerCase() + const matchUA = ua.match(/MicroMessenger/i) + return !!(matchUA && matchUA[0] === 'micromessenger') +} + +export default function ({ + count, + sourceType, + type, + extension, +}: createInputOptions): HTMLInputElement { + const inputEl = document.createElement('input') + inputEl.type = 'file' + + updateElementStyle(inputEl, { + position: 'absolute', + visibility: 'hidden', + zIndex: '-999', + width: '0', + height: '0', + top: '0', + left: '0', + }) + + /** + * 选择文件 + * chooseFile 使用后缀名 + * chooseImage、chooseVideo 使用MIME类型 + */ + inputEl.accept = extension! + .map((item) => { + if (type !== ALL) { + const MIMEKey = item.replace('.', '') + return `${type}/${MIMEType[type!][MIMEKey] || MIMEKey}` + } else { + // 在微信环境里,'.jpeg,.png' 会提示没有应用可执行此操作 + if (isWXEnv()) { + return '.' + } + return item.indexOf('.') === 0 ? item : `.${item}` + } + }) + .join(',') + + if (count && count > 1) { + inputEl.multiple = true + } + + // 经过测试,仅能限制只通过相机拍摄,不能限制只允许从相册选择。 + if ( + type !== ALL && + sourceType instanceof Array && + sourceType.length === 1 && + sourceType[0] === 'camera' + ) { + inputEl.setAttribute('capture', 'camera') + } + + return inputEl +}