找回密码
 立即注册
首页 业界区 业界 HarmonyOS实现快递APP自动识别地址

HarmonyOS实现快递APP自动识别地址

毡轩 昨天 15:19
​    大家好,我是潘Sir,持续分享IT技术,帮你少走弯路。《鸿蒙应用开发从入门到项目实战》系列文章持续更新中,欢迎关注!
随着鸿蒙(HarmonyOS)生态发展,越来越多的APP需要进行鸿蒙适配。本文以快递APP寄件中的收货地址识别功能为例,探讨HarmonyOS鸿蒙版本的开发和适配。
一、需求分析

1、应用场景

随着互联网的发展,网购已成为大家日常生活中不可或缺的一部分。网购涉及到发货和收货操作,为了简化操作、提升APP的使用效率,各大APP都融入了AI能力。在AI赋能下,从传统的文字输入交互方式,逐步拓展到人脸识别、指纹识别、图片识别、语言识别等方式,降低了APP使用门槛的同时极大提高了交互效率。
本文研究以支付宝APP里边的“菜鸟裹裹”寄件场景为例,通过粘贴文字或者图片识别进行收货地址自动识别填充。在鸿蒙操作系统(HarmonyOS)上借助HarmonyOS SDK 提供的AI能力,开发鸿蒙原生应用APP功能,完成上述功能。
ps:相信大家都寄过快递,如果没操作过可以先了解一下。
2、实现效果

主页拍照识别保存
1.png
2.png
3.png
4.png
ps:由于编写的提取规则中,名字为2-4个中文,因此上边的名字“潘Sir”包含了1个英文,故未识别正确。如果改为2-4个汉字则可以正确识别。这就是传统的用规则来匹配的弊端,更好的方法是使用AI大模型或NLP工具来提取地址信息。
读者可以直接运行提供的代码查看效果。由于使用了视觉服务,需要真机运行。
使用说明:

  • 点击图片识别按钮,拉起选择图片获取方式的弹窗,选择拍照方式,通过对要识别的文字进行拍照获得要识别的图片。也可以选择相册方式,在图库中直接选择需要识别的图片。
  • 识别出图片包含的文本信息后,会自动将文本内容填充到文本输入框。
  • 点击`地址解析按钮,会将文本框中的信息提取为结构化数据,显示到按钮下方的列表中。
  • 点击保存地址按钮,提示保存成功,文本框旧的内容会自动清空。
3、技术分析

基于HarmonyOS SDK提供的基础视觉服务(CoreVisionKit),使用@kit.CoreVisionKit提供的通用文字识别能力,通过拍照(CameraPicker)或者相册(PhotoViewPicker)方式,将印刷品文字(如:收货信息)转化为图像信息,再利用文字识别技术将图像信息转化为设备可以使用的文本字符,最后可以根据实际业务规则提取结构化数据。
二、界面制作

1、布局分析

主界面布局分析:
5.png

弹窗界面布局分析:
6.png

2、界面制作

开发环境说明:DevEco Studio5.0.4 Release、 HarmonyOS5.0.4(API 16)
通过DevEco Studio创建项目,项目名称为:ExtractAddress,删除Index.ets文件中默认的代码。
2.1 制作主界面

为了便于代码复用和程序扩展,将收货人信息的界面上的每一行显示的数据,抽取为一个对象,该对象类型为ConsigneeInfo类。在ets目录下新建viewmodel目录,新建DataModel.ets文件,内容如下:
  1. // 收货人信息界面视图模型
  2. @ObservedV2
  3. export class ConsigneeInfo {
  4.   label: ResourceStr;       //标签名称
  5.   placeholder: ResourceStr; //提示语
  6.   @Trace value: string;     //输入值
  7.   constructor(label: ResourceStr, placeholder: ResourceStr, value: string) {
  8.     this.label = label;
  9.     this.placeholder = placeholder;
  10.     this.value = value;
  11.   }
  12. }
复制代码
有了此类,在主界面上就只需要实例化3个对象,通过列表进行循环渲染即可,避免了重复臃肿的代码。接下来制作主界面,Index.ets文件内容如下:
  1. import { ConsigneeInfo} from '../viewmodel/DataModel';
  2. @Entry
  3. @Component
  4. struct Index {
  5.   @State consigneeInfos: ConsigneeInfo[] = []; //收货人信息界面视图
  6.   @State saveAvailable: boolean = false; //保存按钮是否可用
  7.   aboutToAppear(): void {
  8.     this.consigneeInfos = [
  9.       new ConsigneeInfo('收货人', '收货人姓名', ''),
  10.       new ConsigneeInfo('电话', '收货人电话', ''),
  11.       new ConsigneeInfo('地址', '地址', ''),
  12.     ];
  13.   }
  14.   build() {
  15.     RelativeContainer() {
  16.       // 界面主体内容
  17.       Column() {
  18.         Text('新增收货地址')
  19.           .id('title')
  20.           .width('100%')
  21.           .font({ size: 26, weight: 700 })
  22.           .fontColor('#000000')
  23.           .opacity(0.9)
  24.           .height(64)
  25.           .align(Alignment.TopStart)
  26.         Text('地址信息')
  27.           .width('100%')
  28.           .padding({ left: 12, right: 12 })
  29.           .font({ size: 14, weight: 400 })
  30.           .fontColor('#000000')
  31.           .opacity(0.6)
  32.           .lineHeight(19)
  33.           .margin({ bottom: 8 })
  34.         // 识别或填写区域
  35.         Column() {
  36.           TextArea({
  37.             placeholder: '图片识别的文本自动显示到此处(也可手动复制文本到此处),将自动识别收货信息。例:大美丽,182*******,四川省成都市天府新区某小区',
  38.           })
  39.             .height(100)
  40.             .margin({ bottom: 12 })
  41.             .backgroundColor('#FFFFFF')
  42.           Row({ space: 12 }) {
  43.             Button() {
  44.               Row({ space: 8 }) {
  45.                 Text() {
  46.                   SymbolSpan($r('sys.symbol.camera'))
  47.                     .fontSize(26)
  48.                     .fontColor(['#0A59F2'])
  49.                 }
  50.                 Text('图片识别')
  51.                   .fontSize(16)
  52.                   .fontColor('#0A59F2')
  53.               }
  54.             }
  55.             .height(40)
  56.             .layoutWeight(1)
  57.             .backgroundColor('#F1F3F5')
  58.             Button('地址解析')
  59.               .height(40)
  60.               .layoutWeight(1)
  61.           }
  62.           .width('100%')
  63.           .padding({
  64.             left: 16,
  65.             right: 16,
  66.           })
  67.         }
  68.         .backgroundColor('#FFFFFF')
  69.         .borderRadius(16)
  70.         .padding({
  71.           top: 16,
  72.           bottom: 16
  73.         })
  74.         // 收货人信息
  75.         Column() {
  76.           List() {
  77.             //列表渲染,避免重复代码
  78.             ForEach(this.consigneeInfos, (item: ConsigneeInfo) => {
  79.               ListItem() {
  80.                 Row() {
  81.                   Text(item.label)
  82.                     .fontSize(16)
  83.                     .fontWeight(400)
  84.                     .lineHeight(19)
  85.                     .textAlign(TextAlign.Start)
  86.                     .fontColor('#000000')
  87.                     .opacity(0.9)
  88.                     .layoutWeight(1)
  89.                   TextArea({ placeholder: item.placeholder, text: item.value })
  90.                     .type(item.label === '收货人' ? TextAreaType.PHONE_NUMBER : TextAreaType.NORMAL)
  91.                     .fontSize(16)
  92.                     .fontWeight(500)
  93.                     .lineHeight(21)
  94.                     .padding(0)
  95.                     .borderRadius(0)
  96.                     .textAlign(TextAlign.End)
  97.                     .fontColor('#000000')
  98.                     .opacity(0.9)
  99.                     .backgroundColor('#FFFFFF')
  100.                     .heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
  101.                     .layoutWeight(2)
  102.                     .onChange((value: string) => {
  103.                       item.value = value;
  104.                       //判断保存按钮是否可用(均填写即可保存)
  105.                       if (this.consigneeInfos[0].value && this.consigneeInfos[1].value && this.consigneeInfos[2].value) {
  106.                         this.saveAvailable = true;
  107.                       } else {
  108.                         this.saveAvailable = false;
  109.                       }
  110.                     })
  111.                 }
  112.                 .width('100%')
  113.                 .constraintSize({ minHeight: 48 })
  114.                 .justifyContent(FlexAlign.SpaceBetween)
  115.               }
  116.             }, (item: ConsigneeInfo, index: number) => JSON.stringify(item) + index)
  117.           }
  118.           .width('100%')
  119.           .height(LayoutPolicy.matchParent)
  120.           .scrollBar(BarState.Off)
  121.           .divider({ strokeWidth: 0.5, color: '#33000000' })
  122.           .padding({
  123.             left: 12,
  124.             right: 12,
  125.             top: 4,
  126.             bottom: 4
  127.           })
  128.           .borderRadius(16)
  129.           .backgroundColor('#FFFFFF')
  130.         }
  131.         .borderRadius(16)
  132.         .margin({ top: 12 })
  133.         .constraintSize({ minHeight: 150, maxHeight: '50%' })
  134.         .backgroundColor('#FFFFFF')
  135.       }
  136.       // 保存按钮
  137.       if (this.saveAvailable) {
  138.         // 可用状态
  139.         Button("保存地址", { stateEffect: true })
  140.           .width('100%')
  141.           .alignRules({
  142.             bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
  143.           })
  144.       } else {
  145.         // 不可用状态
  146.         Button("保存地址", { stateEffect: false })
  147.           .width('100%')
  148.           .alignRules({
  149.             bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
  150.           })
  151.           .opacity(0.4)
  152.           .backgroundColor('#317AFF')
  153.       }
  154.     }
  155.     .height('100%')
  156.     .width('100%')
  157.     .padding({
  158.       left: 16,
  159.       right: 16,
  160.       top: 24,
  161.       bottom: 24
  162.     })
  163.     .backgroundColor('#F1F3F5')
  164.     .alignRules({
  165.       left: { anchor: '__container__', align: HorizontalAlign.Start }
  166.     })
  167.     .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  168.   }
  169. }
复制代码
该界面中通过RelativeContainer进行相对布局,保存按钮通过相对布局定位到界面底部,主要内容区域使用Column布局。在aboutToAppear周期函数中,初始化收货人列表数据,界面中通过List列表渲染完成显示。至此,主界面的静态效果就实现了。
但主界面代码依然较多,可以考虑将List列表渲染部分抽取为单独的组件,提取到单独文件中。在ets目录下新建components目录,在该目录下新建ConsigneeInfoItem.ets文件,将主界面列表渲染部分的内容拷贝进去并进改造。
ConsigneeInfoItem.ets文件内容:
  1. import { ConsigneeInfo } from '../viewmodel/DataModel';
  2. @Builder
  3. export function ConsigneeInfoItem(item: ConsigneeInfo, checkAvailable?: () => void) {
  4.   Row() {
  5.     Text(item.label)
  6.       .fontSize(16)
  7.       .fontWeight(400)
  8.       .lineHeight(19)
  9.       .textAlign(TextAlign.Start)
  10.       .fontColor('#000000')
  11.       .opacity(0.9)
  12.       .layoutWeight(1)
  13.     TextArea({ placeholder: item.placeholder, text: item.value })
  14.       .type(item.label === '收货人' ? TextAreaType.PHONE_NUMBER : TextAreaType.NORMAL)
  15.       .fontSize(16)
  16.       .fontWeight(500)
  17.       .lineHeight(21)
  18.       .padding(0)
  19.       .borderRadius(0)
  20.       .textAlign(TextAlign.End)
  21.       .fontColor('#000000')
  22.       .opacity(0.9)
  23.       .backgroundColor('#FFFFFF')
  24.       .heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
  25.       .layoutWeight(2)
  26.       .onChange((value: string) => {
  27.         item.value = value;
  28.         //判断保存按钮是否可用(均填写即可保存)
  29.         checkAvailable?.();
  30.       })
  31.   }
  32.   .width('100%')
  33.   .constraintSize({ minHeight: 48 })
  34.   .justifyContent(FlexAlign.SpaceBetween)
  35. }
复制代码
Index.ets改造
  1. import  {ConsigneeInfoItem} from '../components/ConsigneeInfoItem'
  2. ...
  3. ListItem() {
  4.                 //抽取为组件
  5.                 ConsigneeInfoItem(item,() => {
  6.                   if (this.consigneeInfos[0].value && this.consigneeInfos[1].value && this.consigneeInfos[2].value) {
  7.                     this.saveAvailable = true;
  8.                   } else {
  9.                     this.saveAvailable = false;
  10.                   }
  11.                 })
  12.               }
  13. ...
复制代码
主界面效果实现完成
2.2 图片识别弹窗

点击“图片识别”按钮,弹出获取图片方式选择框。接下来完成该界面制作。
为了弹出框管理更加方面,封装弹窗口管理工具类PromptActionManager,在ets目录新建utils目录,新建PromptActionManager.ets文件,内容如下:
  1. import { promptAction } from '@kit.ArkUI';
  2. /**
  3. * Dialog管理类
  4. */
  5. export class PromptActionManager {
  6.   static ctx: UIContext;
  7.   static contentNode: ComponentContent<Object>;
  8.   static options: promptAction.BaseDialogOptions;
  9.   static setCtx(ctx: UIContext) {
  10.     PromptActionManager.ctx = ctx;
  11.   }
  12.   static setContentNode(contentNode: ComponentContent<Object>) {
  13.     PromptActionManager.contentNode = contentNode;
  14.   }
  15.   static setOptions(options: promptAction.BaseDialogOptions) {
  16.     PromptActionManager.options = options;
  17.   }
  18.   static openCustomDialog() {
  19.     if (!PromptActionManager.contentNode) {
  20.       return;
  21.     }
  22.     try {
  23.       PromptActionManager.ctx.getPromptAction().openCustomDialog(
  24.         PromptActionManager.contentNode,
  25.         PromptActionManager.options
  26.       )
  27.     } catch (error) {
  28.     }
  29.   }
  30.   static closeCustomDialog() {
  31.     if (!PromptActionManager.contentNode) {
  32.       return;
  33.     }
  34.     try {
  35.       PromptActionManager.ctx.getPromptAction().closeCustomDialog(
  36.         PromptActionManager.contentNode
  37.       )
  38.     } catch (error) {
  39.     }
  40.   }
  41. }
复制代码
不同的弹出界面可能需要不同的样式控制,因此为弹出框的控制定义参数类型Params。在DataModel.ets文件中新加类Params,代码如下:
  1. ...
  2. export class Params {
  3.   uiContext: UIContext;
  4.   textAreaController: TextAreaController;     //识别信息的TextArea
  5.   loadingController: CustomDialogController;  //识别过程中的加载提示框
  6.   constructor(uiContext: UIContext, textAreaController: TextAreaController, loadingController: CustomDialogController) {
  7.     this.uiContext = uiContext;
  8.     this.textAreaController = textAreaController;
  9.     this.loadingController = loadingController;
  10.   }
  11. }
复制代码
接下来制作弹出框组件界面,在components目录新建dialogBuilder.ets文件
  1. import { Params } from '../viewmodel/DataModel'
  2. import { PromptActionManager } from '../common/utils/PromptActionManager'
  3. @Builder
  4. export function dialogBuilder(params: Params): void {
  5.   Column() {
  6.     Text('图片识别')
  7.       .font({ size: 20, weight: 700 })
  8.       .lineHeight(27)
  9.       .margin({ bottom: 16 })
  10.     Text('选择获取图片的方式')
  11.       .font({ size: 16, weight: 50 })
  12.       .lineHeight(21)
  13.       .margin({ bottom: 8 })
  14.     Column({ space: 8 }) {
  15.       Button('拍照')
  16.         .width('100%')
  17.         .height(40)
  18.       Button('相册')
  19.         .width('100%')
  20.         .height(40)
  21.         .fontColor('#0A59F2')
  22.         .backgroundColor('#FFFFFF')
  23.       Button('取消')
  24.         .width('100%')
  25.         .height(40)
  26.         .fontColor('#0A59F2')
  27.         .backgroundColor('#FFFFFF')
  28.         .onClick(() => {
  29.           PromptActionManager.closeCustomDialog();
  30.         })
  31.     }
  32.   }
  33.   .size({ width: 'calc(100% - 32vp)', height: 235 })
  34.   .borderRadius(32)
  35.   .backgroundColor('#FFFFFF')
  36.   .padding(16)
  37. }
复制代码
修改Index.ets文件,为“图片识别”按钮绑定事件,点击时弹出自定义对话框。
  1. ...
  2. import { PromptActionManager } from '../common/utils/PromptActionManager';
  3. import { ComponentContent, LoadingDialog } from '@kit.ArkUI';
  4. import { ConsigneeInfo,Params} from '../viewmodel/DataModel';
  5. import { dialogBuilder } from '../components/dialogBuilder';
  6. ...
  7. private uiContext: UIContext = this.getUIContext();
  8. private resultController: TextAreaController = new TextAreaController();
  9. private loadingController: CustomDialogController = new CustomDialogController({
  10.     builder: LoadingDialog({
  11.       content: '图片识别中'
  12.     }),
  13.     autoCancel: false
  14.   });
  15. private contentNode: ComponentContent<Object> =
  16.     new ComponentContent(this.uiContext, wrapBuilder(dialogBuilder),
  17.       new Params(this.uiContext, this.resultController, this.loadingController));
  18. ...
  19. //图片识别按钮
  20. .onClick(() => {
  21.               PromptActionManager.openCustomDialog();
  22.             })
复制代码
静态界面制作完成。
三、功能实现

1、通用功能封装

创建OCR识别管理类OCRManager,需要用到HarmonyOS SDK中的AI和媒体两类Kit。在utils目录下新建OCRManager.ets,封装相关方法。
  1. import { textRecognition } from '@kit.CoreVisionKit';
  2. import { camera, cameraPicker } from '@kit.CameraKit';
  3. import { photoAccessHelper } from '@kit.MediaLibraryKit';
  4. import { image } from '@kit.ImageKit';
  5. import { fileIo as fs } from '@kit.CoreFileKit';
  6. export class OCRManager {
  7.   static async recognizeByCamera(ctx: Context, loadingController: CustomDialogController): Promise<string> {
  8.     // The configuration information of cameraPicker
  9.     let pickProfile: cameraPicker.PickerProfile = {
  10.       cameraPosition: camera.CameraPosition.CAMERA_POSITION_UNSPECIFIED
  11.     };
  12.     try {
  13.       let result: cameraPicker.PickerResult =
  14.         await cameraPicker.pick(ctx, [cameraPicker.PickerMediaType.PHOTO], pickProfile);
  15.       if (!result || !result.resultUri) {
  16.         return '';
  17.       }
  18.       loadingController.open();
  19.       return OCRManager.recognizeText(result.resultUri);
  20.     } catch (error) {
  21.       loadingController.close();
  22.       return '';
  23.     }
  24.   }
  25.   static async recognizeByAlbum(loadingController: CustomDialogController): Promise<string> {
  26.     try {
  27.       let photoPicker: photoAccessHelper.PhotoViewPicker = new photoAccessHelper.PhotoViewPicker();
  28.       let photoResult: photoAccessHelper.PhotoSelectResult =
  29.         await photoPicker.select({
  30.           MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
  31.           maxSelectNumber: 1,
  32.           isPhotoTakingSupported: false
  33.         });
  34.       if (!photoResult || photoResult.photoUris.length === 0) {
  35.         return '';
  36.       }
  37.       loadingController.open();
  38.       return OCRManager.recognizeText(photoResult.photoUris[0]);
  39.     } catch (error) {
  40.       loadingController.close();
  41.        return '';
  42.     }
  43.   }
  44.   static async recognizeText(uri: string): Promise<string> {
  45.     // Visual information to be recognized.
  46.     // Currently, only the visual information of the PixelMap type in color data format RGBA_8888 is supported.
  47.     let visionInfo: textRecognition.VisionInfo = { pixelMap: await OCRManager.getPixelMap(uri) };
  48.     let result: textRecognition.TextRecognitionResult = await textRecognition.recognizeText(visionInfo);
  49.     visionInfo.pixelMap.release();
  50.     return result.value;
  51.   }
  52.   static async getPixelMap(uri: string): Promise<image.PixelMap> {
  53.     // Convert image resources to PixelMap
  54.     let fileSource = await fs.open(uri, fs.OpenMode.READ_ONLY);
  55.     let imgSource: image.ImageSource = image.createImageSource(fileSource.fd);
  56.     let pixelMap: image.PixelMap = await imgSource.createPixelMap();
  57.     fs.close(fileSource);
  58.     imgSource.release();
  59.     return pixelMap;
  60.   }
  61. }
复制代码
2、拍照识别

弹出框界面,为“拍照”按钮绑定事件
  1. import { common } from '@kit.AbilityKit'
  2. import { OCRManager } from '../common/utils/OCRManager'
  3. //拍照按钮
  4. .onClick(async () => {
  5.           PromptActionManager.closeCustomDialog();
  6.           let text: string =
  7.             await OCRManager.recognizeByCamera(params.uiContext.getHostContext() as common.UIAbilityContext,
  8.               params.loadingController);
  9.           params.loadingController.close();
  10.           if (text) {
  11.             params.textAreaController.deleteText();
  12.             params.textAreaController.addText(text);
  13.           }
  14.         })
  15.         
复制代码
主界面,修改TextArea,传入controller并双向绑定识别结果。
  1. ...
  2. @State ocrResult: string = '';
  3. ...
  4. //TextArea
  5. controller: this.resultController,
  6. text: $$this.ocrResult
复制代码
拍照识别功能实现。
3、相册识别

弹出框界面,为“相册”按钮绑定事件
  1. //相册按钮
  2. .onClick(async () => {
  3.           PromptActionManager.closeCustomDialog();
  4.           let text: string =
  5.             await OCRManager.recognizeByAlbum(params.loadingController);
  6.           params.loadingController.close();
  7.           if (text) {
  8.             params.textAreaController.deleteText();
  9.             params.textAreaController.addText(text);
  10.           }
  11.         })
复制代码
相册识别功能实现。
4、地址解析

主界面的“地址解析”按钮,将通过图片识别或手工输入的地址信息,解析显示到对应的输入框中。
封装地址解析类AddressParse,本案例使用正则表达式进行匹配。后续可以使用大模型或NLP工具进行解析。
在utils目录下新建AddressParse.ets文件
  1. import { ConsigneeInfo } from '../../viewmodel/DataModel';
  2. export class AddressParse {
  3.   static nameBeforeRegex = /([\w\u4e00-\u9fa5]+[\s\,\,\。]+|[\s\,\,\。]*)([\u4e00-\u9fa5]{2,4})[\s\,\,\。]+/;
  4.   static nameAfterRegex = /[\s\,\,\。]+([\u4e00-\u9fa5]{2,4})[\s\,\,\。]*/;
  5.   static nameTagRegex = /(?:收货人|收件人|姓名|联系人)[::\s]*([\u4e00-\u9fa5]{2,4})/i;
  6.   static namePlainRegex = /[\u4e00-\u9fa5]{2,4}/;
  7.   static phoneRegex =
  8.     /(1[3-9]\d[\s-]?\d{4}[\s-]?\d{4})|(\d{3,4}[\s-]?\d{7,8})|(\(\d{2,4}\)[\s-]?\d{4,8})|(\+\d{1,4}[\s-]?\d{5,15})/g;
  9.   static phoneHyphenRegex = /[\(\)\s-]/g;
  10.   static addressKeywords =
  11.     ['收货地址', '收件地址', '配送地址', '所在地区', '位置',
  12.       '地址', '寄至', '寄往', '送至', '详细地址'];
  13.   static addressNoiseWords = ['收货人', '收件人', '姓名', '联系人', '电话', '手机', '联系方式', ':', ':', ',', ','];
  14.   static extractInfo(text: string, info: ConsigneeInfo[]): ConsigneeInfo[] {
  15.     const baseText: string = text.replace(/\s+/g, ' ')
  16.     const phoneResult: string = AddressParse.extractPhone(baseText);
  17.     const nameResult: string = AddressParse.extractName(baseText, phoneResult);
  18.     const addressResult: string = AddressParse.extractAddress(baseText, phoneResult, nameResult);
  19.     info[0].value = nameResult;
  20.     info[1].value = phoneResult.replace(AddressParse.phoneHyphenRegex, '');
  21.     info[2].value = addressResult;
  22.     return info;
  23.   }
  24.   static extractPhone(text: string): string {
  25.     const phoneMatch: RegExpMatchArray | null = text.match(AddressParse.phoneRegex);
  26.     return phoneMatch ? phoneMatch[0] : '';
  27.   }
  28.   static extractName(text: string, phone: string): string {
  29.     let name = '';
  30.     // Try to extract from the label
  31.     const nameFromTag = text.match(AddressParse.nameTagRegex);
  32.     if (nameFromTag) {
  33.       name = nameFromTag[1];
  34.     }
  35.     // Try to extract before or after the phone
  36.     if (!name && phone) {
  37.       const phoneIndex = text.indexOf(phone);
  38.       const beforePhone = text.substring(0, phoneIndex);
  39.       const nameBefore = beforePhone.match(AddressParse.nameBeforeRegex);
  40.       if (nameBefore) {
  41.         name = nameBefore[2];
  42.       }
  43.       if (!name) {
  44.         const afterPhone = text.substring(phoneIndex + phone.length);
  45.         const nameAfter = afterPhone.match(AddressParse.nameAfterRegex);
  46.         if (nameAfter) {
  47.           name = nameAfter[1];
  48.         }
  49.       }
  50.     }
  51.     // Try to extract 2-4 Chinese characters directly
  52.     if (!name) {
  53.       const nameMatch = text.match(AddressParse.namePlainRegex);
  54.       if (nameMatch) {
  55.         name = nameMatch[0];
  56.       }
  57.     }
  58.     return name;
  59.   }
  60.   static extractAddress(text: string, phone: string, name: string): string {
  61.     for (const keyword of AddressParse.addressKeywords) {
  62.       const keywordIndex = text.indexOf(keyword);
  63.       if (keywordIndex !== -1) {
  64.         const possibleAddress = text.substring(keywordIndex + keyword.length).trim();
  65.         // Clean up the beginning punctuation
  66.         const cleanedAddress = possibleAddress.replace(/^[::,,。、\s]+/, '');
  67.         if (cleanedAddress.length > 5) {
  68.           return cleanedAddress;
  69.         }
  70.       }
  71.     }
  72.     // Try to remove name and phone number
  73.     let cleanedText = text;
  74.     if (name) {
  75.       cleanedText = cleanedText.replace(name, '');
  76.     }
  77.     if (phone) {
  78.       cleanedText = cleanedText.replace(phone, '');
  79.     }
  80.     // Remove common distracting words
  81.     AddressParse.addressNoiseWords.forEach(word => {
  82.       cleanedText = cleanedText.replace(word, '');
  83.     });
  84.     // Extract the longest text segment that may contain an address
  85.     const segments = cleanedText.split(/[\s,,。;;]+/).filter(seg => seg.length > 4);
  86.     if (segments.length > 0) {
  87.       // The segment containing the address key is preferred
  88.       const addressSegments = segments.filter(seg =>
  89.       seg.includes('省') || seg.includes('市') || seg.includes('区') ||
  90.       seg.includes('县') || seg.includes('路') || seg.includes('街') ||
  91.       seg.includes('号') || seg.includes('栋') || seg.includes('单元')
  92.       );
  93.       if (addressSegments.length > 0) {
  94.         return addressSegments.join(' ');
  95.       }
  96.       // Otherwise select the longest segment
  97.       return segments.reduce((longest, current) =>
  98.       current.length > longest.length ? current : longest, '');
  99.     }
  100.     // Finally, return the entire text
  101.     return cleanedText;
  102.   }
  103. }
复制代码
在主文件中调用地址解析方法,修改Index.ets文件
  1. import { AddressParse } from '../common/utils/AddressParse';
  2. ...
  3. //地址解析按钮
  4. .onClick(() => {
  5.                 if (!this.ocrResult || !this.ocrResult.trim()) {
  6.                   this.uiContext.getPromptAction().showToast({ message: $r('app.string.empty_toast') });
  7.                   return;
  8.                 }
  9.                 this.consigneeInfos = AddressParse.extractInfo(this.ocrResult, this.consigneeInfos);
  10.               })
复制代码
5、保存地址

为保存按钮绑定事件,清空界面数据并提示保存结果。
修改Index.ets文件,封装clearConsigneeInfos模拟保存操作后清空数据。
  1. ...
  2. // 保存地址,清空内容
  3.   clearConsigneeInfos() {
  4.     for (const item of this.consigneeInfos) {
  5.       item.value = '';
  6.     }
  7.     this.ocrResult = '';
  8.   }
  9. ...
  10. //保存地址按钮
  11. .onClick(() => {
  12.             if (this.consigneeInfos[0].value && this.consigneeInfos[1].value && this.consigneeInfos[2].value) {
  13.               this.uiContext.getPromptAction().showToast({ message: '保存成功' });
  14.               this.clearConsigneeInfos();
  15.             }
  16.           })
复制代码
至此,功能完成。
《鸿蒙应用开发从入门到项目实战》系列文章持续更新中,欢迎关注!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册