第一步:
npm install react-native-image-picker
第二步: 配置权限--安卓
- 在
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();
实现的效果:


后续继续更新