<

{{title}}

{{file.summary.functions.pct}}%

Functions
{{file.summary.functions.covered}}/{{file.summary.functions.total}}

{{file.summary.branches.pct}}%

Branches
{{file.summary.branches.covered}}/{{file.summary.branches.total}}

{{file.summary.lines.pct}}%

Lines
{{file.summary.lines.covered}}/{{file.summary.lines.total}}
          
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
                
import picker from '@ohos.file.picker'; 
import { photoAccessHelper } from '@kit.MediaLibraryKit'; 
import camera from '@ohos.multimedia.cameraPicker'; 
import common from '@ohos.app.ability.common'; 
import fileIo from '@ohos.file.fs'; 
import fileUri from '@ohos.file.fileuri'; 
 
/** 
 * 处理 `<input type=file>` 触发的文件选择请求。 
 * 
 * 根据 H5 `accept` / `capture` / `multiple` 属性,自动选择系统相机、图库、 
 * 或文档选择器,并返回选中文件的 URI 列表交还 ArkWeb。 
 */ 
export class FileChooserHandler { 
  /** 
   * ArkWeb 把 H5 `accept="image/*"` 在原生层展开后的扩展名清单。 
   * 用于在仅拿到扩展名(如 `.jpg`)时反查媒体大类。 
   */ 
  private static readonly IMAGE_EXTS: ReadonlySet<string> = new Set<string>([ 
    '.tif', '.xbm', '.tiff', '.pjp', '.jfif', '.bmp', '.avif', '.apng', '.ico', 
    '.webp', '.svg', '.gif', '.svgz', '.jpg', '.jpeg', '.png', '.pjpeg' 
  ]); 
  /** 
   * ArkWeb 把 H5 `accept="video/*"` 在原生层展开后的扩展名清单。 
   */ 
  private static readonly VIDEO_EXTS: ReadonlySet<string> = new Set<string>([ 
    '.mp4', '.mpg', '.mpeg', '.m4v', '.ogm', '.ogv', '.webm' 
  ]); 
 
  /** 
   * 入口:根据 ArkWeb 传入的文件选择参数选择合适的系统选择器。 
   * 
   * @param context UIAbilityContext,用于拉起系统应用 
   * @param acceptTypes `getAcceptType()` 返回的扩展名/MIME 数组 
   * @param isCapture 是否要求即时拍摄(对应 H5 `capture` 属性) 
   * @param mode 单选 / 多选(对应 H5 `multiple` 属性) 
   * @returns 选中文件的 URI 列表;用户取消或失败均返回 `[]` 
   */ 
  public static async pick( 
    context: common.UIAbilityContext, 
    acceptTypes: string[], 
    isCapture: boolean, 
    mode: 'single' | 'multiple' 
  ): Promise<string[]> { 
    const hasImage: boolean = FileChooserHandler.hasCategory(acceptTypes, 'image'); 
    const hasVideo: boolean = FileChooserHandler.hasCategory(acceptTypes, 'video'); 
 
    if (isCapture) { 
      return await FileChooserHandler.captureFromCamera(context, hasImage, hasVideo); 
    } 
    if (hasImage || hasVideo) { 
      return await FileChooserHandler.pickPhoto(mode, hasImage, hasVideo); 
    } 
    return await FileChooserHandler.pickDocument(mode, acceptTypes); 
  } 
 
  /** 
   * 拉起系统相机进行拍照或录像。 
   * 
   * `accept` 仅含 image → 拍照;仅含 video → 录像;两者皆有/皆无 → 由系统 picker 让用户选择。 
   * 录像需预先在沙箱内创建 saveUri 文件,否则 5.0.x 上 cameraPicker 会回退到拍照模式。 
   */ 
  private static async captureFromCamera( 
    context: common.UIAbilityContext, 
    hasImage: boolean, 
    hasVideo: boolean 
  ): Promise<string[]> { 
    try { 
      const isVideoOnly: boolean = hasVideo && !hasImage; 
      const isPhotoOnly: boolean = hasImage && !hasVideo; 
      const mediaTypes: camera.PickerMediaType[] = isVideoOnly 
        ? [camera.PickerMediaType.VIDEO] 
        : isPhotoOnly 
          ? [camera.PickerMediaType.PHOTO] 
          : [camera.PickerMediaType.PHOTO, camera.PickerMediaType.VIDEO]; 
 
      const ext: string = isVideoOnly ? '.mp4' : '.jpg'; 
      const filePath: string = context.filesDir + '/qian_' + Date.now() + ext; 
      fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE); 
      const saveUri: string = fileUri.getUriFromPath(filePath); 
 
      const profile: ESObject = { 
        cameraPosition: 1, 
        saveUri: saveUri 
      }; 
      if (isVideoOnly) { 
        // videoDuration 单位秒,0 表示不限时长 
        profile.videoDuration = 0; 
      } 
 
      const result: camera.PickerResult = await camera.pick( 
        context, 
        mediaTypes, 
        profile as camera.PickerProfile 
      ); 
      if (result === null || result === undefined || result.resultCode !== 0) { 
        return []; 
      } 
      const uri: string = result.resultUri; 
      return uri !== null && uri !== undefined && uri.length > 0 ? [uri] : []; 
    } catch (_e) { 
      return []; 
    } 
  } 
 
  /** 拉起系统图库选择器(图片 / 视频 / 混合)。 */ 
  private static async pickPhoto( 
    mode: 'single' | 'multiple', 
    hasImage: boolean, 
    hasVideo: boolean 
  ): Promise<string[]> { 
    try { 
      const photoPicker = new photoAccessHelper.PhotoViewPicker(); 
      const options: photoAccessHelper.PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); 
      options.MIMEType = FileChooserHandler.resolvePhotoMimeType(hasImage, hasVideo); 
      options.maxSelectNumber = mode === 'multiple' ? 9 : 1; 
      const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options); 
      const uris: string[] = result.photoUris; 
      return uris !== null && uris !== undefined ? uris : []; 
    } catch (_e) { 
      return []; 
    } 
  } 
 
  /** 根据 hasImage / hasVideo 选择图库 MIMEType。 */ 
  private static resolvePhotoMimeType( 
    hasImage: boolean, 
    hasVideo: boolean 
  ): photoAccessHelper.PhotoViewMIMETypes { 
    if (hasImage && hasVideo) { 
      return photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE; 
    } 
    return hasVideo 
      ? photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE 
      : photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; 
  } 
 
  /** 拉起文档选择器,按 accept 限定可选后缀。 */ 
  private static async pickDocument( 
    mode: 'single' | 'multiple', 
    acceptTypes: string[] 
  ): Promise<string[]> { 
    try { 
      const documentPicker = new picker.DocumentViewPicker(); 
      const options: picker.DocumentSelectOptions = new picker.DocumentSelectOptions(); 
      options.maxSelectNumber = mode === 'multiple' ? 9 : 1; 
      const filters: string[] = FileChooserHandler.acceptTypesToSuffixFilters(acceptTypes); 
      if (filters.length > 0) { 
        (options as ESObject).fileSuffixFilters = filters; 
      } 
      const uris: string[] = await documentPicker.select(options); 
      return uris !== null && uris !== undefined ? uris : []; 
    } catch (_e) { 
      return []; 
    } 
  } 
 
  /** 
   * 判断 accept 是否覆盖某个媒体大类。 
   * 
   * `getAcceptType()` 返回的元素既可能是 MIME(`image/png`、`video/*`),也可能是 
   * 扩展名(`.jpg`、`.mp4`)——ArkWeb 会把通配 MIME 展开成扩展名清单。两种格式同时识别。 
   */ 
  private static hasCategory( 
    acceptTypes: string[], 
    category: 'image' | 'video' 
  ): boolean { 
    const whitelist: ReadonlySet<string> = category === 'image' 
      ? FileChooserHandler.IMAGE_EXTS 
      : FileChooserHandler.VIDEO_EXTS; 
    for (const raw of FileChooserHandler.flatten(acceptTypes)) { 
      if (raw === category || raw.indexOf(category + '/') === 0) { 
        return true; 
      } 
      if (raw.charAt(0) === '.' && whitelist.has(raw)) { 
        return true; 
      } 
    } 
    return false; 
  } 
 
  /** 规范化 accept 数组:拆逗号、trim、转小写、去空,得到扁平的 token 列表。 */ 
  private static flatten(values: string[]): string[] { 
    if (values === null || values === undefined || values.length === 0) { 
      return []; 
    } 
    const out: string[] = []; 
    for (const raw of values) { 
      if (raw === null || raw === undefined) { 
        continue; 
      } 
      const parts: string[] = raw.split(','); 
      for (const p of parts) { 
        const t: string = p.trim().toLowerCase(); 
        if (t.length > 0) { 
          out.push(t); 
        } 
      } 
    } 
    return out; 
  } 
 
  /** 
   * 把 accept 列表转换为文档选择器的后缀过滤数组。 
   * 含通配(`* / *`、`image/*`)或未识别 MIME 时返回空数组,表示不过滤。 
   */ 
  private static acceptTypesToSuffixFilters(acceptTypes: string[]): string[] { 
    if (acceptTypes === null || acceptTypes === undefined || acceptTypes.length === 0) { 
      return []; 
    } 
    const filters: string[] = []; 
    const seen: Set<string> = new Set<string>(); 
    for (const raw of acceptTypes) { 
      const t: string = raw.trim().toLowerCase(); 
      if (t.length === 0) { 
        continue; 
      } 
      if (t.indexOf('/*') >= 0 || t === '*' || t === '*/*') { 
        return []; 
      } 
      if (t.charAt(0) === '.') { 
        const filterStr: string = FileChooserHandler.suffixToFilter(t); 
        if (!seen.has(filterStr)) { 
          seen.add(filterStr); 
          filters.push(filterStr); 
        } 
        continue; 
      } 
      const ext: string = FileChooserHandler.mimeToSuffix(t); 
      if (ext.length === 0) { 
        return []; 
      } 
      const filterStr: string = FileChooserHandler.suffixToFilter(ext); 
      if (!seen.has(filterStr)) { 
        seen.add(filterStr); 
        filters.push(filterStr); 
      } 
    } 
    return filters; 
  } 
 
  /** 后缀转 picker filter 字符串。 */ 
  private static suffixToFilter(suffix: string): string { 
    const ext: string = suffix.charAt(0) === '.' ? suffix : ('.' + suffix); 
    const upper: string = ext.substring(1).toUpperCase(); 
    return upper + '文件|' + ext; 
  } 
 
  /** 常见 MIME 到文件后缀的映射。 */ 
  private static mimeToSuffix(mime: string): string { 
    switch (mime) { 
      case 'application/pdf': 
        return '.pdf'; 
      case 'image/png': 
        return '.png'; 
      case 'image/jpeg': 
      case 'image/jpg': 
        return '.jpg'; 
      case 'image/gif': 
        return '.gif'; 
      case 'image/bmp': 
        return '.bmp'; 
      case 'image/webp': 
        return '.webp'; 
      case 'application/msword': 
        return '.doc'; 
      case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 
        return '.docx'; 
      case 'application/vnd.ms-excel': 
        return '.xls'; 
      case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 
        return '.xlsx'; 
      case 'application/vnd.ms-powerpoint': 
        return '.ppt'; 
      case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 
        return '.pptx'; 
      case 'text/plain': 
        return '.txt'; 
      case 'application/zip': 
        return '.zip'; 
      default: 
        return ''; 
    } 
  } 
 
  private constructor() {} 
} 

              
Code coverage generated by bjc at {{date}}