位置:首页 > 进阶教程 > TMS运输管理系统司机App开发实现运输任务签收

TMS运输管理系统司机App开发实现运输任务签收

时间:2026-07-24  |  作者:深海捕梦者  |  阅读:0

一、技术选型与架构设计

先聊聊为什么选React Native。物流行业的终端设备五花八门:安卓大屏机、手持PDA、个人手机……碎片化程度很高。

RN“一次编写,多端运行”的特性,能大幅降低维护成本。它的性能已足够支撑复杂的画板和相机操作,接近原生体验。

1.1 核心技术栈

功能模块 技术/库 选型理由
手写签名 react-native-signature-canvas 基于原生WebView封装,性能优于纯JS实现,支持压感和笔锋
图像采集 react-native-image-picker + react-native-image-resizer 前者调用系统相机/相册,后者解决大图上传慢的问题
网络请求 axios + rn-fetch-blob rn-fetch-blob支持文件断点续传,适合不稳定网络
本地存储 async-storage + realm async-storage存简单配置,realm存结构化离线数据
UI组件 react-native-paper Material Design风格,开发效率高

1.2 签收模块架构图

签收模块架构图

架构解析

  • 离线优先(Offline First):这是核心设计理念。不管网络状况如何,签收操作必须能成功。数据先落本地数据库(Realm),等网络恢复后,后台服务自动同步。
  • 流程控制SignFlow Controller统一管理状态,比如“已拍照但未签名”、“已签名但未上传”,防止脏数据生成。
  • 原生桥接:复杂的图像处理和网络请求通过原生模块执行,避免JS线程阻塞导致UI卡顿。

二、核心功能实现:电子签名板

电子签名是法律效力的核心。我们需要一个流畅、清晰、且能导出高清图片的手写板。

2.1 签名板组件封装

import React, { useRef, useState } from 'react';
import { View, Button, StyleSheet, Alert } from 'react-native';
import SignatureCanvas from 'react-native-signature-canvas';

const SignaturePad = ({ onOK, onEmpty }) => {
  const ref = useRef();
  const [signatureBase64, setSignatureBase64] = useState(null);

  // 当绘制完成时触发(手指离开屏幕)
  const handleEnd = () => {
    ref.current.readSignature();
  };

  // 获取到Base64数据
  const handleData = (base64Data) => {
    setSignatureBase64(base64Data);
    // Base64格式: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
  };

  const handleClear = () => {
    ref.current.clearSignature();
    setSignatureBase64(null);
    onEmpty && onEmpty();
  };

  const handleConfirm = () => {
    if (!signatureBase64) {
      Alert.alert('提示', '请先进行手写签名');
      return;
    }
    onOK && onOK(signatureBase64);
  };

  const style = `.m-signature-pad { box-shadow: none; border: 1px solid #ddd; } 
    .m-signature-pad--body { border: none; } 
    .m-signature-pad--footer { display: none; }`;

  return (
    
      
      
        

代码深度解析

  • Ref控制:通过ref直接调用组件内部的readSignatureclearSignature方法,实现精准控制。
  • Base64传输:签名板直接输出Base64编码的图片数据。这非常适合直接传给后端或存入本地数据库,避免文件路径管理的复杂性。
  • WebStyle属性:由于该库底层基于WebView,可以通过注入CSS(webStyle)来深度定制外观,比如去掉默认的Footer按钮,用我们自己的React Native按钮,保持App整体UI风格一致。
  • 性能注意:Base64数据体积较大(比二进制大30%),不适合一次性加载过多张,适合单张签名图的即时处理。

三、核心功能实现:照片上传与压缩

货物照片——特别是异常破损照片——是理赔的重要依据。手机摄像头动辄1200万像素,一张照片可能高达5MB,直接上传的话就算4G网络也很痛苦。

必须进行压缩

3.1 图像选择与压缩流程

import ImagePicker from 'react-native-image-picker';
import ImageResizer from 'react-native-image-resizer';
import RNFS from 'react-native-fs';

const imageService = {
  // 1. 打开相机
  launchCamera: async () => {
    const options = {
      mediaType: 'photo',
      quality: 1.0, // 先获取原图,后续压缩
      maxWidth: 2000, // 限制最大宽度
      maxHeight: 2000, // 限制最大高度
      includeBase64: false, // 暂时不包含base64,节省内存
      sa veToPhotos: false // 不保存到系统相册,保护隐私
    };

    return new Promise((resolve, reject) => {
      ImagePicker.launchCamera(options, (response) => {
        if (response.didCancel) {
          resolve(null);
        } else if (response.errorCode) {
          reject(new Error(response.errorMessage));
        } else {
          resolve(response.assets[0]);
        }
      });
    });
  },

  // 2. 压缩图片
  compressImage: async (uri, fileName) => {
    try {
      const compressedImage = await ImageResizer.createResizedImage(
        uri,
        1280, // 宽度:1280px足够清晰,且体积适中
        960, // 高度
        'JPEG', // 格式
        80, // 质量:80%肉眼无差别,体积减半
        0, // 旋转角度
        RNFS.CachesDirectoryPath // 存储路径
      );
      // compressedImage.uri: file:///.../.cache/resize.jpg
      return compressedImage;
    } catch (error) {
      console.error('压缩失败:', error);
      throw error;
    }
  },

  // 3. 转换为Base64(用于上传或存DB)
  convertToBase64: async (uri) => {
    try {
      const base64 = await RNFS.readFile(uri, 'base64');
      return `data:image/jpeg;base64,${base64}`;
    } catch (error) {
      console.error('Base64转换失败:', error);
      return null;
    }
  }
};

export default imageService;

代码深度解析

  • 两步法压缩:先通过ImagePicker限制硬件采集的最大尺寸(maxWidth/maxHeight),再通过ImageResizer进行二次压缩。因为部分手机硬件采集的优先级高于Picker的设置。
  • 黄金参数1280x960 + Quality 80是经过验证的“甜点”参数。一张原图5MB的照片压缩后通常在200KB-400KB之间,清晰度足以辨认货物细节,上传速度提升10倍以上。
  • 缓存目录:压缩后的图片存放在RNFS.CachesDirectoryPath(应用缓存目录),而不是Documents目录,方便系统清理,避免占用用户过多存储空间。

四、离线签收与队列同步

这部分是整篇最难也最有价值的地方。司机进了地下车库或者偏远地区,网络中断怎么办?

必须实现一个可靠的离线队列

4.1 使用Realm存储离线数据

相比AsyncStorage,Realm更适合存储结构化的、需要查询的数据。

export const SignRecordSchema = {
  name: 'SignRecord',
  primaryKey: 'localId',
  properties: {
    localId: 'string', // UUID,本地唯一标识
    taskId: 'string', // 任务ID
    signatureBase64: 'string?', // 签名数据
    photos: 'string[]', // 照片Base64数组
    latitude: 'double?', // 签收纬度
    longitude: 'double?', // 签收经度
    signedAt: 'date', // 签收时间
    status: 'string', // PENDING, UPLOADING, SUCCESS, FAILED
    retryCount: 'int', // 重试次数
    errorMessage: 'string?' // 错误信息
  }
};

// services/offlineQueue.js
import Realm from 'realm';
import { SignRecordSchema } from '../database/schemas';
import NetInfo from '@react-native-community/netinfo';

class OfflineQueue {
  constructor() {
    this.realm = null;
    this.init();
    this.setupNetworkListener();
  }

  async init() {
    this.realm = await Realm.open({
      schema: [SignRecordSchema],
      schemaVersion: 1
    });
  }

  // 添加记录到队列
  add(record) {
    if (!this.realm) return;
    this.realm.write(() => {
      this.realm.create('SignRecord', {
        ...record,
        status: 'PENDING',
        retryCount: 0
      });
    });
  }

  // 监听网络变化
  setupNetworkListener() {
    NetInfo.addEventListener(state => {
      if (state.isConnected && state.isInternetReachable) {
        console.log('网络恢复,开始同步...');
        this.syncPendingRecords();
      }
    });
  }

  // 同步逻辑
  async syncPendingRecords() {
    if (!this.realm) return;
    const pendingRecords = this.realm.objects('SignRecord').filtered('status == "PENDING" OR (status == "FAILED" AND retryCount < 3)');

    for (const record of pendingRecords) {
      try {
        this.realm.write(() => {
          record.status = 'UPLOADING';
        });

        // 调用上传API (此处为伪代码)
        await this.uploadToServer(record);

        this.realm.write(() => {
          record.status = 'SUCCESS';
        });
        console.log(`记录 ${record.localId} 上传成功`);
      } catch (error) {
        this.realm.write(() => {
          record.status = 'FAILED';
          record.retryCount += 1;
          record.errorMessage = error.message;
        });
        console.warn(`记录 ${record.localId} 上传失败,重试次数 ${record.retryCount}`);
      }
    }
  }

  // 伪代码:上传到服务器
  uploadToServer(record) {
    // 使用 FormData 或 GraphQL Multipart Request
    // 注意:上传Base64时,后端可能需要解码为二进制文件
    return new Promise((resolve) => setTimeout(resolve, 1000));
  }
}

export default new OfflineQueue();

代码深度解析

  • 状态机设计:记录拥有PENDING → UPLOADING → SUCCESS/FAILED的状态流转。这防止了重复上传和数据冲突。
  • 重试机制:设置retryCount,限制最大重试次数(如3次),避免死循环和网络风暴。
  • 网络监听:使用@react-native-community/netinfo监听网络变化。一旦网络恢复,自动触发syncPendingRecords,实现“无感同步”。
  • 事务(Transaction):Realm的写入操作必须在realm.write()中进行,确保数据一致性。

4.2 签收流程时序图

签收流程时序图

五、性能与安全优化

5.1 内存泄漏防治

React Native中图片处理极易引发OOM(Out Of Memory)。

  • 及时释放:在componentWillUnmountuseEffect的清理函数中,释放不再使用的图片资源。
  • 避免Base64大对象:如果照片很多,不建议将所有Base64都存在一个数组中。建议压缩后立即上传(在线时)或写入文件(离线时),只在UI上展示缩略图。

5.2 数据安全

  • 本地加密:Realm支持数据库加密。在初始化Realm时传入encryptionKey,防止手机Root后数据被盗。
  • HTTPS:所有上传接口必须使用HTTPS,防止签名数据被中间人劫持。
  • 法律合规:在首次启动App或进入签收页时,必须弹窗告知用户“本次签收将采集电子签名及现场照片,作为运输凭证”,获得用户明确授权。

总结

这篇文章详细介绍了基于React Native构建TMS司机端签收模块的全过程。不只是实现了基础的电子签名和拍照功能,更重要的是构建了一套健壮的离线优先架构

通过本文,你掌握了:

  • 高性能手写板集成:利用react-native-signature-canvas实现流畅签名,并处理Base64数据流。
  • 图像工程化技巧:通过ImageResizer进行参数调优,在保证证据清晰度的前提下最大化压缩比,提升上传体验。
  • 离线数据架构:使用Realm数据库构建本地队列,结合NetInfo实现网络状态感知与自动重试机制,彻底解决弱网环境下的数据可靠性问题。
  • 业务流程闭环:理解了签收数据如何触发后续的结算流程,实现真正的数字化回单管理。

这套方案确保司机在任何极端环境下都能完成交付,财务部门能第一时间获取有效凭证,显著缩短了回单流转周期,减少了因回单丢失或模糊导致的财务纠纷。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多