{"id":23430124,"url":"https://github.com/srect/harmony-arkts-todolist","last_synced_at":"2025-04-09T14:18:21.378Z","repository":{"id":265424266,"uuid":"895951221","full_name":"sRect/Harmony-Arkts-todolist","owner":"sRect","description":"HarmonyOS todolist demo","archived":false,"fork":false,"pushed_at":"2024-12-23T02:09:08.000Z","size":4987,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-15T08:15:43.660Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sRect.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-29T08:48:56.000Z","updated_at":"2024-12-23T02:09:11.000Z","dependencies_parsed_at":null,"dependency_job_id":"94e1479a-f514-4e5b-94d4-7874023b1795","html_url":"https://github.com/sRect/Harmony-Arkts-todolist","commit_stats":null,"previous_names":["srect/harmonyos-demo","srect/harmonyos-arkts-todolist"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2FHarmony-Arkts-todolist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2FHarmony-Arkts-todolist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2FHarmony-Arkts-todolist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2FHarmony-Arkts-todolist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sRect","download_url":"https://codeload.github.com/sRect/Harmony-Arkts-todolist/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248054196,"owners_count":21039952,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-23T08:15:49.065Z","updated_at":"2025-04-09T14:18:21.342Z","avatar_url":"https://github.com/sRect.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 使用ArkTS、Uni-app、Taro开发鸿蒙 todolist对比\n\n\u003e 本文将通过使用ArkTS、Uni-app、Taro三种方式，分别完成todolist小demo。\n\n**1. 本文操作系统及主要package version**\n\n| name | version |\n| --- | --- | \n| 操作系统 | macOS |\n| Nodejs | v20.9.0 |\n| DevEco-Studio | 5.0.0 Release |\n| HbuilderX | 4.36 |\n| @tarojs/cli | v4.0.8 |\n\n**2. 本文github仓库地址**\n\n| name | url |\n| --- | --- | \n|Harmony-Arkts-todolist|https://github.com/sRect/Harmony-Arkts-todolist|\n|Harmony-uniapp-todolist|https://github.com/sRect/Harmony-uniapp-todolist|\n|Harmony-taro-todolist|https://github.com/sRect/Harmony-taro-todolist|\n\n## 1. 前置条件\n\n1. 需要进行华为开发者实名认证\n2. [`ArkTS`](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-get-started-V5)\n3. [`uniapp + Vue3`](https://uniapp.dcloud.net.cn/tutorial/harmony/intro.html)\n4. [`Taro + React`](https://taro-docs.jd.com/docs/harmony)\n\n## 2. 使用ArkTS\n\n\u003e 下载好`DevEco Studio`，[跟着鸿蒙文档快速上手hello world](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/start-with-ets-stage-V5)\n\n### 2.1 添加Todolist页面\n\n新建文件 `entry \u003e src \u003e main \u003e ets \u003e pages \u003e Todolist.ets`\n\n\n### 2.2 添加todolist路由\n\n`entry \u003e src \u003e main \u003e resources \u003e base \u003e profile \u003e main_pages.json`\n\n```json\n{\n  \"src\": [\n    \"pages/Index\",\n    \"pages/Todolist\"\n  ]\n}\n```\n\n### 2.3 首页增加跳转按钮\n\n`entry \u003e src \u003e main \u003e ets \u003e pages \u003e Index.ets`\n\n```typescript\nimport { router } from \"@kit.ArkUI\";\nimport { BusinessError } from '@kit.BasicServicesKit';\n\nButton() {\n Text('goto todolist')\n  .fontSize(30)\n  .fontWeight(FontWeight.Normal)\n  .fontColor('white')\n}\n  .type(ButtonType.Capsule)\n  .backgroundColor(\"#0D9FFB\")\n  .width('auto')\n  .height('auto')\n  .padding('10 5')\n  .onClick(() =\u003e {\n    try {\n      router.pushUrl({\n        url: \"pages/Todolist\"\n      });\n    }catch(e) {\n      const errMsg = (e as BusinessError).message;\n      console.error(errMsg);\n    }\n  })\n```\n\n### 2.4 完成Todolist页面\n\n\u003e [布局文档](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-layout-development-linear-V5)，有web开发人员熟悉的Flex、Row、Column等布局方式，直接干\n\n![](markdown-static/todolist.png)\n\n1. 首先完成顶部输入框+提交按钮\n\n```typescript\nexport interface ICurEditObj {\n    val: string;\n    index: number;\n}\n\n@Entry\n@Component\nstruct Todolist {\n  @State inputVal: string = ''; // 输入框内容\n  @State private list: string[] = []; // 列表\n  @State curEditObj: ICurEditObj = { val: \"\", index: 0 }; // 当前修改项\n  scroller:Scroller = new Scroller();\n  \n  build() {\n    Flex({direction: FlexDirection.Column}) {\n      // 顶部输入\n      Row({space: 10}) {\n        TextInput({text: this.inputVal, placeholder: '请输入...'})\n          .placeholderFont({ size: 14, weight: 400 })\n          .placeholderColor(Color.Gray)\n          .width('76%')\n          .height('100%')\n          .fontSize(14)\n          .fontColor('#333333')\n          .type(InputType.Normal)\n          .onChange((val: string) =\u003e {\n            this.inputVal = val;\n          })\n\n        Button() {\n          Text('提交')\n            .fontSize(18)\n            .fontWeight(FontWeight.Normal)\n            .fontColor('white')\n        }\n          .type(ButtonType.Capsule)\n          .width('24%')\n          .height('100%')\n          .flexShrink(1)\n          .backgroundColor('#06BA8C')\n          .onClick(():void =\u003e {\n            console.log('click:', this.inputVal);\n            \n            if(this.inputVal === \"\" || this.inputVal.trim() === \"\") {\n              // 这里暂时没找到Toast的方法，先用AlertDialog代替\n              AlertDialog.show({\n                title: \"\",\n                message: \"内容不可为空\",\n                isModal: true,\n                autoCancel: true,\n                alignment: DialogAlignment.Center,\n                borderWidth: 0,\n                cornerRadius: 10,\n                width: '50%',\n                gridCount: 1\n              });\n              return;\n            }\n\n            this.list.unshift(this.inputVal);\n            this.inputVal = \"\";\n          });\n      }.width('100%').height(40);\n\n      // 中间列表滚动区域\n      Scroll(this.scroller) {\n        // list item\n      }\n        .backgroundColor('white')\n        .scrollable(ScrollDirection.Vertical) // 滚动方向为垂直方向\n          // .scrollBar(BarState.On) // 滚动条常驻显示\n        .scrollBarColor(Color.Gray) // 滚动条颜色\n        .scrollBarWidth(5) // 滚动条宽度\n        .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹\n        .height('auto')\n        .margin({top: 20})\n        .padding(10)\n        .borderRadius(4)\n    }\n      .height('100%')\n      .width('100%')\n      .backgroundColor('#f6f6f6')\n      .padding(10)\n  }\n}\n```\n\n2. 接着完成中间列表滚动部分\n\n    + [if/else条件渲染](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-rendering-control-ifelse-V5)\n    + [ForEach循环渲染](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-rendering-control-foreach-V5)\n  \n```typescript\nScroll(this.scroller) {\n  Column({space:10}) {\n    if(this.list.length \u003e 0) {\n      ForEach(this.list, (item?:string, index?:number) =\u003e {\n        if(item \u0026\u0026 typeof index === 'number') {\n          Row({space: 10}) {\n            Text(`${index+1}. ${item}`)\n              .fontSize(14)\n              .fontColor('#333333')\n              .width('58%')\n              .wordBreak(WordBreak.BREAK_ALL)\n              .maxLines(2)\n              .textOverflow({ overflow: TextOverflow.MARQUEE })\n\n            Row({space: 5}) {\n              Button() {\n                Text('编辑').fontSize(14).fontColor(Color.White)\n              }\n              .width('50%')\n                .height('100%')\n                .flexShrink(0)\n                .flexGrow(0)\n                .flexBasis('50%')\n                .backgroundColor(Color.Blue)\n                .onClick(() =\u003e {\n                  // 打开自定义模态框\n                  console.log(\"当前数据：\", item);\n                })\n\n              Button() {\n                Text('删除').fontSize(14).fontColor(Color.White)\n              }\n              .type(ButtonType.Capsule)\n                .width('50%')\n                .height('100%')\n                .flexShrink(0)\n                .flexGrow(0)\n                .flexBasis('50%')\n                .backgroundColor(Color.Orange)\n                .onClick(() =\u003e {\n                  this.list.splice(index, 1);\n                })\n            }.width('40%').height('100%').flexShrink(0);\n          }\n          .justifyContent(FlexAlign.SpaceBetween)\n            .alignItems(VerticalAlign.Center)\n            .width('100%')\n            .flexGrow(0)\n            .height('50')\n            .backgroundColor('#f0f0f0')\n            .padding('10')\n            .borderRadius(4);\n        }\n      })\n    } else {\n      Text('暂无数据').fontColor(Color.Gray).margin({top: '30%', bottom: '30%'});\n    }\n  }\n    .width('100%')\n    .height('auto')\n    .justifyContent(FlexAlign.Start)\n    .alignItems(HorizontalAlign.Center)\n}\n```\n\n3. 接着完成列表item弹框修改\n\n    + [自定义弹窗 (CustomDialog)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-common-components-custom-dialog-V5)\n\n```typescript\n@Component\n@CustomDialog\nstruct CustomDialogExample {\n  @State curEditVal: string = \"\";\n  cancel?: () =\u003e void\n  confirm?: (val?: string) =\u003e void\n  controller: CustomDialogController\n\n  build() {\n    Column() {\n      Text(\"请修改\").fontSize(20).fontWeight(FontWeight.Bold)\n      TextInput({text: this.curEditVal, placeholder: \"请输入\"})\n        .fontSize(14)\n        .fontColor('#333333')\n        .onChange((val: string) =\u003e {\n          this.curEditVal = val;\n        });\n\n      Row() {\n        Button('取消')\n          .onClick(() =\u003e {\n            this.controller.close();\n            this.curEditVal = \"\";\n\n            if (this.cancel) {\n              this.cancel()\n            }\n          })\n          .backgroundColor(\"#cccccc\")\n          .fontColor(Color.Black)\n          .type(ButtonType.Capsule)\n\n        Button('确定')\n          .onClick(() =\u003e {\n            this.controller.close()\n            if (this.confirm) {\n              // 点击确定修改，将当前的值传回去\n              this.confirm(this.curEditVal)\n            }\n          })\n          .backgroundColor('#06BA8C')\n          .fontColor(Color.White)\n          .type(ButtonType.Capsule)\n      }.width('100%').alignItems(VerticalAlign.Center).justifyContent(FlexAlign.SpaceAround);\n    }\n      .padding('20 12')\n      .height('30%')\n      .justifyContent(FlexAlign.SpaceBetween)\n  }\n}\n```\n\n```typescript\n@Entry\n@Component\nstruct Todolist {\n  // ...\n  // 创建构造器，与上面装饰器呼应相连\n  dialogController: CustomDialogController = new CustomDialogController({\n    builder: CustomDialogExample({\n      confirm: (val?:string)=\u003e {\n        console.info(\"获取到模态框输入值：\", val)\n        if(val) {\n          // 获取到dialog确定返回的新值，修改原数据\n          this.list[this.curEditObj.index] = val;\n        }\n      }\n    }),\n    alignment: DialogAlignment.Center,\n    cornerRadius: 10,\n  })\n}\n```\n\n4. 点击按钮，打开自定义弹框\n\n![](markdown-static/todolist-arkts.gif)\n\n```typescript\nButton() {\n  Text('编辑').fontSize(14).fontColor(Color.White)\n}\n    .width('50%')\n    .height('100%')\n    .flexShrink(0)\n    .flexGrow(0)\n    .flexBasis('50%')\n    .backgroundColor(Color.Blue)\n    .onClick(() =\u003e {\n      console.log(\"当前数据：\", item);\n      this.curEditObj = {\n        val: item,\n        index: index\n      }\n      \n      // 打开自定义模态框\n      this.dialogController.open();\n    })\n```\n\n美中不足的是，打开弹框的时候，没有把当前的值显示在弹框上，这样方便在原先值的基础上进行修改。\n\n### 2.5 调用系统相机api进行拍照\n\n\u003e [通过系统相机拍照和录像(ArkTS)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/camera-picker-V5)\n\n**注意：**\n- 调用CameraPicker拍摄照片或录制视频，无需申请相机权限\n- 应用调试时，开发者需在release模式下调用系统相机（CameraPicker）\n\n```typescript\nimport { camera, cameraPicker as picker } from '@kit.CameraKit'\nimport { fileIo, fileUri } from '@kit.CoreFileKit'\n\nlet pathDir = getContext().filesDir;\nlet fileName = `${new Date().getTime()}`\nlet filePath = pathDir + `/${fileName}.tmp`\nfileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);\n\nlet uri = fileUri.getUriFromPath(filePath);\nlet pickerProfile: picker.PickerProfile = {\n   cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,\n   saveUri: uri\n};\nlet result: picker.PickerResult =\n   await picker.pick(getContext(), [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO],\n      pickerProfile);\nconsole.info(`picker resultCode: ${result.resultCode},resultUri: ${result.resultUri},mediaType: ${result.mediaType}`);\nif (result.resultCode == 0) {\n   if (result.mediaType === picker.PickerMediaType.PHOTO) {\n      this.imgSrc = result.resultUri;\n   } else {\n      this.videoSrc = result.resultUri;\n   }\n}\n```\n\n### 2.5 云真机调试\n\n#### 2.5.1 准备数字签名证书\n\n\u003e 由于本地没有华为鸿蒙真机，登录[AppGallery Connect](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/)，把打包好的release包上传，选择对应的机型，进行测试。如果本地有华为鸿蒙机型，直接本地调试\n\n1. [Stage模型下两个重要文件](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/application-configuration-file-overview-stage-V5)\n - `app.json5配置文件`：应用的全局配置信息，包含应用的Bundle名称、开发厂商、版本号等基本信息\n - `module.json5配置文件`：包含Module名称、类型、描述、支持的设备类型等基本信息、权限信息等\n\n2. [AppGallery Connect管理中心-创建项目](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/myProject)，进入到`证书、APP ID和Profile`模块，新增证书，选择`发布证书`，证书需要的`CSR`文件，在DevEco Studio里生成\n\n3. [HarmonyOS应用/元服务发布](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-publish-app-V5)，跟着文档，这一步生成两个文件，`xxx_release.csr`和`xxx_release.p12`\n\n4. 上面第2步需要的csr文件选择上传，生成release证书，点击`xxx_release.cer`文件下载到本地\n\n5. [AppGallery Connect管理中心-\n   证书、APP ID和Profile-Profile](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/harmonyOSDevPlatform)，新增Profile，`xxx.p7b`下载到本地\n\n6. [DevEco Studio配置工程的签名信息](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-publish-app-V5), 跟着文档，选择对应的文件，点击`Apply`+`OK`\n\n上面6步，一共产生了4个文件。\n \n- xxx.csr\n- xxx.p12\n- xxx.cer\n- xxx.p7b\n\n此时，根目录下的`build-profile.json5`文件如下：\n\nsigningConfigs下的default为本地开发debug模式使用，release为生产打包使用\n\n```json5\n{\n  \"app\": {\n    \"signingConfigs\": [\n      {\n        \"name\": \"default\",\n        \"type\": \"HarmonyOS\",\n        \"material\": {\n          \"certpath\": \"/Users/xxx/.ohos/config/default_harmonyTodolist_1E20EuW6JSNNRofCNXMMXzxBWhxfMG9yA9x0FxAlCXE=.cer\",\n          \"storePassword\": \"xxx\",\n          \"keyAlias\": \"debugKey\",\n          \"keyPassword\": \"xxx\",\n          \"profile\": \"/Users/xxx/.ohos/config/default_harmonyTodolist_1E20EuW6JSNNRofCNXMMXzxBWhxfMG9yA9x0FxAlCXE=.p7b\",\n          \"signAlg\": \"SHA256withECDSA\",\n          \"storeFile\": \"/Users/xxx/.ohos/config/default_harmonyTodolist_1E20EuW6JSNNRofCNXMMXzxBWhxfMG9yA9x0FxAlCXE=.p12\"\n        }\n      },\n      {\n        \"name\": \"release\",\n        \"type\": \"HarmonyOS\",\n        \"material\": {\n          \"storePassword\": \"xxx\",\n          \"certpath\": \"/Users/xxx/code/harmonyTodolist/arkts-todolist.cer\",\n          \"keyAlias\": \"arktsTodolist_release\",\n          \"keyPassword\": \"xxx\",\n          \"profile\": \"/Users/xxx/code/harmonyTodolist/arktsTodolistProfileRelease.p7b\",\n          \"signAlg\": \"SHA256withECDSA\",\n          \"storeFile\": \"/Users/xxx/code/harmonyTodolist/arktsTodolist_release.p12\"\n        }\n      }\n    ],\n    \"products\": [\n      {\n        \"name\": \"default\",\n        \"signingConfig\": \"release\",\n        \"compatibleSdkVersion\": \"5.0.0(12)\",\n        \"runtimeOS\": \"HarmonyOS\",\n        \"buildOption\": {\n          \"strictMode\": {\n            \"caseSensitiveCheck\": true,\n            \"useNormalizedOHMUrl\": true\n          }\n        }\n      }\n    ],\n    \"buildModeSet\": [\n      {\n        \"name\": \"debug\",\n      },\n      {\n        \"name\": \"release\"\n      }\n    ]\n  },\n  \"modules\": [\n    {\n      \"name\": \"entry\",\n      \"srcPath\": \"./entry\",\n      \"targets\": [\n        {\n          \"name\": \"default\",\n          \"applyToProducts\": [\n            \"default\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n```\n\n#### 2.5.2 进行打包\n\n1. DevEco Studio`Build \u003e Build Hap(s)/APP(s) \u003e Build APP(s)`\n\n```log\n\u003e hvigor Finished ::PreBuildApp... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@PreBuild...  \n\u003e hvigor Finished ::DuplicateDependencyCheck... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@GenerateMetadata...  \n\u003e hvigor Finished :entry:default@ConfigureCmake... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@MergeProfile...  \n\u003e hvigor UP-TO-DATE :entry:default@CreateBuildProfile...  \n\u003e hvigor Finished :entry:default@PreCheckSyscap... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@GeneratePkgContextInfo...  \n\u003e hvigor Finished :entry:default@ProcessIntegratedHsp... after 1 ms \n\u003e hvigor Finished :entry:default@BuildNativeWithCmake... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@MakePackInfo...  \n\u003e hvigor UP-TO-DATE :entry:default@ProcessProfile...  \n\u003e hvigor Finished :entry:default@SyscapTransform... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@ProcessRouterMap...  \n\u003e hvigor Finished :entry:default@BuildNativeWithNinja... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@ProcessResource...  \n\u003e hvigor UP-TO-DATE :entry:default@GenerateLoaderJson...  \n\u003e hvigor UP-TO-DATE :entry:default@ProcessLibs...  \n\u003e hvigor UP-TO-DATE :entry:default@CompileResource...  \n\u003e hvigor UP-TO-DATE :entry:default@DoNativeStrip...  \n\u003e hvigor UP-TO-DATE :entry:default@CompileArkTS...  \n\u003e hvigor Finished :entry:default@BuildJS... after 1 ms \n\u003e hvigor UP-TO-DATE :entry:default@CacheNativeLibs...  \n\u003e hvigor Finished :entry:default@GeneratePkgModuleJson... after 1 ms \n\u003e hvigor WARN: If obfuscation is needed, enable obfuscation settings in this build process; failing to do so may prevent future obfuscation. \n               Properly configure obfuscation rules to avoid runtime issues.\n\u003e hvigor UP-TO-DATE :entry:default@PackageHap...  \n\u003e hvigor UP-TO-DATE :entry:default@SignHap...  \n\u003e hvigor Finished :entry:assembleHap... after 1 ms \n\u003e hvigor UP-TO-DATE ::MakeProjectPackInfo...  \n\u003e hvigor UP-TO-DATE ::GeneratePackRes...  \n\u003e hvigor UP-TO-DATE ::PackageApp...  \n\u003e hvigor UP-TO-DATE ::SignApp...  \n\u003e hvigor Finished ::assembleApp... after 1 ms \n\u003e hvigor BUILD SUCCESSFUL in 156 ms \n\nProcess finished with exit code 0\n\nBuild Analyzer results available\n```\n\n有一个关于obfuscation的警告，是代码加固混淆的，先不管，真正需要上线的再改。\n\n2. 不出意外，`build/outputs/default`下多出了3个文件\n\n```\nbuild/outputs/default\n├── harmonyTodolist-default-signed.app\n├── harmonyTodolist-default-unsigned.app\n└── pack.info\n```\n\n#### 2.5.2 云上真机测试\n\n[AppGallery Connect管理中心-云调试](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/myProject)\n\n![](markdown-static/appgalleryConnect-mobile-debug.png)\n\n选择一个机型，将刚才打包好的`xxx-signed.app`拖拽上传，进行测试\n\n![](markdown-static/huaweimate60_openCamera.png)\n\n可以看到，成功使用系统相机，拍了一张黑漆漆的照片\n\n## 3. 使用uniapp+vue3\n\n**注意:**\n \n1. uniapp目前仅支持vue3编译成鸿蒙\n2. HBuilderX需要更新到最新版，不然顶部菜单点击运行的时候，没有`运行到鸿蒙`选项\n3. DevEco-Studio还是要下载，模拟器要先在DevEco-Studio中启动\n\n### 3.1 创建一个vue3项目\n\n\u003e 这里就使用HBuilderX的方式创建项目，暂时不使用cli命令的方式\n\n右击新建项目，选择默认模板，Vue版本选择3\n\n此时项目目录结构大致如下:\n\n```\n├── App.vue\n├── README.md\n├── index.html\n├── main.js\n├── manifest.json\n├── node_modules\n├── package.json\n├── pages\n|  ├── index\n|  └── todolist\n├── pages.json\n├── pnpm-lock.yaml\n├── static\n|  ├── css\n|  └── logo.png\n├── tailwind.config.js\n├── uni.promisify.adaptor.js\n├── uni.scss\n├── uni_modules\n└── unpackage\n   ├── debug\n   ├── dist\n   └── release\n```\n\n另外，这里我添加了`tailwindcss`和`uv-ui`，不添加也没关系\n\n### 3.2 新建tolist页面，添加路由\n\n1. pages文件下新建页面todolist\n2. pages.json中新增路由\n\n```json\n{\n\t\"pages\": [\n\t\t{\n\t\t\t\"path\" : \"pages/todolist/todolist\",\n\t\t\t\"style\" :\n\t\t\t{\n\t\t\t\t\"navigationBarTitleText\" : \"todolist\"\n\t\t\t}\n\t\t}\n\t]\n}\n```\n\n### 3.3 完成todolist页面\n\n1. template部分\n\n```html\n\u003ctemplate\u003e\n\t\u003cview class=\"w-full h-[95%] flex flex-col gap-[4px] overflow-hidden bg-white px-[16px] pt-[5px] box-border\"\u003e\n\t\t\u003cview class=\"w-full flex items-center justify-between gap-[10px]\"\u003e\n      \u003cuv-input placeholder=\"请输入...\" border=\"surround\" v-model=\"inputVal\"\u003e\u003c/uv-input\u003e\n\n      \u003cuv-button text=\"添加\" type=\"primary\" @click=\"handleAdd\"\u003e\u003c/uv-button\u003e\n    \u003c/view\u003e\n\n    \u003cview class=\"w-full flex-1 overflow-hidden\"\u003e\n      \u003cscroll-view :scroll-y=\"true\" class=\"w-full h-[100%]\"\u003e\n        \u003cuv-empty v-if=\"list.length === 0\" mode=\"data\" text=\"暂无数据\" marginTop=\"40\"\u003e\u003c/uv-empty\u003e\n        \u003ctemplate v-else\u003e\n          \u003cview\n            class=\"w-full flex items-center justify-between gap-[8px] px-[10px] py-[20px] box-border bg-slate-100 mb-[8px] rounded-[4px] overflow-hidden\"\n            v-for=\"(item, key) in list\"\n            :key=\"key\"\n          \u003e\n            \u003ctext class=\"flex-[1_1_auto] text-[#333333] font-[500] text-[14px] break-all\"\u003e{{key+1}}. {{item}}\u003c/text\u003e\n\n            \u003cview class=\"flex-none flex items-center gap-[4px]\"\u003e\n              \u003cuv-button text=\"编辑\" plain :hairline=\"false\" size=\"normal\" type=\"info\" @click=\"handleEdit(key)\"\u003e\u003c/uv-button\u003e\n              \u003cuv-button text=\"删除\" plain :hairline=\"false\" size=\"normal\" type=\"error\" @click=\"handleDelete(key)\"\u003e\u003c/uv-button\u003e\n            \u003c/view\u003e\n          \u003c/view\u003e\n        \u003c/template\u003e\n      \u003c/scroll-view\u003e\n    \u003c/view\u003e\n\t\u003c/view\u003e\n  \u003cuv-modal\n    ref=\"modalRef\"\n    title=\"修改\"\n    :closeOnClickOverlay=\"false\"\n    :showCancelButton=\"true\"\n    :asyncClose=\"true\"\n    @confirm=\"handleConfirmEdit\"\n    @cancel=\"handleCloseModal\"\n  \u003e\n    \u003cview class=\"slot-content w-full\"\u003e\n      \u003cuv-input placeholder=\"请输入...\" border=\"surround\" v-model=\"currentEditObj.val\"\u003e\u003c/uv-input\u003e\n    \u003c/view\u003e\n  \u003c/uv-modal\u003e\n\n  \u003cuv-safe-bottom\u003e\u003c/uv-safe-bottom\u003e\n\u003c/template\u003e\n\u003cscript setup\u003e\n   // 下面js部分\n\u003c/script\u003e\n```\n\n2. js部分\n```javascript\nimport {ref, reactive} from \"vue\";\n\nconst modalRef = ref();\nconst inputVal = ref(\"\");\nconst list = ref([]);\nconst currentEditObj = reactive({\n   index: \"\",\n   val: \"\"\n});\n\nconst handleCloseModal = () =\u003e {\n   modalRef.value.close();\n\n   currentEditObj.val = \"\";\n}\n\nconst handleAdd = () =\u003e {\n   const newVal = inputVal.value.trim();\n   if(newVal === \"\") {\n      uni.showToast({\n         title: \"添加内容不可为空\",\n         icon: \"none\"\n      })\n      return;\n   }\n\n   list.value.push(newVal);\n   inputVal.value = \"\";\n   console.log(\"list:\", JSON.stringify(list.value));\n}\n\nconst handleEdit = (index) =\u003e {\n   //  这里uni.showModal的editable属性暂时不支持，所以modal选择了uv-ui框架的modal\n   currentEditObj.index = index;\n   currentEditObj.val = list.value[index];\n\n   modalRef.value.open();\n};\n\nconst handleConfirmEdit = () =\u003e {\n   const newVal = currentEditObj.val.trim();\n   if(newVal === \"\") {\n      uni.showToast({\n         title: \"修改内容不可为空\",\n         icon: \"none\"\n      });\n      modalRef.value.closeLoading();\n      return;\n   }\n\n   list.value.splice(currentEditObj.index, 1, newVal);\n   handleCloseModal();\n}\n\nconst handleDelete = (index) =\u003e {\n   uni.showModal({\n      title: \"提示\",\n      content: \"确定删除吗？\",\n      success(res) {\n         if(res.confirm) {\n            list.value.splice(index, 1);\n         }\n      }\n   })\n}\n```\n\n### 3.4 本地运行\n\n1. [先到DevEco-Studio中打开模拟器](https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html)\n\n2. HbuilderX顶部菜单`运行-运行到手机或模拟器-运行到鸿蒙`,选择启动的模拟器，点击`运行`\n\n3. 模拟器中即可看到已经安装了我们的应用\n\n![](markdown-static/todolist-uniapp.gif)\n\n### 3.5 调用鸿蒙原生API\n\n- [uniapp调用鸿蒙原生API](https://uniapp.dcloud.net.cn/tutorial/harmony/native-api.html#%E8%B0%83%E7%94%A8%E9%B8%BF%E8%92%99%E5%8E%9F%E7%94%9Fapi)\n\n- uniapp中要调用鸿蒙原生api，必须通过`uts插件`方式接入，`uts`就当成`ts`来写就可以了\n\n跟着文档，这里我们写打开手机系统相机，拍照返回给页面。\n\n\n1. `uni_modules`文件夹右击，`新建uni_modules插件`，选择`UTS插件-API插件`，插件ID命名成`ikun-openCamera`\n2. `/uni_modules/ikun-openCamera/package.json`中找到`uni_modules`字段，新增\n   ```json\n   {\n      \"uni_modules\": {\n         \"uni-ext-api\": {\n            \"uni\": {\n               \"openCamera\": {\n                  \"name\": \"openCamera\",\n                  \"app\": {\n                     \"js\": false,\n                     \"kotlin\": false,\n                     \"swift\": false,\n                     \"arkts\": true\n                  }\n               }\n            }\n         }\n      }\n   }\n   ```\n   \n3. `uni_modules/ikun-openCamera/utssdk/interface.uts`中声明类型\n   ```typescript\n   export interface Uni {\n       /**\n           * OpenCamera()\n           * @description\n           * 打开相机\n           * @param {OpenCameraOptions}  options\n           * @return {void}\n           * @example\n            ```typescript\n               uni.OpenCamera({});\n            ```\n           */\n       openCamera(options : OpenCameraOptions) : void;\n   }\n   \n   export type OpenCamera = (options : OpenCameraOptions) =\u003e void;\n   export type OpenCameraSuccess = {\n     [key:string]: string\n   };\n   export type OpenCameraSuccessCallback = (result : OpenCameraSuccess) =\u003e void;\n   export type OpenCameraFail = {\n       /**\n        * 错误信息\n        */\n       errMsg : string\n   };\n   export type OpenCameraFailCallback = (result : OpenCameraFail) =\u003e void;\n   export type OpenCameraComplete = {\n       /**\n        * 错误信息\n        */\n       errMsg : string\n   };\n   export type OpenCameraCompleteCallback = (result : OpenCameraComplete) =\u003e void;\n   \n   export type OpenCameraOptions = {\n       /**\n        * 接口调用成功的回调函数\n        * @defaultValue null\n        */\n       success ?: OpenCameraSuccessCallback | null,\n       /**\n        * 接口调用失败的回调函数\n        * @defaultValue null\n        */\n       fail ?: OpenCameraFailCallback | null,\n       /**\n        * 接口调用结束的回调函数（调用成功、失败都会执行）\n        * @defaultValue null\n        */\n       complete ?: OpenCameraCompleteCallback | null\n   };\n   ```\n\n4. `uni_modules/ikun-openCamera/utssdk/app-harmony/index.uts`中暴露调用相机方法\n   \n   ```\n   ├── uni_modules\n   |  └── ikun-openCamera\n          ├── confis.json\n          └── index.uts\n   ```\n\n   - 没有`app-harmony`文件夹，就新建\n   - [通过系统相机拍照和录像(ArkTS)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/camera-picker-V5)\n   - 应用可调用CameraPicker拍摄照片或录制视频，无需申请相机权限\n   - 应用调试时，开发者需在release模式下调用系统相机（CameraPicker）\n\n   ```typescript\n   import { camera, cameraPicker as picker } from '@kit.CameraKit';\n   import { fileIo, fileUri } from '@kit.CoreFileKit';\n   // import { BusinessError } from '@kit.BasicServicesKit';\n   import {OpenCameraOptions, OpenCameraSuccess, OpenCameraFail} from \"../interface.uts\";\n   \n   export async function openCamera(options : OpenCameraOptions) {\n      try {\n         let imgSrc: string = '';\n         let videoSrc: string = '';\n   \n         let pathDir = getContext().filesDir;\n         let fileName = `${new Date().getTime()}`\n         let filePath = pathDir + `/${fileName}.tmp`\n         fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);\n   \n         let uri = fileUri.getUriFromPath(filePath);\n         let pickerProfile: picker.PickerProfile = {\n            cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,\n            saveUri: uri\n         };\n         let result: picker.PickerResult =\n            await picker.pick(getContext(), [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO],\n               pickerProfile);\n   \n         console.log(\"result:\", result);\n         console.info(`picker resultCode: ${result.resultCode},resultUri: ${result.resultUri},mediaType: ${result.mediaType}`);\n         if (result.resultCode == 0) {\n            if (result.mediaType === picker.PickerMediaType.PHOTO) {\n               imgSrc = result.resultUri;\n            } else {\n               videoSrc = result.resultUri;\n            }\n         }\n   \n         let obj: OpenCameraSuccess = {\n            imgSrc,\n            videoSrc,\n         }\n   \n         options?.success?.(obj);\n      } catch (error) {\n         let result : OpenCameraFail = {\n            errMsg: error.message ?? \"\"\n         };\n   \n         options?.fail?.(result);\n      }\n   }\n   ```\n\n5. vue3页面中调用上面暴露的方法\n   ```html\n   \u003ctemplate\u003e\n      \u003cbutton type=\"primary\" @click=\"handleOpenCamera\"\u003e打开系统相机拍照\u003c/button\u003e\n      \u003cimage :src=\"obj.imgSrc\" class=\"w-[200px] h-[200px]\" @error=\"handleImgLoadErr\"\u003e\u003c/image\u003e\n                \n       \u003ctext\u003eimgSrc: \u003c/text\u003e\n       \u003ctext :selectable=\"true\" class=\"my-text-wrap\"\u003e{{obj.imgSrc}}\u003c/text\u003e\n       \u003ctext class=\"my-text-wrap\"\u003e{{imgLoadErrRef || '暂无图片加载失败信息'}}\u003c/text\u003e        \n    \u003c/template\u003e\n   ```\n\n   ```typescript\n   import { reactive, ref } from 'vue';\n   import { openCamera } from '@/uni_modules/ikun-openCamera';\n   \n   export type TCameraReturnObj = {\n      imgSrc?: string;\n      videoSrc?: string;\n   }\n         \n   export type TImgLoadErr = {\n     detail?: object\n   }\n      \n   const obj = reactive({\n      imgSrc: \"\",\n      videoSrc: \"\",\n   });\n   const imgLoadErrRef = ref();\n   \n   const handleOpenCamera = async () =\u003e {\n     try {\n       await openCamera({\n         success(r: TCameraReturnObj) {\n           console.log(\"r\", JSON.stringify(r));\n            // 这里通过打印看出，r.imgSrc是file://协议，uniapp的image标签无法显示\n            obj.imgSrc = r.imgSrc;\n            obj.videoSrc = r.videoSrc;\n         },\n         fail(e) {\n           console.error(\"openCamera error:\", JSON.stringify(e));\n         }\n       });\n     } catch (error) {\n       console.error(error);\n     }\n   }\n   \n   const handleImgLoadErr = (event:TImgLoadErr) =\u003e {\n    imgLoadErrRef.value = JSON.stringify(event.detail);\n   }\n   ```\n\n### 3.6 打包测试\n\n1. HbuilderX点击`发行-\u003eApp-Harmony-本地打包`，此时因为没有配置签名，打的包是无法提交云上测试的\n2. 可以将打完包路径`unpackage/release/xxx`直接用DevEco-Studio打开，找到根目录的`build-profile.json5`，配置`signingConfigs`字段，按照上面那些步骤，将需要生成的文件都准备好，最后进行打包\n3. 将打包完的`xxx-default-signed.app`拖拽到AppGallery Connect 云测试里安装测试，发现uniapp的image标签无法显示file协议图片\n\n![](./markdown-static/uniapp.todolist.file.png)\n\n### 3.7  尝试解决uniapp中image标签无法显示file://协议问题，未解决\n\n\u003e [【HarmonyOS】ArrayBuffer转Base64，Base64转ArrayBuffer，Uri转ArrayBuff，PixelMap转ArrayBuffer，图片Uri转为PixelMap](https://blog.csdn.net/superherowupan/article/details/143227934)\n\n```typescript\nimport { fileIo } from '@kit.CoreFileKit';\nimport util from '@ohos.util';\n\n// 1. 先将图片uri转为arraybuffer\nfunction imageUri2Buffer(uri: string){\n let file = fs.openSync(uri, fileIo.OpenMode.READ_ONLY);\n let buffer = new ArrayBuffer(4096);\n fs.readSync(file.fd, buffer);\n return buffer ;\n}\n\n// 2. 然后将arraybuffer转为base64\nfunction arrayBuffer2Base64(buffer: ArrayBuffer){\n   let temp = new Uint8Array(buffer);\n   // 官方提供的base64编码转换工具\n   let helper = new util.Base64Helper();\n   let res = helper.encodeToStringSync(temp);\n   return res;\n}\n```\n\n通过上面这样的转换，成功返回base64字符串\n\n```typescript\nconst handleOpenCamera = async () =\u003e {\n   try {\n      await openCamera({\n         success: async (r: TCameraReturnObj) =\u003e {\n            if(r.imgSrc) {\n               obj.imgSrc = `data:image/jpeg;base64,${r.imgSrc}`\n            } else {\n               obj.imgSrc = r.imgSrc;\n            }\n\n            obj.videoSrc = r.videoSrc;\n         },\n         fail(e) {\n            errorRef.value = JSON.stringify(e);\n            console.error(\"openCamera error:\", JSON.stringify(e));\n         }\n      });\n   } catch (error) {\n      console.error(error);\n   }\n}\n```\n\n但是最终失败了，image标签的error事件显示base64路径错误`404 Not Found`` 有大佬知道这里怎么解决的，麻烦留言告知一声。\n\n## 4. 使用taro+react\n\n\u003e [Taro开发鸿蒙ArkUI文档](https://taro-docs.jd.com/docs/next/harmony)\n\n### 4.1 首先初始化taro项目\n\n1. 安装taro-cli，选择vite模板\n\n  ```bash\n  npm i -g @tarojs/cli@beta`\n  ```\n\n2. 初始化项目\n\n  ```bash\n  taro init xxx\n  ```\n\n3. 安装 Taro 适配鸿蒙插件并修改 Taro 编译配置\n\n  - 安装插件\n\n  ```bash\n  npm i @tarojs/plugin-platform-harmony-ets@beta\n  ```\n\n  - config/index.ts\n\n  ```javascript\n  import path from 'node:path';\n\n  config = {\n    // 配置使用插件\n    plugins: ['@tarojs/plugin-platform-harmony-ets'],\n    // harmony 相关配置\n    harmony: {\n      // 将编译方式设置为使用 Vite 编译\n      compiler: 'vite',\n      // 【必填】鸿蒙主应用的绝对路径\n      // 这里等下在DevEco-studio里创建项目的时候，就选择这个文件夹地址\n      projectPath: path.resolve(process.cwd(), '../MyApplication'),\n      hapName: 'entry',\n      name: 'default',\n    },\n  }\n  ```\n\n4. package.json文件中新增scripts命令\n\n  ```json\n  {\n    \"scripts\": {\n      \"build:harmony\": \"taro build --type harmony\",\n      \"dev:harmony\": \"npm run build:harmony -- --watch\"\n    }\n  }\n  ```\n\n### 4.2 DevEco-Studio中创建主项目\n\n![](./markdown-static/taro-projectpath.png)\n\n**注意：**文件夹关系一定要对应好，即现在有两个项目，taro中编译的输出到DevEco-Studio里\n\n### 4.3 启动运行\n\n\u003e 下面报错信息弄得头疼\n\n1. 启动taro项目\n\n```bash\nnpm run dev:harmony\n```\n\n启动后直接报错：\n\n```log\nnode:internal/modules/cjs/loader:1327\n  return process.dlopen(module, path.toNamespacedPath(filename));\n                ^\n\nError: dlopen(/Users/xxx/code/harmonyTodolistTaro/node_modules/.pnpm/@tarojs+parse-css-to-stylesheet-darwin-arm64@0.0.69/node_modules/@tarojs/parse-css-to-stylesheet-darwin-arm64/parse-css-to-stylesheet.darwin-arm64.node, 0x0001): Library not loaded: /opt/homebrew/opt/pcre2/lib/libpcre2-8.0.dylib\n  Referenced from: \u003c005B3B76-8884-3214-A052-75F904AFBABF\u003e /Users/xxx/code/harmonyTodolistTaro/node_modules/.pnpm/@tarojs+parse-css-to-stylesheet-darwin-arm64@0.0.69/node_modules/@tarojs/parse-css-to-stylesheet-darwin-arm64/parse-css-to-stylesheet.darwin-arm64.node\n  Reason: tried: '/opt/homebrew/opt/pcre2/lib/libpcre2-8.0.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/pcre2/lib/libpcre2-8.0.dylib' (no such file), '/opt/homebrew/opt/pcre2/lib/libpcre2-8.0.dylib' (no such file)\n    at Object.Module._extensions..node (node:internal/modules/cjs/loader:1327:18)\n    at Module.load (node:internal/modules/cjs/loader:1091:32)\n    at Function.Module._load (node:internal/modules/cjs/loader:938:12)\n    at Module.require (node:internal/modules/cjs/loader:1115:19)\n    at require (node:internal/modules/helpers:130:18)\n    at Object.\u003canonymous\u003e (/Users/xxx/code/harmonyTodolistTaro/node_modules/.pnpm/@tarojs+parse-css-to-stylesheet@0.0.69/node_modules/@tarojs/parse-css-to-stylesheet/index.js:141:29)\n    at Module._compile (node:internal/modules/cjs/loader:1241:14)\n    at Module._extensions..js (node:internal/modules/cjs/loader:1295:10)\n    at Object.newLoader [as .js] (/Users/xxx/code/harmonyTodolistTaro/node_modules/.pnpm/pirates@4.0.6/node_modules/pirates/lib/index.js:121:7)\n    at Module.load (node:internal/modules/cjs/loader:1091:32) {\n  code: 'ERR_DLOPEN_FAILED'\n}\n\nNode.js v20.9.0\n```\n2. 解决启动报错\n\n  - 删除node_modules后，重新`pnpm install`安装依赖，还是不行\n  - 以为是全局安装的taro cli版本是beta版本导致的，换了稳定v4.0.8版本，再次启动还是不行\n  - 以为是初始化项目选择了pnpm导致安装，重新来选择npm，结果还是不行\n\n  - **解决：**\n\n    最终丢给ai，提示要安装`pcre2`,执行`brew list | grep pcre2`后，果然没有打印，执行`brew install pcre2`安装，重新启动项目，解决了。\n\n3. 正常启动日志：\n\n```log\n\u003e harmonyTodolistTaro@1.0.0 build:harmony\n\u003e taro build --type harmony --watch\n\n👽 Taro v4.0.8\nvite v4.5.5 building for production...\n\nwatching for file changes...\n\nbuild started...\ntransforming (1) taro:compiler(node:25315) [stylelint:002] DeprecationWarning: The CommonJS Node.js API is deprecated.\nSee https://stylelint.io/migration-guide/to-16\n(Use `node --trace-deprecation ...` to show where the warning was created)\n(node:25315) [stylelint:002] DeprecationWarning: The CommonJS Node.js API is deprecated.\nSee https://stylelint.io/migration-guide/to-16\n✓ 7 modules transformed.\nrendering chunks (6)...\n\n开始 ohpm install 脚本执行...\n\n/bin/sh: /Users/xxx/Library/Huawei/ohpm/bin/ohpm: No such file or directory\n自动安装依赖失败，请手动执行 ohpm install 或在 DevEco Studio 中打开 oh-package.json5 并点击 Sync Now 按钮\nMyApplication/entry/src/main/ets/app.css.xss.js                  0.09 kB │ gzip: 0.10 kB │ map: 0.10 kB\nMyApplication/entry/src/main/ets/index.css.xss.js                0.10 kB │ gzip: 0.10 kB │ map: 0.10 kB\nMyApplication/entry/src/main/ets/app_comp.js                     0.27 kB │ gzip: 0.21 kB │ map: 0.69 kB\nMyApplication/entry/src/main/ets/pages/index/index_taro_comp.js  0.40 kB │ gzip: 0.27 kB │ map: 0.11 kB\nMyApplication/entry/src/main/ets/app_taro_comp.js                0.83 kB │ gzip: 0.46 kB │ map: 0.13 kB\nMyApplication/entry/src/main/ets/pages/index/index_comp.js       0.89 kB │ gzip: 0.42 kB │ map: 0.98 kB\nMyApplication/entry/src/main/ets/app.ets                         2.21 kB │ gzip: 0.86 kB\nMyApplication/entry/src/main/ets/render.ets                      5.76 kB │ gzip: 1.23 kB\nMyApplication/entry/src/main/ets/pages/index/index.ets           9.04 kB │ gzip: 2.44 kB\nbuilt in 312ms.\n```\n\n4. DevEco-Studio中打开模拟器，不出意外，hello world正常显示\n\n5. 关于上面启动后警告信息`ohpm install`自动安装依赖失败\n\n  \u003e 其实就是本地全局的OHPM_HOME环境变量配置问题\n\n  打开本地zsh，测试下，能打印出版本号，说明就没问题\n\n  ```bash\n  ohpm -v\n  ```\n\n  但我这本地，通过zsh打开的命令行，没打印出ohpm的版本号，而通过DevEco-Studio内置的命令行，可以打印出ohpm版本号5.0.8\n\n  还有本地就没有`/Users/xxx/Library/Huawei/ohpm`这个目录，哪里出问题了？\n\n  **解决：**\n\n  - 1. [首先下载ohpm工具包](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-command-line-ohpm-0000001490235312-V2),找到`Command Line Tools for HarmonyOS`,选择[对应的平台下载即可](https://developer.huawei.com/consumer/cn/deveco-studio/archive/)\n\n  - 2. 解压目录\n    将解压后的ohpm文件夹复制到本地`/Users/xxx/Library/Huawei`文件夹\n\n    ```bash\n    # 首先切到该文件夹\n    cd /Users/xxx/Library/Huawei/ohpm/bin\n    # 然后执行下面命令\n    ./init\n    ```\n\n  - 3. 配置ohpm环境变量\n    ```bash\n    echo $SHELL\n    ```\n    我这里输出`/bin/zsh`,所以修改`~/.zshrc`配置文件，bash的修改`~/.bashrc`即可\n\n    ```bash\n    sudo vim ~/.zshrc\n    ```\n\n    进入编辑模式，然后添加\n    ```bash\n    # ohpm\n    export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm\n    export PATH=$PATH:$OHPM_HOME/bin\n    # ohpm end\n    ```\n\n    保存退出后，执行\n\n    ```bash\n    source ~/.bashrc\n    ```\n\n    此时再执行,不出意外，正确打印出版本号。但这里打印出来的是`1.2.0`，但是DevEco-Studio的内置命令行打印出来的是`5.0.8`。\n\n    ```\n    ohpm -v \n    ```\n\n6. 再次执行`npm run dev:harmony`,ohpm警告问题解决，修改文件后也ok。\n\n  ```log\n  \u003e harmonyTodolistTaro@1.0.0 build:harmony\n  \u003e taro build --type harmony --watch\n\n  👽 Taro v4.0.8\n\n  vite v4.5.5 building for production...\n\n  watching for file changes...\n\n  build started...\n  transforming (1) taro:compiler(node:11988) [stylelint:002] DeprecationWarning: The CommonJS Node.js API is deprecated.\n  See https://stylelint.io/migration-guide/to-16\n  (Use `node --trace-deprecation ...` to show where the warning was created)\n  (node:11988) [stylelint:002] DeprecationWarning: The CommonJS Node.js API is deprecated.\n  See https://stylelint.io/migration-guide/to-16\n  ✓ 7 modules transformed.\n  rendering chunks (6)...\n\n  开始 ohpm install 脚本执行...\n\n  install completed in 0s 1ms\n  执行 ohpm install 脚本成功。\n\n  ../harmonyTodolistTaroMain/entry/src/main/ets/app.less.xss.js                 0.10 kB │ gzip: 0.10 kB │ map: 0.10 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/index.less.xss.js               0.10 kB │ gzip: 0.10 kB │ map: 0.10 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/app_comp.js                     0.27 kB │ gzip: 0.21 kB │ map: 0.71 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/pages/index/index_taro_comp.js  0.40 kB │ gzip: 0.27 kB │ map: 0.11 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/app_taro_comp.js                0.83 kB │ gzip: 0.46 kB │ map: 0.13 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/pages/index/index_comp.js       0.89 kB │ gzip: 0.42 kB │ map: 1.00 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/app.ets                         2.21 kB │ gzip: 0.86 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/render.ets                      5.76 kB │ gzip: 1.23 kB\n  ../harmonyTodolistTaroMain/entry/src/main/ets/pages/index/index.ets           9.04 kB │ gzip: 2.44 kB\n  built in 435ms.\n  ```\n\n7. 关于taro项目启动后，DevEco-Studio打开模拟器报错\n\n  ```log\n  \u003e hvigor ERROR: Failed :default:default@HotReloadArkTS... \n  \u003e hvigor ERROR:  ERROR: srcEntry file '/Users/xxx/code/harmonyTodolistTaroMain/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets' does not exist. \n  \u003e hvigor ERROR: BUILD FAILED in 904 ms\n  ```\n\n  本地确实没有这个文件，不知道什么原因造成的\n\n  **解决：**\n\n  卸载掉全局安装的taro/cli的beta版本，重新安装稳定版本，再次重新初始化项目，按着那些步骤重新运行，ok了\n\n  ```bash\n  npm i -g @tarojs/cli\n  ```\n\n### 4.4 完成todolist页面\n\n```tsx\nimport {useState} from \"react\";\nimport { View, Text, Input, Button, ScrollView } from '@tarojs/components';\nimport Taro from \"@tarojs/taro\";\nimport './index.less'\n\nexport default function Index () {\n  const [val, setVal] = useState\u003cstring\u003e(\"\");\n  const [list, setList] = useState\u003cArray\u003cstring\u003e\u003e([]);\n\n  const handleAdd = () =\u003e {\n    if(val.trim() === \"\") {\n      Taro.showToast({\n        title: \"添加内容不可为空\",\n        icon: \"none\"\n      });\n\n      return;\n    }\n\n    setList([val, ...list]);\n    setVal(\"\");\n  }\n\n  const handleDelete = (index: number) =\u003e {\n    console.log(\"index:\", index);\n    Taro.showModal({\n      title: \"提示\",\n      content: \"确定要删除此项吗？\",\n      success: (res) =\u003e {\n        if (res.confirm) {\n          const newList = [...list];\n\n          newList.splice(index, 1);\n          setList(newList);\n        }\n      }\n    });\n  }\n\n  return (\n    \u003cView className='wrap'\u003e\n      \u003cView className='head'\u003e\n        \u003cView className='inputWrap'\u003e\n          \u003cInput className='input' type='text' placeholder='请输入...' value={val} onInput={e =\u003e setVal(e.detail.value)} /\u003e\n        \u003c/View\u003e\n\n        \u003cView className='btnWrap'\u003e\n          \u003cButton size='mini' className='btn' type='primary' onClick={handleAdd}\u003e\n            \u003cText className='text-white text-[14px] font-[500]'\u003e添加\u003c/Text\u003e\n          \u003c/Button\u003e\n        \u003c/View\u003e\n      \u003c/View\u003e\n\n      \u003cView className='scrollWrap'\u003e\n        {\n          list.length === 0\n            ? \u003cView className='empty'\u003e\u003cText className='text'\u003e暂无数据\u003c/Text\u003e\u003c/View\u003e\n            : \u003cScrollView className='scroll'\u003e\n                {\n                  list.map((item, index) =\u003e (\u003cView key={index} className='item'\u003e\n                    \u003cView className='left'\u003e\n                      \u003cText className='text'\u003e{index+1}. {item}\u003c/Text\u003e\n                    \u003c/View\u003e\n\n                    \u003cView className='right'\u003e\n                      \u003cButton size='mini' className='btn' type='default'\u003e\n                        \u003cText className='text-[#333333] text-[14px] font-[500]'\u003e编辑\u003c/Text\u003e\n                      \u003c/Button\u003e\n                      \u003cButton size='mini' className='btn' type='warn' onClick={() =\u003e handleDelete(index)}\u003e\n                        \u003cText className='text-white text-[14px] font-[500]'\u003e删除\u003c/Text\u003e\n                      \u003c/Button\u003e\n                    \u003c/View\u003e\n                  \u003c/View\u003e))\n                }\n              \u003c/ScrollView\u003e\n        }\n      \u003c/View\u003e\n    \u003c/View\u003e\n  )\n}\n```\n\n![](./markdown-static/taro.gif)\n\n实际感受就是，css样式有些不支持，最要命的是，项目热更新没生效，虽然没报错，但是模拟器中的样式不是最新的，只能每次重新启动taro项目，重新启动模拟器，这样才是最新的样式。很心累，使用姿势哪里有问题，麻烦评论区说下。只能匆忙结束taro。\n\n## 5.写在最后\n\n如果文章对您有帮助，可以关注我的个人公众号`半个柠檬2020`，偶尔也会在公众号上面更新一些自己的学习笔记。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrect%2Fharmony-arkts-todolist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsrect%2Fharmony-arkts-todolist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrect%2Fharmony-arkts-todolist/lists"}