react native 实现选择图片或者拍照上传(多张)

第一步:

复制代码
npm install react-native-image-picker

第二步: 配置权限--安卓

  1. android/app/src/main/AndroidManifest.xml 中添加权限

xml

复制代码
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

2.对于 Android 13+,还需要添加这些权限

xml

复制代码
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

第三步:

TypeScript 复制代码
// services/ImagePickerService.ts
import { launchCamera, launchImageLibrary, Asset } from 'react-native-image-picker';
import { PermissionsAndroid, Platform, Alert } from 'react-native';

interface ImageFile {
  uri: string;
  type: string;
  name: string;
  fileSize?: number;
}

class ImagePickerService {
  private static instance: ImagePickerService;

  public static getInstance(): ImagePickerService {
    if (!ImagePickerService.instance) {
      ImagePickerService.instance = new ImagePickerService();
    }
    return ImagePickerService.instance;
  }

  /**
   * 请求相机权限
   */
  private async requestCameraPermission(): Promise<boolean> {
    if (Platform.OS !== 'android') return true;

    try {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.CAMERA,
        {
          title: '相机权限',
          message: '应用需要访问相机以拍照',
          buttonPositive: '确定',
          buttonNegative: '取消',
        }
      );
      return granted === PermissionsAndroid.RESULTS.GRANTED;
    } catch (error) {
      console.error('请求相机权限失败:', error);
      return false;
    }
  }

  /**
   * 请求存储权限
   */
  private async requestStoragePermission(): Promise<boolean> {
    if (Platform.OS !== 'android') return true;
    try {
      // 对于 Android 13+ (API 33+),需要使用新的权限
      const apiLevel = Platform.Version;
      let permission: any;
      
      if (apiLevel >= 33) {
        // Android 13+ 使用新的媒体权限
        permission = PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES;
      } else {
        // Android 12 及以下使用存储权限
        permission = PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE;
      }

      const granted = await PermissionsAndroid.request(permission, {
        title: '存储权限',
        message: '应用需要访问相册以选择图片',
        buttonPositive: '确定',
        buttonNegative: '取消',
      });

      return granted === PermissionsAndroid.RESULTS.GRANTED;
    } catch (error) {
      console.error('请求存储权限失败:', error);
      return false;
    }
    // try {
    //   const granted = await PermissionsAndroid.request(
    //     PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
    //     {
    //       title: '存储权限',
    //       message: '应用需要访问相册以选择图片',
    //       buttonPositive: '确定',
    //       buttonNegative: '取消',
    //     }
    //   );
    //   return granted === PermissionsAndroid.RESULTS.GRANTED;
    // } catch (error) {
    //   console.error('请求存储权限失败:', error);
    //   return false;
    // }
  }

  /**
   * 选择多张图片
   */
  public async pickMultipleImages(maxCount: number = 3): Promise<ImageFile[]> {
    try {
      const hasPermission = await this.requestStoragePermission();
      if (!hasPermission) {
        Alert.alert('权限被拒绝', '需要存储权限来选择图片');
        return [];
      }

      return new Promise((resolve) => {
        launchImageLibrary(
          {
            mediaType: 'photo',
            includeBase64: false,
            maxHeight: 2000,
            maxWidth: 2000,
            quality: 0.8,
            selectionLimit: maxCount, // 最大选择数量
            // includeExtra: true,
            presentationStyle: 'fullScreen',
          },
          (response) => {
            if (response.didCancel) {
              console.log('用户取消了选择');
              resolve([]);
            } else if (response.errorCode) {
              console.error('选择图片错误:', response.errorMessage);
              Alert.alert('错误', '选择图片失败');
              resolve([]);
            } else if (response.assets && response.assets.length > 0) {
              const selectedImages = response.assets.map((asset: Asset) => ({
                uri: asset.uri!,
                type: asset.type || 'image/jpeg',
                name: asset.fileName || `image_${Date.now()}.jpg`,
                fileSize: asset.fileSize,
                width: asset.width,
                height: asset.height,
              }));
              resolve(selectedImages);
            } else {
              resolve([]);
            }
          }
        );
      });
    } catch (error) {
      console.error('选择多张图片失败:', error);
      Alert.alert('错误', '选择图片时发生错误');
      return [];
    }
  }

  /**
   * 拍照
   */
  public async takePhoto(): Promise<ImageFile | null> {
    try {
      const hasCameraPermission = await this.requestCameraPermission();
      const hasStoragePermission = await this.requestStoragePermission();

      if (!hasCameraPermission || !hasStoragePermission) {
        Alert.alert('权限被拒绝', '需要相机和存储权限来拍照');
        return null;
      }

      return new Promise((resolve) => {
        launchCamera(
          {
            mediaType: 'photo',
            includeBase64: false,
            maxHeight: 2000,
            maxWidth: 2000,
            quality: 0.8,
            saveToPhotos: true, // 保存到相册
            cameraType: 'back',
            includeExtra: true,
          },
          (response) => {
            if (response.didCancel) {
              console.log('用户取消了拍照');
              resolve(null);
            } else if (response.errorCode) {
              console.error('拍照错误:', response.errorMessage);
              Alert.alert('错误', '拍照失败');
              resolve(null);
            } else if (response.assets && response.assets.length > 0) {
              const asset = response.assets[0];
              const imageFile: ImageFile = {
                uri: asset.uri!,
                type: asset.type || 'image/jpeg',
                name: asset.fileName || `photo_${Date.now()}.jpg`,
                fileSize: asset.fileSize,
                width: asset.width,
                height: asset.height,
              };
              resolve(imageFile);
            } else {
              resolve(null);
            }
          }
        );
      });
    } catch (error) {
      console.error('拍照失败:', error);
      Alert.alert('错误', '拍照时发生错误');
      return null;
    }
  }

  /**
   * 压缩图片(如果需要)
   */
  private async compressImage(uri: string, quality: number = 0.7): Promise<string> {
    // 这里可以集成图片压缩库,如 react-native-image-resizer
    // 暂时返回原图URI
    return uri;
  }

  /**
   * 获取文件大小(MB)
   */
  public getFileSizeInMB(fileSize: number): string {
    return (fileSize / (1024 * 1024)).toFixed(2);
  }
}

export default ImagePickerService.getInstance();
TypeScript 复制代码
// services/UploadMeterReadingService.ts
import Realm from 'realm';
import NetInfo from '@react-native-community/netinfo';
import RealmDB from '@/database/RealmDB';
import { realmConfig } from '../database/config';
import { http } from '@utils/http';
import { config } from '@utils/config';

interface MeterReadingData {
  CheckMonth?: string;
  Statue: number;
  ReduceNum: string;
  WaterMeterNO?: string;
  CustomerNO?: string;
  MeterReadingID?: string;
  CurrPeriodData: string;
  AddNum: string;
  LastPeriodData?: string;
}

class UploadMeterReadingService {
  private static instance: UploadMeterReadingService;
  private db: RealmDB;
  private unsubscribeFromNetInfo: (() => void) | null = null;

  constructor() {
    this.db = RealmDB.getInstance(realmConfig);
  }

  public static getInstance(): UploadMeterReadingService {
    if (!UploadMeterReadingService.instance) {
      UploadMeterReadingService.instance = new UploadMeterReadingService();
    }
    return UploadMeterReadingService.instance;
  }

  /**
   * 保存抄表数据到本地数据库
   */
  private async saveReadingsToLocal(
    readingData: MeterReadingData[],
  ): Promise<void> {
    const realm = this.db.getRealm();

    return new Promise((resolve, reject) => {
      try {
        realm.write(() => {
          readingData.forEach(data => {
            const existing = realm
              .objects('UploadMeterReading')
              .filtered(
                `CustomerNO == $0 AND isUploaded == false`,
                data.CustomerNO,
              )[0];

            if (existing) {
              // 更新现有记录
              Object.assign(existing, {
                ...data,
                updatedAt: new Date(),
              });
            } else {
              // 创建新记录
              realm.create(
                'UploadMeterReading',
                {
                  ...data,
                  isUploaded: false,
                  createdAt: new Date(),
                  updatedAt: new Date(),
                },
                Realm.UpdateMode.Modified,
              );
            }
          });
        });
        resolve();
      } catch (error) {
        console.error('保存数据到本地失败:', error);
        reject(new Error('保存数据到本地失败'));
      }
    });
  }

  /**
   * 带网络检测的数据保存和上传
   */
  public async saveAndUpload(
    readingData: MeterReadingData[],
    options: {
      retryCount?: number;
      immediateUpload?: boolean;
    } = {},
  ): Promise<void> {
    const { retryCount = 3, immediateUpload = true } = options;

    try {
      // 1. 先保存到本地
      await this.saveReadingsToLocal(readingData);

      // 2. 检查网络并决定是否上传
      const isConnected = await this.checkNetworkConnection();

      if (isConnected && immediateUpload) {
        await this.uploadWithRetry(retryCount);
      }
    } catch (error) {
      console.error('保存并上传数据失败:', error);
      throw error;
    }
  }

  /**
   * 带重试机制的上传
   */
  private async uploadWithRetry(maxRetries: number): Promise<void> {
    let attempts = 0;

    while (attempts < maxRetries) {
      try {
        await this.uploadReadingsToServer();
        return; // 上传成功则退出
      } catch (error) {
        attempts++;
        if (attempts >= maxRetries) {
          throw error; // 重试次数用尽
        }

        // 指数退避等待
        const delay = Math.pow(2, attempts) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  /**
   * 检查网络连接
   */
  private async checkNetworkConnection(): Promise<boolean> {
    try {
      const state = await NetInfo.fetch();
      return state.isConnected ?? false;
    } catch (error) {
      console.warn('网络状态检测失败:', error);
      return false;
    }
  }

  /**
   * 上传数据到服务器
   */
  public async uploadReadingsToServer(): Promise<void> {
    try {
      const unuploaded = this.getUnuploadedReadings();
      if (unuploaded.length === 0) return;

      // 准备上传数据
      const readingsToUpload = unuploaded.map(reading => {
        const obj = { ...reading.toJSON() };
        delete obj.isUploaded;
        delete obj.createdAt;
        delete obj.updatedAt;
        return obj;
      });

      // 调用API上传
      const response = await http.post(
        `${config.apiPrefix}${config.uploadMeterReadingApi}`,
        readingsToUpload,
      );

      if (!response.Status) {
        throw new Error('上传失败: ' + (response.Message || '未知错误'));
      }

      // 标记为已上传
      const idsToMark = unuploaded.map((item: any) => item.CustomerNO);
      await this.markAsUploaded(idsToMark);
    } catch (error) {
      console.error('上传数据失败:', error);
      throw error;
    }
  }

  /**
   * 初始化网络状态监听
   */
  public initNetworkListener(): void {
    this.unsubscribeFromNetInfo = NetInfo.addEventListener(state => {
      if (state.isConnected) {
        console.log('网络恢复,尝试自动上传...');
        this.uploadReadingsToServer().catch(error => {
          console.warn('自动上传失败:', error);
        });
      }
    });
  }

  /**
   * 清理网络监听
   */
  public cleanupNetworkListener(): void {
    if (this.unsubscribeFromNetInfo) {
      this.unsubscribeFromNetInfo();
      this.unsubscribeFromNetInfo = null;
    }
  }

  // 以下是原有方法的实现保持不变
  public getUnuploadedReadingsByCustomer(
    customerNO: string,
  ): Realm.Results<Realm.Object> {
    return this.db.filter(
      'UploadMeterReading',
      'CustomerNO == $0 AND isUploaded == false',
      customerNO,
    );
  }

  /**
   * 获取所有未上传的抄表数据
   */
  public getUnuploadedReadings(): Realm.Results<Realm.Object> {
    return this.db.filter('UploadMeterReading', 'isUploaded == false');
  }

  /**
   * 标记数据为已上传
   * @param meterReadingIDs 要标记的数据ID数组
   */
  public markAsUploaded(meterReadingIDs: string[]): Promise<void> {
    const realm = this.db.getRealm();
    return new Promise((resolve, reject) => {
      try {
        realm.write(() => {
          meterReadingIDs.forEach(id => {
            const reading = realm.objectForPrimaryKey('UploadMeterReading', id);
            if (reading) {
              reading.isUploaded = true;
              reading.updatedAt = new Date();
            }
          });
        });
        resolve();
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * 删除已上传的数据
   */
  public cleanupUploadedData(): Promise<void> {
    const realm = this.db.getRealm();
    return new Promise((resolve, reject) => {
      try {
        realm.write(() => {
          const uploadedData = realm
            .objects('UploadMeterReading')
            .filtered('isUploaded == true');
          realm.delete(uploadedData);
        });
        resolve();
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * 安全清除所有上传数据(类型安全版本)
   */
  public async clearAllUploadData(): Promise<{
    success: boolean;
    deletedCount: number;
  }> {
    try {
      const realm = this.db.getRealm();
      let deletedCount = 0;
  
      // 在写事务外部获取数据
      const allData = realm.objects('UploadMeterReading');
      
      if (!allData.isValid()) {
        console.error('Realm 对象集合已失效');
        return { success: false, deletedCount: 0 };
      }
  
      deletedCount = allData.length;
  
      if (deletedCount === 0) {
        console.log('没有上传数据需要清除');
        return { success: true, deletedCount: 0 };
      }
  
      // 修复:Realm 的 write 方法不是异步的,不要使用 async/await
      realm.write(() => {
        realm.delete(allData);
      });
  
      console.log(`成功清除 ${deletedCount} 条上传数据`);
      return { success: true, deletedCount };
      
    } catch (error) {
      console.error('清除数据失败:', error);
      return { success: false, deletedCount: 0 };
    }
  }
}

const uploadMeterReadingService = UploadMeterReadingService.getInstance();
export default uploadMeterReadingService;

然后封装一个组件:

TypeScript 复制代码
// services/ImagePickerService.ts
import { launchCamera, launchImageLibrary, Asset } from 'react-native-image-picker';
import { PermissionsAndroid, Platform, Alert } from 'react-native';

interface ImageFile {
  uri: string;
  type: string;
  name: string;
  fileSize?: number;
}

class ImagePickerService {
  private static instance: ImagePickerService;

  public static getInstance(): ImagePickerService {
    if (!ImagePickerService.instance) {
      ImagePickerService.instance = new ImagePickerService();
    }
    return ImagePickerService.instance;
  }

  /**
   * 请求相机权限
   */
  private async requestCameraPermission(): Promise<boolean> {
    if (Platform.OS !== 'android') return true;

    try {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.CAMERA,
        {
          title: '相机权限',
          message: '应用需要访问相机以拍照',
          buttonPositive: '确定',
          buttonNegative: '取消',
        }
      );
      return granted === PermissionsAndroid.RESULTS.GRANTED;
    } catch (error) {
      console.error('请求相机权限失败:', error);
      return false;
    }
  }

  /**
   * 请求存储权限
   */
  private async requestStoragePermission(): Promise<boolean> {
    if (Platform.OS !== 'android') return true;
    try {
      // 对于 Android 13+ (API 33+),需要使用新的权限
      const apiLevel = Platform.Version;
      let permission: any;
      
      if (apiLevel >= 33) {
        // Android 13+ 使用新的媒体权限
        permission = PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES;
      } else {
        // Android 12 及以下使用存储权限
        permission = PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE;
      }

      const granted = await PermissionsAndroid.request(permission, {
        title: '存储权限',
        message: '应用需要访问相册以选择图片',
        buttonPositive: '确定',
        buttonNegative: '取消',
      });

      return granted === PermissionsAndroid.RESULTS.GRANTED;
    } catch (error) {
      console.error('请求存储权限失败:', error);
      return false;
    }
    // try {
    //   const granted = await PermissionsAndroid.request(
    //     PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
    //     {
    //       title: '存储权限',
    //       message: '应用需要访问相册以选择图片',
    //       buttonPositive: '确定',
    //       buttonNegative: '取消',
    //     }
    //   );
    //   return granted === PermissionsAndroid.RESULTS.GRANTED;
    // } catch (error) {
    //   console.error('请求存储权限失败:', error);
    //   return false;
    // }
  }

  /**
   * 选择多张图片
   */
  public async pickMultipleImages(maxCount: number = 3): Promise<ImageFile[]> {
    try {
      const hasPermission = await this.requestStoragePermission();
      if (!hasPermission) {
        Alert.alert('权限被拒绝', '需要存储权限来选择图片');
        return [];
      }

      return new Promise((resolve) => {
        launchImageLibrary(
          {
            mediaType: 'photo',
            includeBase64: false,
            maxHeight: 2000,
            maxWidth: 2000,
            quality: 0.8,
            selectionLimit: maxCount, // 最大选择数量
            // includeExtra: true,
            presentationStyle: 'fullScreen',
          },
          (response) => {
            if (response.didCancel) {
              console.log('用户取消了选择');
              resolve([]);
            } else if (response.errorCode) {
              console.error('选择图片错误:', response.errorMessage);
              Alert.alert('错误', '选择图片失败');
              resolve([]);
            } else if (response.assets && response.assets.length > 0) {
              const selectedImages = response.assets.map((asset: Asset) => ({
                uri: asset.uri!,
                type: asset.type || 'image/jpeg',
                name: asset.fileName || `image_${Date.now()}.jpg`,
                fileSize: asset.fileSize,
                width: asset.width,
                height: asset.height,
              }));
              resolve(selectedImages);
            } else {
              resolve([]);
            }
          }
        );
      });
    } catch (error) {
      console.error('选择多张图片失败:', error);
      Alert.alert('错误', '选择图片时发生错误');
      return [];
    }
  }

  /**
   * 拍照
   */
  public async takePhoto(): Promise<ImageFile | null> {
    try {
      const hasCameraPermission = await this.requestCameraPermission();
      const hasStoragePermission = await this.requestStoragePermission();

      if (!hasCameraPermission || !hasStoragePermission) {
        Alert.alert('权限被拒绝', '需要相机和存储权限来拍照');
        return null;
      }

      return new Promise((resolve) => {
        launchCamera(
          {
            mediaType: 'photo',
            includeBase64: false,
            maxHeight: 2000,
            maxWidth: 2000,
            quality: 0.8,
            saveToPhotos: true, // 保存到相册
            cameraType: 'back',
            includeExtra: true,
          },
          (response) => {
            if (response.didCancel) {
              console.log('用户取消了拍照');
              resolve(null);
            } else if (response.errorCode) {
              console.error('拍照错误:', response.errorMessage);
              Alert.alert('错误', '拍照失败');
              resolve(null);
            } else if (response.assets && response.assets.length > 0) {
              const asset = response.assets[0];
              const imageFile: ImageFile = {
                uri: asset.uri!,
                type: asset.type || 'image/jpeg',
                name: asset.fileName || `photo_${Date.now()}.jpg`,
                fileSize: asset.fileSize,
                width: asset.width,
                height: asset.height,
              };
              resolve(imageFile);
            } else {
              resolve(null);
            }
          }
        );
      });
    } catch (error) {
      console.error('拍照失败:', error);
      Alert.alert('错误', '拍照时发生错误');
      return null;
    }
  }

  /**
   * 压缩图片(如果需要)
   */
  private async compressImage(uri: string, quality: number = 0.7): Promise<string> {
    // 这里可以集成图片压缩库,如 react-native-image-resizer
    // 暂时返回原图URI
    return uri;
  }

  /**
   * 获取文件大小(MB)
   */
  public getFileSizeInMB(fileSize: number): string {
    return (fileSize / (1024 * 1024)).toFixed(2);
  }
}

export default ImagePickerService.getInstance();

实现的效果:

后续继续更新

相关推荐
谢尔登2 小时前
【React】React组件的渲染过程分为哪几个阶段?
前端·javascript·react.js
无敌最俊朗@2 小时前
Vue 3 概况
前端·javascript·vue.js
拉不动的猪3 小时前
一文搞懂:localhost和局域网 IP 的核心区别与使用场景
前端·javascript·面试
未来之窗软件服务3 小时前
自建开发工具IDE(二)文件托拽读取——东方仙盟炼气期
开发语言·前端·javascript·仙盟创梦ide·东方仙盟
GISer_Jing4 小时前
OpenCV头文件路径配置终极修复指南
javascript·opencv·webpack
s9123601015 小时前
【Rust】使用lldb 调试core dump
前端·javascript·rust
不是鱼5 小时前
Canvas学习笔记(一)
前端·javascript·canvas
我有一棵树6 小时前
React 中 useRef 和 useState 的使用场景区别
前端·javascript·react.js
fmk10236 小时前
Vue中的provide与inject
前端·javascript·vue.js