import { image } from '@kit.ImageKit'
import fs from '@ohos.file.fs';
import { picker } from '@kit.CoreFileKit';
import { http } from '@kit.NetworkKit';
import { BusinessError, request } from '@kit.BasicServicesKit';

export class BaseHttpResponse<T> {
  code: number;
  msg: string;
  data: T;

  constructor(code: number, msg: string, data: T) {
    this.code = code;
    this.msg = msg;
    this.data = data;
  }
}

// 定义类型
interface UploadResponse {
  body: string
}

/**
 * 上传响应数据类型
 */
export interface UploadData {
  /**
   * 上传成功的图片-在线地址
   */
  url: string;

  /**
   * OSS文件ID
   */
  ossId: string;

  /**
   * 图片宽高比
   */
  aspectRatio: string;
}

/**
 * 原始响应数据结构
 */
interface RawUploadResponse {
  code: number;
  msg: string;
  data: UploadData;
}

/**
 * 选择的图片信息
 */
export interface SelectedImageInfo {
  /**
   * 唯一ID
   */
  id: string;

  /**
   * 原始图片URI
   */
  originalUri: string;

  /**
   * 临时文件路径
   */
  tempFilePath: string;

  /**
   * 文件名
   */
  fileName: string;

  /**
   * 文件大小(字节)
   */
  fileSize: number;

  /**
   * 选择时间戳
   */
  selectTime: number;
}

/**
 * 图片选择类型枚举
 */
export enum ImageSelectType {
  COVER = 'cover', // 封面图片
  MULTIPLE = 'multiple' // 多张图片
}

/**
 * 图片上传API类 - 专门处理接口请求
 */
export class ImageUploadAPI {
  private static readonly UPLOAD_URL = '';

  /**
   * 上传单张图片到服务器
   * @param context 页面上下文
   * @param files 文件信息数组
   * @param aspectRatio 宽高比
   * @returns Promise<BaseHttpResponse<UploadData>> 返回服务器响应
   */
  static async uploadImage(
    context: Context,
    files: Array<request.File>,
    aspectRatio: string = "4:3"
  ): Promise<BaseHttpResponse<UploadData>> {
    return new Promise((resolve, reject) => {
      request.uploadFile(context, {
        url: ImageUploadAPI.UPLOAD_URL,
        method: http.RequestMethod.POST,
        header: {
          contentType: 'multipart/form-data',
        },
        files,
        data: [
          {
            name: 'aspectRatio',
            value: aspectRatio
          }
        ]
      })
        .then((res) => {
          res.on('headerReceive', (value) => {
            try {
              const uploadRes = (value as UploadResponse)
              // 使用明确的类型进行JSON解析
              const responseData = JSON.parse(uploadRes.body) as RawUploadResponse

              // 使用BaseHttpResponse包装响应数据
              const response = new BaseHttpResponse<UploadData>(
                responseData.code,
                responseData.msg,
                responseData.data
              )

              resolve(response)
            } catch (parseError) {
              reject(new Error('响应解析失败'))
            }
          })
        })
        .catch((error: BusinessError) => {
          reject(error)
        })
    })
  }

  /**
   * 批量上传多张图片
   * @param context 页面上下文
   * @param filesList 文件信息数组列表
   * @param aspectRatio 宽高比
   * @returns Promise<BaseHttpResponse<UploadData>[]> 返回所有上传结果
   */
  static async uploadMultipleImages(
    context: Context,
    filesList: Array<request.File>[],
    aspectRatio: string = "4:3"
  ): Promise<BaseHttpResponse<UploadData>[]> {
    const uploadPromises = filesList.map(files =>
    ImageUploadAPI.uploadImage(context, files, aspectRatio)
    );

    return Promise.all(uploadPromises);
  }
}

/**
 * 多图片上传结果
 */
export interface MultiUploadResult {
  success: UploadData[]; // 成功上传的图片数据列表(包含url、ossId、aspectRatio)
  failed: string[]; // 失败的文件名列表
  total: number; // 总文件数
  successCount: number; // 成功数量
  failedCount: number; // 失败数量
}

/**
 * 图片上传管理器 - 处理业务逻辑
 */
export class ImageUploadManager {
  // 图片选择配置
  private static photoSelectOptions = new picker.PhotoSelectOptions();
  static {
    ImageUploadManager.photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    ImageUploadManager.photoSelectOptions.maxSelectNumber = 1;
  }

  // 分别存储不同类型的图片信息
  private static coverImage: SelectedImageInfo | null = null; // 封面图片
  private static multipleImages: SelectedImageInfo[] = []; // 多张图片

  /**
   * 获取封面图片信息
   * @returns SelectedImageInfo | null 封面图片信息
   */
  static getCoverImage(): SelectedImageInfo | null {
    return ImageUploadManager.coverImage;
  }

  /**
   * 获取多张图片信息列表
   * @returns SelectedImageInfo[] 多张图片信息数组
   */
  static getMultipleImages(): SelectedImageInfo[] {
    return ImageUploadManager.multipleImages;
  }

  /**
   * 清空封面图片
   */
  static clearCoverImage(): void {
    if (ImageUploadManager.coverImage) {
      // 清理封面图片的临时文件
      ImageUploadManager.cleanupTempFile(ImageUploadManager.coverImage.tempFilePath);
      ImageUploadManager.coverImage = null;
    }
  }

  /**
   * 清空多张图片
   */
  static clearMultipleImages(): void {
    // 清理多张图片的临时文件
    ImageUploadManager.multipleImages.forEach(imageInfo => {
      ImageUploadManager.cleanupTempFile(imageInfo.tempFilePath);
    });
    ImageUploadManager.multipleImages = [];
  }

  /**
   * 清空所有图片
   */
  static clearAllImages(): void {
    ImageUploadManager.clearCoverImage();
    ImageUploadManager.clearMultipleImages();
  }

  /**
   * 页面离开时清理所有临时文件
   * 建议在页面的 onPageHide 或 onDestroy 生命周期中调用
   */
  static onPageLeave(): void {
    console.log('页面离开,开始清理临时文件...');
    ImageUploadManager.clearAllImages();
    console.log('临时文件清理完成');
  }

  /**
   * 根据类型获取图片信息
   * @param type 图片选择类型
   * @returns SelectedImageInfo[] | SelectedImageInfo | null 图片信息
   */
  static getImagesByType(type: ImageSelectType): SelectedImageInfo[] | SelectedImageInfo | null {
    switch (type) {
      case ImageSelectType.COVER:
        return ImageUploadManager.coverImage;
      case ImageSelectType.MULTIPLE:
        return ImageUploadManager.multipleImages;
      default:
        return null;
    }
  }

  /**
   * 选择封面图片
   * @param context 页面上下文
   * @param aspectRatio 宽高比,默认为 "4:3"
   * @returns Promise<UploadData> 返回上传后的图片数据
   */
  static async selectCoverImage(context: Context, aspectRatio: string = "4:3"): Promise<UploadData> {
    return new Promise((resolve, reject) => {
      const photoViewPicker = new picker.PhotoViewPicker();

      photoViewPicker.select(ImageUploadManager.photoSelectOptions)
        .then(res => {
          const uri = res.photoUris[0]
          console.log('选择的封面图片URI:', uri)

          // 创建封面图片信息并存储
          const imageInfo = ImageUploadManager.createImageInfo(uri, context);
          ImageUploadManager.coverImage = imageInfo;

          ImageUploadManager.uploadImage(context, uri, aspectRatio)
            .then(resolve)
            .catch(reject)
        })
        .catch((error: BusinessError) => {
          console.error('选择封面图片失败:', error)
          reject(error)
        })
    })
  }

  /**
   * 选择多张图片(追加模式)
   * @param context 页面上下文
   * @param maxCount 最大选择数量,默认为9
   * @param aspectRatio 宽高比,默认为 "4:3"
   * @returns Promise<MultiUploadResult> 返回上传结果
   */
  static async selectMultipleImages(
    context: Context,
    maxCount: number = 9,
    aspectRatio: string = "4:3"
  ): Promise<MultiUploadResult> {
    return new Promise((resolve, reject) => {
      const multiSelectOptions = new picker.PhotoSelectOptions();
      multiSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      multiSelectOptions.maxSelectNumber = maxCount;

      const photoViewPicker = new picker.PhotoViewPicker();

      photoViewPicker.select(multiSelectOptions)
        .then(res => {
          const uris = res.photoUris
          console.log('选择的多张图片URIs:', JSON.stringify(uris))
          console.log(`选择了 ${uris.length} 张图片`)

          // 为每张图片创建信息并追加到现有数组
          const imageInfos = uris.map(uri => ImageUploadManager.createImageInfo(uri, context));
          ImageUploadManager.multipleImages.push(...imageInfos); // 追加而不是替换

          console.log(`当前多张图片总数: ${ImageUploadManager.multipleImages.length}`);

          ImageUploadManager.uploadMultipleImages(context, uris, aspectRatio)
            .then(resolve)
            .catch(reject)
        })
        .catch((error: BusinessError) => {
          console.error('选择多张图片失败:', error)
          reject(error)
        })
    })
  }

  /**
   * 上传单张图片(直接上传指定URI)
   * @param context 页面上下文
   * @param uri 图片URI
   * @param aspectRatio 宽高比,默认为 "4:3"
   * @returns Promise<UploadData> 返回上传后的图片数据
   */
  static async uploadImage(context: Context, uri: string, aspectRatio: string = "4:3"): Promise<UploadData> {
    return ImageUploadManager.uploadSingleImage(context, uri, aspectRatio)
  }

  /**
   * 批量上传多张图片(直接上传指定URI数组)
   * @param context 页面上下文
   * @param uris 图片URI数组
   * @param aspectRatio 宽高比,默认为 "4:3"
   * @returns Promise<MultiUploadResult> 返回上传结果
   */
  static async uploadMultipleImages(
    context: Context,
    uris: string[],
    aspectRatio: string = "4:3"
  ): Promise<MultiUploadResult> {
    const result: MultiUploadResult = {
      success: [],
      failed: [],
      total: uris.length,
      successCount: 0,
      failedCount: 0
    };

    // 并发上传所有图片
    const uploadPromises = uris.map(async (uri, index) => {
      try {
        const uploadData = await ImageUploadManager.uploadSingleImage(context, uri, aspectRatio)
        result.success.push(uploadData)
        result.successCount++
        console.log(`第 ${index + 1} 张图片上传成功:`, uploadData)
      } catch (error) {
        result.failed.push(`image_${Date.now()}_${index}.jpg`)
        result.failedCount++
        console.error(`第 ${index + 1} 张图片上传失败:`, error)
      }
    });

    // 等待所有上传完成
    await Promise.all(uploadPromises)

    console.log(`批量上传完成: 成功 ${result.successCount} 张,失败 ${result.failedCount} 张`)
    return result
  }

  /**
   * 创建图片信息对象
   * @param uri 图片URI
   * @param context 页面上下文
   * @returns SelectedImageInfo 图片信息对象
   */
  private static createImageInfo(uri: string, context: Context): SelectedImageInfo {
    const id = `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    const fileName = `image_${Date.now()}.jpg`;
    const tempFilePath = context.cacheDir + '/' + fileName;

    // 获取文件大小
    let fileSize = 0;
    try {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const stat = fs.statSync(file.fd);
      fileSize = stat.size;
      fs.closeSync(file);
    } catch (error) {
      console.warn('获取文件大小失败:', error);
    }

    return {
      id,
      originalUri: uri,
      tempFilePath,
      fileName,
      fileSize,
      selectTime: Date.now()
    };
  }

  /**
   * 上传单张图片(内部方法)
   * @param context 页面上下文
   * @param uri 图片URI
   * @param aspectRatio 宽高比
   * @returns Promise<UploadData> 返回上传后的图片数据
   */
  private static async uploadSingleImage(
    context: Context,
    uri: string,
    aspectRatio: string
  ): Promise<UploadData> {
    let tempFilePath: string | null = null;

    try {
      // 1. 处理临时文件
      const files = await ImageUploadManager.prepareTempFile(context, uri);
      tempFilePath = context.cacheDir + '/' + files[0].filename;

      // 2. 调用API上传
      const response = await ImageUploadAPI.uploadImage(context, files, aspectRatio);

      // 3. 处理响应
      if (response.code === 200) {
        return response.data;
      } else {
        throw new Error(response.msg || '上传失败');
      }
    } catch (error) {
      console.error('上传图片失败:', error);
      throw new Error();
    } finally {
      // 4. 清理临时文件
      if (tempFilePath) {
        ImageUploadManager.cleanupTempFile(tempFilePath);
      }
    }
  }

  /**
   * 处理临时文件 - 拷贝文件到缓存目录并准备上传数据
   * @param context 页面上下文
   * @param uri 图片URI
   * @returns Promise<Array<request.File>> 返回文件信息数组
   */
  private static async prepareTempFile(
    context: Context,
    uri: string
  ): Promise<Array<request.File>> {
    return new Promise((resolve, reject) => {
      let file: fs.File | null = null;

      try {
        // 1. 生成文件名
        const fileName = `image_${Date.now()}.jpg`;
        const tempFilePath = context.cacheDir + '/' + fileName;

        // 2. 打开原文件
        file = fs.openSync(uri, fs.OpenMode.READ_ONLY);

        // 3. 拷贝文件到临时目录
        fs.copyFileSync(file.fd, tempFilePath);

        // 4. 关闭原文件
        fs.closeSync(file);
        file = null;

        // 5. 创建文件信息数组
        const files: Array<request.File> = [
          {
            filename: fileName,
            type: 'jpg',
            name: 'file',
            uri: `internal://cache/${fileName}`
          }
        ];

        resolve(files);
      } catch (error) {
        // 确保文件句柄关闭
        if (file) {
          try {
            fs.closeSync(file);
          } catch (closeError) {
            console.warn('关闭文件句柄失败:', closeError);
          }
        }
        reject(error);
      }
    });
  }

  /**
   * 清理临时文件
   * @param filePath 文件路径
   */
  private static cleanupTempFile(filePath: string): void {
    try {
      if (fs.accessSync(filePath)) {
        fs.unlinkSync(filePath);
        console.log('临时文件已清理:', filePath);
      }
    } catch (error) {
      console.warn('清理临时文件失败:', filePath, error);
    }
  }
}

@Entry
@Component
struct Test {
  @State pixel: image.PixelMap | undefined = undefined;

  onPageHide() {
    ImageUploadManager.onPageLeave();
  }

  build() {
    Column() {
      Button('选择封面图片')
        .onClick(async () => {
          try {
            const context = getContext(this);
            const uploadData = await ImageUploadManager.selectCoverImage(context);

            // 获取封面图片信息
            const coverImage = ImageUploadManager.getCoverImage();
            console.log('封面图片信息:', JSON.stringify(coverImage));

            AlertDialog.show({
              message: `封面图片上传成功!\n图片地址: ${uploadData.url}\nOSS ID: ${uploadData.ossId}`
            });
          } catch (error) {
            AlertDialog.show({
              message: `操作失败: ${error.message || '未知错误'}`
            });
          }
        })

      Button('选择多张图片(追加)')
        .onClick(async () => {
          try {
            const context = getContext(this);
            const result = await ImageUploadManager.selectMultipleImages(context, 5);

            // 获取多张图片信息
            const multipleImages = ImageUploadManager.getMultipleImages();
            console.log('多张图片信息:', JSON.stringify(multipleImages));

            AlertDialog.show({
              message: `多张图片上传完成!\n成功: ${result.successCount} 张\n失败: ${result.failedCount} 张\n当前总图片数量: ${multipleImages.length}`
            });
          } catch (error) {
            AlertDialog.show({
              message: `操作失败: ${error.message || '未知错误'}`
            });
          }
        })

      Button('查看封面图片信息')
        .onClick(() => {
          const coverImage = ImageUploadManager.getCoverImage();
          if (!coverImage) {
            AlertDialog.show({
              message: '没有选择封面图片'
            });
          } else {
            AlertDialog.show({
              message: `封面图片信息:\n\nID: ${coverImage.id}\n文件名: ${coverImage.fileName}\n大小: ${coverImage.fileSize}字节\n选择时间: ${new Date(coverImage.selectTime).toLocaleString()}`
            });
          }
        })

      Button('查看多张图片信息')
        .onClick(() => {
          const multipleImages = ImageUploadManager.getMultipleImages();
          if (multipleImages.length === 0) {
            AlertDialog.show({
              message: '没有选择多张图片'
            });
          } else {
            const info = multipleImages.map((img, index) =>
            `第${index +
              1}张:\nID: ${img.id}\n文件名: ${img.fileName}\n大小: ${img.fileSize}字节\n选择时间: ${new Date(img.selectTime).toLocaleString()}`
            ).join('\n\n');

            AlertDialog.show({
              message: `多张图片信息 (共${multipleImages.length}张):\n\n${info}`
            });
          }
        })

      Button('清空封面图片')
        .onClick(() => {
          ImageUploadManager.clearCoverImage();
          AlertDialog.show({
            message: '已清空封面图片'
          });
        })

      Button('清空多张图片')
        .onClick(() => {
          ImageUploadManager.clearMultipleImages();
          AlertDialog.show({
            message: '已清空多张图片'
          });
        })

      Button('清空所有图片')
        .onClick(() => {
          ImageUploadManager.clearAllImages();
          AlertDialog.show({
            message: '已清空所有图片'
          });
        })

      Button('手动清理临时文件')
        .onClick(() => {
          ImageUploadManager.onPageLeave();
          AlertDialog.show({
            message: '已手动清理所有临时文件'
          });
        })
    }
    .width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐