基于HarmonyOS_Next的送餐骑手端应用开发笔记(上)

1. 分层架构逻辑模型解析

HarmonyOS应用的分层架构主要包括三个层次:产品定制层products、基础特性层features和公共能力层commons,为开发者构建了一个清晰、高效、可扩展的设计架构。

  • 不同设备意味着不同的入口 。products是个入口,可能包含手机 + 平板 + 2in1 + 手表 + Car
  • Hap-entry
  • Hsp-共享包
  • Har-静态共享包

products-hap包

features-hap包和hsp包

commons-hsp包

三层: 三个大的文件夹. UI界面适配的。可复用的业务逻辑、组合各个模块

  • 产品定制层:产品定制层专注于满足不同设备或使用场景(如应用)的个性化需求,包括UI设计、资源和配置,以及针对特定场景的交互逻辑和功能特性。产品定制层的功能模块独立运作,同时依赖基础特性层和公共能力层来实现具体功能。作为应用的入口,产品定制层是用户直接互动的界面。为满足特定产品需求,产品定制层可灵活地调整和扩展,从而满足各种使用场景。
  • 基础特性层:基础特性层位于公共能力层之上,用于存放基础特性集合,例如相对独立的功能UI和业务逻辑实现。该层的每个功能模块都具有高内聚、低耦合、可定制的特点,以支持产品的灵活部署。基础特性层为上层的产品定制层提供稳健且丰富的基础功能支持,包括UI组件、基础服务等。同时依赖于下层的公共能力层为其提供通用功能和服务。为了增强系统的可扩展性和维护性,基础特性层将功能进行模块化处理。例如,一个应用的底部导航栏中的每个选项都可能是一个独立的业务模块。
  • 公共能力层:公共功能层用于存放公共基础能力,集中了例如公共UI组件、数据管理、外部交互以及工具库等的共享功能。应用可以共享和调用这些公共能力。公共能力层为上层的基础特性层和产品定制层提供稳定可靠的功能支持,确保整个应用的稳定性和可维护性。公共能力层包括但不限于以下组成:
    • 公共UI组件:这些组件被设计成通用且高度可复用的,确保在不同的应用程序模块间保持一致的用户体验。公共UI组件提供了标准化且友好的界面,帮助开发者快速实现常见的用户交互需求,例如提示、警告、加载状态显示等,从而提高开发效率和用户满意度。
    • 数据管理:负责应用程序中数据的存储和访问,包括应用数据、系统数据等,提供了统一的数据管理接口,简化数据的读写操作。通过集中式的数据管理方式不仅使得数据的维护更为简单,而且能够保证数据的一致性和安全性。
    • 外部交互:负责应用程序与外部系统的交互,包括网络请求、文件I/O、设备I/O等,提供统一的外部交互接口,简化应用程序与外部系统的交互。开发者可以更为方便地实现应用程序的网络通信、数据存储和硬件接入等功能,从而加速开发流程并保证程序的稳定性和性能。
    • 工具库:提供一系列常用工具函数和类,例如字符串处理、日期时间处理、加密解密、数据压缩解压等,帮助开发者提高效率和代码质量。

Hap-entry

Hsp-共享包

Har包-静态共享包

1.1. 开发模型

图2 分层架构开发模型

  • 产品定制层产品定制层的各个子目录会被编译成一个Entry类型的HAP,作为应用的主入口。该层主要针对跨多种设备,为各种设备形态集成相应的功能和特性。产品定制层被划分为多个功能模块,每个功能模块都针对特定的设备或使用场景设计,并根据具体的产品需求进行功能及交互的定制开发。说明
    • 在产品定制层,开发者可以从不同设备对应应用的UX设计和功能两个维度,结合具体的业务场景,选择一次编译生成相同或者不同的HAP(或其组合)
    • 通过使用定制多目标构建产物的定制功能,可以将应用所对应的HAP编译成各自的.app文件,用于上架到应用市场。
  • 基础特性层在基础特性层中,功能模块根据部署需求被分为两类。对于需要通过Ability承载的功能,可以设计为Feature类型的HAP,而对于不需要通过Ability承载的功能,根据是否需要实现按需加载,可以选择设计为HAR模块或者HSP模块,编译后对应HAR包或者HSP包。
  • 公共能力层公共能力层的各子目录将被编译成HAR包,而他们只能被产品定制层和基础特性层所依赖,不允许存在反向依赖。该层旨在提取模块化公共基础能力,为上层提供标准接口和协议,从而提高整体的复用率和开发效率。

1.2. 部署模型

图3 分层架构部署模型(不同设备的定制)

应用程序(.app文件)在流水线或应用市场上被解包为n * Entry类型的HAP + n * Feature类型的HAP,根据设备类型和使用场景将应用部署到不同类型的设备上,实现多端的统一用户体验。

说明

当Entry类型的HAP和Feature类型的HAP被分发并部署到相应的设备时,他们所依赖的HSP也会一同被分发并部署到相应的设备上。

在部署模型中,每个Entry类型的HAP代表了应用的入口点,而Feature类型的HAP则包含了应用的特定功能模块。允许应用能够以模块化的方式适配和部署,从而满足不同设备和场景的需求。

该部署模型不仅优化了应用的组织结构,也为保持应用在各种设备和场景中的一致性提供了支持。通过按照设备类型和使用场景来区分和部署不同的HAP,能确保无论在何种设备或场景中,用户都能获得统一且高质量

腾讯的面试官问了个这么个问题:har会多份拷贝,hsp不会,但是hsp不会进行资源共享,有什么方案吗?

腾讯用hsp包了一个har,向外暴露

1.3. 单层项目架构

ets
├── common                      // 通用模块
│   ├── builders                // - 通用组件 @Builder
│   ├── components              // - 通用组件 @Component
│   ├── constants               // - 常量数据
│   ├── images                  // - 图片资源
│   └── utils                   // - 工具类
├── entryability                // 入口UIAbility
│   └── EntryAbility.ts
├── models                      // - 数据模型
├── pages                       // - 页面组件
│   ├── HomePage.ets 
│   └── Index.ets
└── views                       // - 页面对应自定义组件
    ├── HomePage 
    └── Index

三层项目架构官方代码实例

📎优秀实践-HMOS世界(ArkTS).zip

1.4. 三层架构

project-root
├── commons                     // 基础层(通用组件、工具类、资源)
│   └── basic                   // - 通用模块
├── features                    // 需求层(业务组件)
│   ├── home                    // - 需求模块
│   └── mine                    // - 需求模块
└── products                    // 入口层(页面)
    └── phone                   // - 设备模块

2. 创建项目-用git管理

使用DevEcoSudio创建一个空项目

gitee创建仓库

复制地址

创建推送成功。

3. 创建一个静态har包, 导入素材和资源

本次项目采用单层架构 + har公共包的模式进行开发

  • 新建一个common目录, 在该目录下新建一个静态模块Static Library

建好了

📎media.zip

  1. 下载该media文件夹,将所有的图标素材拖动到静态资源包下的 src/main/resources/base/media下

  • 导入色值

{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "primary",
      "value": "#FE6A3D"
    },
    {
      "name": "primary_disabled",
      "value": "#FADCD9"
    },
    {
      "name": "success",
      "value": "#27BA9B"
    },
    {
      "name": "warning",
      "value": "#FFAB2E"
    },
    {
      "name": "danger",
      "value": "#FF4C4C"
    },
    {
      "name": "text_primary",
      "value": "#2A2929"
    },
    {
      "name": "text_secondary",
      "value": "#818181"
    },
    {
      "name": "text_placeholder",
      "value": "#C2C1C1"
    },
    {
      "name": "border",
      "value": "#D9D9D9"
    },
    {
      "name": "background_divider",
      "value": "#EEEEEE"
    },
    {
      "name": "background_page",
      "value": "#F4F4F4"
    },
    {
      "name": "black",
      "value": "#000000"
    },
    {
      "name": "white",
      "value": "#FFFFFF"
    },
    {
      "name": "gray_1",
      "value": "#F7F8FA"
    },
    {
      "name": "gray_2",
      "value": "#F2F3F5"
    },
    {
      "name": "gray_3",
      "value": "#EBEDF0"
    },
    {
      "name": "gray_4",
      "value": "#DEDEE0"
    },
    {
      "name": "gray_5",
      "value": "#C8C9CC"
    },
    {
      "name": "gray_6",
      "value": "#969799"
    },
    {
      "name": "gray_7",
      "value": "#646566"
    },
    {
      "name": "gray_8",
      "value": "#323233"
    },
    {
      "name": "btn_plain",
      "value": "#FFE0DD"
    },
    {
      "name": "btn_gray",
      "value": "#E6E6E6"
    },
    {
      "name": "upload_panel",
      "value": "#f2f2f2"
    }
  ]
}

4. 修改项目名称和图标

  • 修改项目名称

  • 通过生成图标工具创建app图标

生成后复制替换,删除多余项

5. 公共静态包(HAR)基础目录搭建

在har包中建立如下目录

  • 按照客户端开发的需要我们要在项目中建立如下结构

  • 在项目中添加上述的目录,并统一新建一个index.ets文件

  • 清空har包下index.ets文件内容

6. createSubWindow广告展示页

广告页的思路

-华为有广告业务,但是我们不用- ad模块

想自定义广告-

场景: app启动-有广告需求,就打开广告页,没有的话就去登录或者主页

腾讯体育的广告- 启动有广告页,退到后台的情况下,再次进入前台也会有广告

分析- 广告页作为一个app启动的首页,应该是在我们应用启动就进去的。

  • 有的app有的需要广告页,有的不需要,搞个配置呗!!!
  1. 通过首选项配置存储我们的一些常用配置,比如要不要广告页,还有广告页的路由地址,点击广告页跳转的链接,广告页倒计时的秒数
  2. 在入口处进行判断是否需要广告页,需要的话,跳转广告页-广告页根据设置的参数进行渲染
  3. 有的同学可能会问,广告页能不能设置-因为运营人员肯定不能每次都去改我们底层的代码-这里我还可以设置成动态的-就是初始化的时候通过请求去读一下云端的请求,然后把我们的图片和一些参数配置下来,这样每次你启动app就是运营人员给你配置的广告和设置了
  • 新建一个关于广告类的数据模型-basic/models/advert.ets

  • 在utils中新建一个关于读取首选项的类,用来读取和设置首选项的广告设置
//读写广告配置的class
import { USER_SETTING,USER_SETTING_AD }from '../constants'
import { preferences } from '@kit.ArkData'
import { Context } from '@ohos.abilityAccessCtrl'
import { AdvertClass } from '../../../../Index'
//默认广告
const defaultAd:AdvertClass={
  showAd:true,
  adTime:5,
  adImg:$r("app.media.start")
}
export class UserSettingClass{
  //此时需要上下文和读写方法
  context:Context
  constructor(ctx:Context) {
    this.context=ctx
  }
  //获取仓库
  getStore(){
    return preferences.getPreferencesSync(this.context,{
      name:USER_SETTING
    })
  }
  //设置用户的广告配置
  async setUserAd(ad:AdvertClass){
    const  store =this.getStore()//拿到仓库
    store.putSync(USER_SETTING_AD,JSON.stringify(ad))//放入仓库
    await store.flush()//写入磁盘
  }
  //读取用户的广告配置
  getUserAd(){
    const  store =this.getStore()//拿到仓库
    return JSON.parse(store.getSync(USER_SETTING_AD,JSON.stringify(defaultAd)) as  string) as AdvertClass//拿到广告
  }
}
  • 上面还用到了两个常量,我们同样需要在constants目录下定义一个文件专门用来记录-setting

entry模块-oh-package.json5

  • 在ability中引入该har包依赖,就可以在entry中用我刚才的那些东西了

ability中判断

这里我们模拟了一个请求,给了一个默认广告, 写入首选项-正常加载主页

  1. 在pages下新建Start目录,下面新建Start的page页面
import { UserSettingClass, AdvertClass } from '@hm/basic'

@Entry
  @Component
  struct Start {
    userSetting: UserSettingClass = new UserSettingClass(getContext(this))
    @State
    adObj: AdvertClass  = {
      showAd: false,
      adTime: 0
    }
    timer: number = -1

    async aboutToAppear() {
      this.adObj = await this.userSetting.getUserAd()
      this.timer = setInterval(() => {
        if(this.adObj.adTime === 0) {
          clearInterval(this.timer)
          return
        }
        this.adObj.adTime--
      }, 1000)
    }
    build() {
      Stack({ alignContent: Alignment.TopEnd }) {
        Image(this.adObj.adImg).objectFit(ImageFit.Cover)
        Text(`${this.adObj.adTime}秒后跳过`)
          .padding({ left: 10, right: 10 })
          .margin({ right: 20, top: 20 })
          .height(30)
          .fontSize(14)
          .borderRadius(15)
          .backgroundColor($r("app.color.background_page"))
          .textAlign(TextAlign.Center)

      }.height('100%').width('100%')
    }
  }
  • 实现Start页的页面结构及倒计时逻辑
  • 使用子窗口模式加载广告

我们可以使用windowStage的createSubWindow来实现在当前页面上创建一个窗口

  • 广告页在广告结束时,关闭广告

Start/Start页面

实现点击跳过广告

效果:

实现每次打开都有广告:

7. 检查token跳登录页

由于司机端是必须要求用户登录的,所以我们需要在跳转到主页前检查是否有token,有token的话跳转到主页,没有token的话跳转到登录

Entry模块

  1. 新建登录页- pages/Login/Login

Har包basic

  1. 在广告页跳转前检查token,有的话跳转到主页 Index,没有的话跳转到Login
  • 声明一个token_key的常量-constants/user.ets

这里需要在首选项中在声明两个方法

Entry模块,实现开启app判断有无token跳转页面

8. 登录页界面

Entry模块:登录信息属于业务的数据,所以内容留存在当前的entry模块中

pages/Login/Login.ets

//登录页面
@Entry
  @Component
  struct Login {

    @State showLoading: boolean = false//控制进度条显示

    @Styles
    loginStyle() {
      .backgroundColor('#fff')
        .border({ color: $r('app.color.background_divider'), width: { bottom: 1 } })
        .width('100%')
        .height(58)
        .borderRadius(0)
    }

    build() {
      Column() {
        // 顶部标题
        Text("鲨鱼快递").fontColor($r('app.color.text_primary')).fontSize(18).height(25)
        // 账号登录
        Row() {
          Text('账号登录').fontColor($r('app.color.text_primary')).fontSize(24).fontWeight(FontWeight.Bold)
          Row() {
            Text("手机号登录").fontColor($r('app.color.primary')).fontSize(16).fontWeight(FontWeight.Bold)
            Image($r("app.media.ic_angle")).width(10).height(10).margin({ left: 5 })
          }
        }
        .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 50, bottom: 50 })

        // 用户名输入框
        TextInput({ placeholder: '请输入账号' })
          .loginStyle()

        // 密码框
        TextInput({ placeholder: '请输入密码' })
          .loginStyle()
          .type(InputType.Password) // 密码框
          .showPasswordIcon(true) // 显示密码按钮

        // 登录按钮
        Button({ type: ButtonType.Capsule }) {
          Row() {
            if (this.showLoading) {
              LoadingProgress().width(20).height(20).margin({ right: 12 }).color($r('app.color.white'))
            }
            Text('登录').fontColor($r('app.color.white'))
          }
        }
        .backgroundColor($r('app.color.primary_disabled'))
          .width('100%')
          .height(50)
          .margin({ top: 50 })
      }
      .padding({ left: 32, right: 32 })
        .margin({ top: 40 })
    }
  }
  • 接下来我们来把表单数据进行一下双向绑定-和类型定义
  • 首先在models下新建user.ts文件

这里我们拷贝接口文档中的interface接口声明-要去除默认自带的

  • 完成账户和密码的双向绑定,这样输入的内容就会生为一个账户类型
  • 注意: 这里不能使用$$ 因为我们$$只支持单层数据的绑定,出现嵌套的情况还得使用监听方法

控制登陆按钮的显示,账户密码都有的时候才会显示按钮。

- 2024/8/21

9. 封装统一请求工具request

  • 申请基础网络权限-在module.json5中配置

Basic模块

  • 在constants/index.ets中设置baseURL基础地址常量

  • 封装泛型工具-models/index.ets

工具的目的- 让返回的结构完成统一

  • 封装一个公共的request方法来支持get/post/put/delete方法

http官网链接

//封装一个公共的request方法来支持get/post/put/delete方法
import {BASE_URL} from "../constants/url"
import http from '@ohos.net.http';//原生的请求地址
import { TOKEN_KEY } from '../constants';
import { promptAction, router } from '@kit.ArkUI';
import { UserSettingClass } from '.';
import { request } from '@kit.BasicServicesKit';
import json from '@ohos.util.json';
import { ResponseData } from '../models';

//url 类型 参数
async function requestHttp<T>(url:string="",method:http.RequestMethod = http.RequestMethod.GET,data?:object):Promise<T>{
  //http需要创建一个请求对象
  const httpRequest = http.createHttp()//创建一个请求对象,如果你想用单例,那么请不要在请求时销毁
  let  urlStr=BASE_URL+url //基础地址拼接url地址
  //进行处理
  if (method ===http.RequestMethod.GET) {
    //data参数都拼接到地址上 ?a=1&b=2&c=3
    if (data&&Object.keys(data).length) {
      urlStr += "?" + Object.keys(data).map(key=>{
        return `${key}=${data[key]}`
      })
        .join("&")//拼接地址完成
    }
  }
  //组装参数
  const config:http.HttpRequestOptions={
    extraData:method!==http.RequestMethod.GET?data:"",//当请求类型不等于get时 赋值
    header:{
      "Content-Type":"application/json",
      Authorization:AppStorage.get(TOKEN_KEY)||"", //设置请求头的token,相当于每个接口都有token
    },
    method:method,
    readTimeout:10000//如果多少秒没响应就直接断开
  }
  try {
    //发请求:
    const result = await httpRequest.request(urlStr,config)//如果请求成功了,他会返回一个结果给result
    //这里要处理一些异常,首先要判断状态码,一种叫http状态码,一种叫业务状态码
    if (result.responseCode === 401) {
      //401表示token失效 超时 没传
      //提示错误 删除目前的token,跳到登录页面
      promptAction.showToast({message:'401登录失效啦'})
      //删除token,首选项里的和全局状态里的
      new UserSettingClass(getContext()).setUserToken("")//清空首选项里的token
      AppStorage.setOrCreate(TOKEN_KEY,"")//全局状态的token设为空
      router.replaceUrl({
        url:'pages/Login/Login'
      })
      return Promise.reject(new Error("401登录失效!"))
    }
    else if (result.responseCode===404){
      promptAction.showToast({message:'404请求地址错误啦'})
      return Promise.reject(new Error("404请求地址错误!"))
    }else {
      //假设已经成功,此时不一定成功哦,还有业务状态码呢,只有业务状态码是200的时候才成功
      const res = JSON.parse(result.result as string) as ResponseData<T>
      if (res.code===200) {
        //这次请求才算成功
        return res.data as T
      }else {
        //业务错误
        promptAction.showToast({message:"服务器异常咯"})
        return Promise.reject(new Error(res.msg))//业务错误 请求终止
      }
    }
  }catch (error){
    promptAction.showToast({message:error.message})
    return Promise.reject(error)
  }
}
//再封装一层成一个单例
export class Request {
  static get<T =null>(url: string, data?: object): Promise<T> {
    return requestHttp<T>(url, http.RequestMethod.GET, data)
  }

  static post<T = null>(url: string, data?: object): Promise<T> {
    return requestHttp<T>(url, http.RequestMethod.POST, data)
  }

  static delete<T = null>(url: string, data?: object): Promise<T> {
    return requestHttp<T>(url, http.RequestMethod.DELETE, data)
  }

  static put<T =null>(url: string, data?: object): Promise<T> {
    return requestHttp<T>(url, http.RequestMethod.PUT, data)
  }
}

总结: 上面请求工具总共处理了这么几件事

  • 封装请求-拼接基础地址-传入参数
  • 解构返回的数据-判断状态
  • 200 认为成功直接返回data数据
  • 200以外 401 认为是token超时
  • 500认为是服务器任务
  • 最后暴露一个类的四个静态方法 可以方便的调用 get/put/delete/post
  • 提交代码

10. 封装请求登录的api

Entry模块

  • 在api下新建user.ets, 并在index.ets导出

  • index.ets中导出

export * from './user'

封装api是为后续更灵活的使用

  • Request.post<string>这里面的string相当于传入的泛型数据,指代返回的data是个string
  • 提交代码

11. 登录存储token跳转主页

  • 首先大家先通过这个网址

神领TMS管理系统

  • 去注册一个属于自己的账号 ,我的账号密码:xz991129
  • 在登录页引入登录api-存入token-跳转主页

调用登录接口

  • 注册事件位置

  • 当我们使用键盘回车之后,账号和密码也可以直接提交

总结

  • 登录需要自己去网站上注册账号-移动端暂时没有注册接口
  • 请求的时候需要准确的准备返回的响应体
  • 登录成功之后-存入token- 跳转页面
  • 这里登录失败为啥没处理,因为在请求模块的位置,我们判断了200,只要是200 我们就终止了请求,不会再往下走啦
  • 最后不要忘记提交代码

12. 首页布局

basic模块

entry模块

  • 上图得知,首页底部分为三个区域,任务,消息,我的
  • 任务里面有 待提货,在途, 已完成
  • 底部为第一级别,上部分内容为第二级别,我们先完成第一级别

首先把我们的首页设置成Index/Index,改成目录结构的嵌套

  • 另外需要调整原来路由跳转的地方- Login/EntrtAbility

采用Tabs组件就可以轻松实现

//主页:
import {TabClass} from  '@hm/basic'
@Entry
  @Component
  struct Index {
    @State currentIndex: number =0//当前激活项
    @State tabsData:TabClass[]=[//底部栏目数据
      {
        title: '任务',
        name: 'task',
        icon: $r("app.media.ic_tab_btn_task")
      },{
        title: '消息',
        name: 'message',
        icon: $r("app.media.ic_tab_btn_mess_nor")
      },{
        title: '我的',
        name: 'my',
        icon: $r("app.media.ic_tab_btn_mine_nor")
      }]
    @Builder//底部引导框渲染
    getTabBar(item: TabClass) {
      Column() {
        Image(item.icon).width(22).height(22)
          .fillColor(item.name === this.tabsData[this.currentIndex].name ?
                     $r('app.color.primary') : $r('app.color.text_secondary'))
        Text(item.title)
          .fontSize(12)
          .fontWeight(400)
          .margin({ top: 5 })
          .fontColor(item.name === this.tabsData[this.currentIndex].name ?
                     $r('app.color.primary') : $r('app.color.text_secondary'))
      }.alignItems(HorizontalAlign.Center)
    }

    build() {
      Tabs({ barPosition: BarPosition.End, index: $$this.currentIndex }){
        ForEach(this.tabsData, (item: TabClass) => {
          TabContent(){
            if(item.name === 'task') {
              Text("任务组件")
            }
            else if(item.name === 'message') {
              Text("消息组件")
            }
            else {
              Text("我的组件")
            }
          }.tabBar(this.getTabBar(item))
                })
      }
    }
  }

13. 我的-组件布局

Entry模块

  • 页面也可以被当成组件来使用, 既可以被引用-也可以被跳转
  • 组件不能当成页面来使用

注意:

我的是组件而不是页面,因为它属于Tabs中的子组件,所以我们在pages/Index 目录下新建一个My文件夹

里面新建一个My.ets(组件可以是Page,也可以不是,但是如果是Page的话,需要进行导出)

  • 新建pages/Index/My/My.ets文件-结构从静态模板中拷贝
//我的页面
@Component
  struct My {
    build() {
      Column(){
        // 基本信息
        Column() {
          Image($r("app.media.ic_avatar_driver"))
            .width(67)
            .height(67)
            .borderRadius(34.5)
            .backgroundColor($r('app.color.white'))
          Text("司机")
            .fontSize(18)
            .fontWeight(600)
            .lineHeight(25)
            .margin({
              top: 9,
              bottom: 9
            })
            .fontColor($r('app.color.white'))
          Text(`司机编号: 66666666`).fontSize(14).fontWeight(400).lineHeight(20).fontColor($r('app.color.white'))
          Text(`手机号: 66666666`)
            .fontSize(14)
            .fontWeight(400)
            .lineHeight(20)
            .margin({
              top: 10
            })
            .fontColor($r('app.color.white'))
        }
        .backgroundImage($r("app.media.bg_page_my"))
          .backgroundImageSize(ImageSize.Cover)
          .width('100%')
          .alignItems(HorizontalAlign.Center)
          .justifyContent(FlexAlign.Center)
          .height(292)
          .margin({
            top: -2
          })

        // 本月任务
        Column() {
          Text("- 本月任务 -").fontSize(14).fontColor($r('app.color.text_secondary')).lineHeight(20)
          Row() {
            Column() {
              Text("1000").fontSize(18).fontColor($r('app.color.text_primary')).lineHeight(25).margin({
                bottom: 17
              })
              Text("任务总量").fontSize(12).fontColor($r('app.color.text_primary')).lineHeight(17)
            }.justifyContent(FlexAlign.SpaceAround).layoutWeight(1)

            Column() {
              Text("1000")
                .fontSize(18)
                .fontColor($r('app.color.text_primary'))
                .lineHeight(25)
                .margin({ bottom: 17 })
              Text("完成任务量").fontSize(12).fontColor($r('app.color.text_primary')).lineHeight(17)
            }.justifyContent(FlexAlign.SpaceAround).layoutWeight(1)

            Column() {
              Text("1000")
                .fontSize(18)
                .fontColor($r('app.color.text_primary'))
                .lineHeight(25)
                .margin({ bottom: 17 })
              Text("运输里程(km)").fontSize(12).fontColor($r('app.color.text_primary')).lineHeight(17)
            }.justifyContent(FlexAlign.SpaceAround).layoutWeight(1)
          }.justifyContent(FlexAlign.SpaceBetween).width('100%').layoutWeight(1)
        }
        .backgroundColor($r('app.color.white'))
          .borderRadius(8)
          .margin({ left: 14.5, right: 14.5, top: -55, bottom: 15 })
          .height(134)
          .padding({ top: 13.5, bottom: 13.5 })
          .justifyContent(FlexAlign.SpaceBetween)

        // 信息列表
        Column() {
          Row() {
            Text("车辆信息").fontSize(16).fontWeight(400).fontColor($r('app.color.text_primary'))
            Image($r("app.media.ic_btn_more")).width(24).height(24)
          }
          .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
            .border({ width: { bottom: 1 }, color: $r('app.color.background_page') })
            .width('100%')
            .height(60)

            Row() {
          Text("任务数据").fontSize(16).fontWeight(400).fontColor($r('app.color.text_primary'))
          Image($r("app.media.ic_btn_more")).width(24).height(24)
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        .border({ width: { bottom: 1 }, color: $r('app.color.background_page') })
        .width('100%')
        .height(60)

        Row() {
          Text("系统设置").fontSize(16).fontWeight(400).fontColor($r('app.color.text_primary'))
          Image($r("app.media.ic_btn_more")).width(24).height(24)
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .height(60)
      }
      .padding({ left: 17.5, right: 17.5 })
      .backgroundColor($r('app.color.white'))
      .margin({ left: 14.5, right: 14.5, bottom: 15 })
      .borderRadius(8)

    }.width('100%').height('100%').backgroundColor($r('app.color.background_page')).borderRadius(8)
  }
}

export default My

这里用了默认导出 export default My 为什么不用按需导出啦,按需导出针对的是我们的组件库及一些公用的方法和组件,因为不确定一个组件可能会导出哪些内容,可能一个可能多个,但是这里My属于业务组件,只用于完成这个业务,所以这里用了默认导出

  • 放置在Index组件中的我的位置

效果:

  • 大家拷贝过程中发现了什么,就是我们车辆信息,任务数据,系统设置好像结构基本都一样,但是这样代码可读性确实有点低,怎么办?
  • 封装组件库是一个好的办法
  • 大家先把代码提交,接下来,我们来抽提两个组件 HmCard和HmCardItem

14. 抽提HmCard和HmCardItem组件

Basic模块

  • 抽提组件的原因是为了复用,小时达中有很多类似的需求设计如下图

综上,我们需要一个卡片的容器,和一个左右显示内容的组件,并且容器中还可以放置其他组件

  • 所以我们首先封装一个卡片HmCard- components/HmCard.ets
@Component
  struct HmCard {
    //UI结构
    @BuilderParam
    HmCardFn:()=>void//可以不给
    build() {
      Column(){
        Column(){
          if (this.HmCardFn){
            this.HmCardFn()//渲染传入的结构内容
          }
        }
        .width('100%')
          .borderRadius(10)
          .backgroundColor($r("app.color.white"))
          .padding({
            left:15,
            right:15
          })
      }
      .padding(15)
        .width('100%')
    }
  }
export {HmCard}
  • HmCard组件中,我们定义了一个BuilderParam的函数(插槽),通过传入的方式传入内容
  • 接下来我们封装HmCardItem.ets组件,在HmCard同级新建HmCardItem组件

需求

  1. 能够两头对齐
  2. 右侧能够显示向右的图标,也可以不显示
  3. 右侧还可以显示内容-并且应该是可变得
  4. 右侧的内容可以触发点击事件,并且在使用它时可以监听到
  5. 可以控制是否显示下边框

HmCardItem.ets代码实现

@Component
  struct HmCardItem {
    leftTitle:string="左侧内容"//左侧内容
    @Prop
    rightText:string="右侧"//响应式变量
    //是否显示右侧图标
    showRightIcon:boolean=true
    //是否显示底部边框
    showBottomBorder:boolean=true
    //点击右侧的箭头触发的回调函数:
    onRightClick:()=>void = ()=>{}
    build() {
      Row(){
        //左侧标题
        Text(this.leftTitle)
          .fontSize(16)
          .fontColor($r("app.color.text_primary"))
        Row(){
          if (this.rightText){
            Text(this.rightText)
              .fontColor($r("app.color.text_secondary"))
          }
          if (this.showRightIcon){
            Image($r("app.media.ic_btn_more"))
              .width(24)
              .height(24)
          }
        }
        .onClick(()=>{
          this.onRightClick()
        })
      }
      .justifyContent(FlexAlign.SpaceBetween)
        .border({
          width:{
            bottom:this.showBottomBorder?1:0
          },
          color:$r("app.color.background_divider")
        })
        .width('100%')
        .height(60)
    }
  }
export {HmCardItem}
  • 接下来使用HmCard和HmCardItem去替换我的页面的三个内容吧

总结

  • 封装了HmCard和HmCardItem组件,利用插槽技术把CardItem放入Card中
  • CardItem设置了几个属性 如 显示图标 右侧文本(Prop) 显示底边框
  • 提交代码

15. 获取我的个人信息的api

之前的遗留问题

标准流程

  • 定义接口数据结构
  • 封装api
  • 组件中定义响应式数据
  • 封装方法调用api获取数据赋值给响应式数据
  • 在aboutToAppear中调用方法
  • 将响应式数据更换到视图上换成真实的
  1. 找到获取个人信息的接口文档

  • 定义个人信息接口
export interface UserInfoModel {
  /** 头像 */
  avatar: string;

  /** 姓名 */
  name: string;

  /** 司机编号 */
  number: string;

  /** 手机号 */
  phone: string;
}

在api/user.ts中封装获取用户信息的接口

在My页面中导入类型并声明State数据

在父组件通过Provide传入一个当前name

在子组件监听数据变化,如果是当前的tab则获取个人信息

只要切换到我的页面就会执行请求!

更新数据

效果:

16. 任务数据获取的api

上一节如果你能够顺利完成的话,那么接下来,我们来做一个带参数的查询

获取任务数据接口地址

  1. 导入数据类型- entry/models/user_task.ets

导入的数据包括

  • 请求参数UserTaskInfoParams
  • 响应数据UserTaskInfo
/** 响应数据 */
export interface UserTaskInfoModel {
  /** 完成任务数量,基于实际完成时间统计 */
  completedAmounts: number;
  /** 每日里程,基于实际完成时间统计 */
  dailyMileage: DailyMileageModel[];
  /** 任务数量,基于计划完成时间统计 */
  taskAmounts: number;
  /** 运输里程,单位:公里,基于实际完成时间统计 */
  transportMileage: number;
}

export interface DailyMileageModel {
  /** 日期,格式:2022-07-16 00:00:00 */
  dateTime: string | null;
  /** 里程,单位:公里;计算公式:原始数据(单位米)/1000 四舍五入取整 */
  mileage: number | null;
}

export interface UserTaskInfoParamsModel {
  /** 月 */
  month: string;
  /** 年 */
  year: string;
}
export class UserTaskInfoModelModel implements UserTaskInfoModel {
  completedAmounts: number = 0
  dailyMileage: DailyMileageModel[] = []
  taskAmounts: number = 0
  transportMileage: number = 0

  constructor(model: UserTaskInfoModel) {
    this.completedAmounts = model.completedAmounts
    this.dailyMileage = model.dailyMileage
    this.taskAmounts = model.taskAmounts
    this.transportMileage = model.transportMileage
  }
}
export class DailyMileageModelModel implements DailyMileageModel {
  dateTime: string | null = null
  mileage: number | null = null

  constructor(model: DailyMileageModel) {
    this.dateTime = model.dateTime
    this.mileage = model.mileage
  }
}
export class UserTaskInfoParamsModelModel implements UserTaskInfoParamsModel {
  month: string = ''
  year: string = ''

  constructor(model: UserTaskInfoParamsModel) {
    this.month = model.month
    this.year = model.year
  }
}

封装api-user.ets

在my组件中定义数据,封装方法调用

注意:由于接口的问题,任务数据中的年和月是必填的,而且必须是字符串,否则会报错,请注意

最后一步替换数据

解释:

接口返回的是number类型,而只有字符串才能显示到文本上,我们使用了toString(),只不过你需要 this.TaskInfo.transportMileage?.toString() 并且加上短路表达式来 如果前者为空 则处理成空字符

why?

因为TaskInfo我们定义的是空对象,直接对undefind的数据进行toString,会直接空指针异常的

效果:

17. 系统设置页面

上面这个页面我们完全可以复用之前的HmCard和HmCardItem,但是顶部的内容还带标题比较通用,所以我们来封装一个新的组件

  1. 新建一个Page,因为车辆设置是点击系统设置跳转的页面,所以它是一个页面,不是一个组件
  • 在pages下新建Setting/Setting.ets页面
import { HmCard, HmCardItem } from '@hm/basic'
@Entry
  @Component
  struct Setting {
    build() {
      Column() {
        HmCard() {
          HmCardItem({ leftTitle: '换绑手机', rightText: '' })
          HmCardItem({ leftTitle: '修改密码', rightText: '' })
          HmCardItem({ leftTitle: '消息通知设置', rightText: '' })
          HmCardItem({ leftTitle: '清理缓存', rightText: '', showBottomBorder: false })
        }
        Row() {
          Button("退出", { type: ButtonType.Normal })
            .backgroundColor($r('app.color.white'))
            .fontColor($r("app.color.text_primary"))
            .width('100%')
            .borderRadius(8)
            .height(50)
        }
        .width('100%')
          .margin({ top: 20 })
          .padding({ left: 15, right: 15 })
          .justifyContent(FlexAlign.Center)
      }
      .width('100%')
        .height('100%')
        .backgroundColor($r('app.color.background_page'))
    }
  }

在点击系统设置时,跳转到系统设置页-pages/Index/My/My.ets

总结:

  • 封装好的组件用起来很爽
  • 按照业务的需求和UI的设计抽提组件时要考虑复用性,这样就会积累起自己的一套组件库,甚至为开源做贡献

18. 封装头顶导航

basic包

分析需求

  • 能够设置中间的标题部分
  • 点击左侧按钮能够返回上一层页面
  1. 新建components/HmNavBar组件
//导航
import router from '@ohos.router';
@Component
  export struct HmNavBar {
    title: string = "测试个人中心"
    showBackIcon: boolean = true

    build() {
      Stack({ alignContent: Alignment.TopStart }) {
        Row() {
          if (this.title) {
            Text(this.title).fontColor($r('app.color.text_primary')).fontSize(18).fontWeight(600)
          }
        }
        .justifyContent(FlexAlign.Center)
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .height('100%')

        if (this.showBackIcon) {
          Row() {
            Image($r("app.media.ic_btn_nav_back")).width(44).height(44).onClick(() => {
              router.back() // 回上一页
            })
          }.alignItems(VerticalAlign.Center).width(44)
        }
      }
      .backgroundColor($r('app.color.white'))
        .height(50)
        .width('100%')
        .padding(10)
    }
  }
  1. 直接在系统设置中引用,放置到最上方即可

总结

  • 这里我们的HmNavBar的组件title和showBackIcon属性均没有采用@Link和@Prop ,因为它并不需要向响应式更新,这里请知晓,需要的话再去封装对应的需求即可

19. @CustomDialog自定义弹窗-退出登录

Basic

按道理说-这是个很简单的需求,但是!!!目前鸿蒙提供的默认弹窗有点不忍直视,它是长这个样子的

  • 差别有点大,为了避免以后我们和UI或者产品开撕的风险, 我们还是自己来吧,系统默认的弹窗不好看,不符合要求,我们自己整一个呗,怎么整呢? 我们需要使用鸿蒙提供的自定义弹窗功能
  • 现在来浅析一下自定义弹窗的使用方式
  1. 使用@CustomDialog装饰器装饰自定义弹窗。
  2. @CustomDialog装饰器用于装饰自定义弹框,此装饰器内进行自定义内容(也就是弹框内容)。
  3. 创建构造器,与装饰器呼应相连。
  4. 点击与onClick事件绑定的组件使弹窗弹出
  • 封装一个自定义弹窗组件
//确定弹窗组件
@CustomDialog
  @Component
  struct HmConfirm {
    controller:CustomDialogController
    //
    message:string="你TM确定退出登录吗?"
    //接收按钮的类型
    buttonList:HmConfirmButton[]=[{//按钮默认
      text:"ok",
      fontSize:14,
      fontcolor:$r("app.color.primary")
    }]
    build() {
      Column(){
        //显示内容
        Row(){
          Text(this.message)
            .fontSize(16)
            .fontColor($r("app.color.text_primary"))
        }
        .justifyContent(FlexAlign.Center)
          .border({
            width:{
              bottom:0.5
            },
            color:$r("app.color.background_divider")
          })
          .width('100%')
          .height(90)
        //按钮
        Row(){
          ForEach(this.buttonList,(item:HmConfirmButton,index:number)=>{
            Text(item.text)
              .fontColor(item.fontcolor||$r("app.color.text_secondary"))
              .fontSize(item.fontSize||16)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .border({
                color:$r("app.color.background_divider"),
                width:{
                  right:index!==this.buttonList.length - 1 ? 0.5 :0
                }
              })
              .onClick(async ()=>{
                if (item.action) {
                  await item.action()
                }
                this.controller.close()
              })
          })
        }
        .width('100%')
          .height(50)
      }
      .width(278)
        .borderRadius(12)
        .backgroundColor($r("app.color.white"))
    }
  }
//按钮的类型
class HmConfirmButton{
  text:string=""
  fontSize:number=12
  fontcolor:ResourceStr=""
  action?:()=>void = ()=>{}//点击该按钮所触发的逻辑
}
export {HmConfirm}

绑定点击事件

实现退出方法

效果:

20. 封装骨架屏组件

20. 封装骨架屏组件

basic模块

我们发现获取用户资料时速度较慢,此时页面会出现undefined的情况,我们可以判断下数据是否存在,如果不存在,我们直接显示一个骨架屏,如果数据出来,再把骨架屏撤
●在basic中封装HmSkeleton

//骨架屏
@Preview
  @Component
  export struct HmSkeleton {
    @Prop
    showAvatar: boolean = true
    @Prop
    count: number = 3
    timer: number = -1
    @State
    currentColor: string = "#f3f4f5"

    aboutToAppear(): void {
      this.timer = setInterval(() => {
        if (this.currentColor === "#f3f4f5") {
          this.currentColor = "#f7f8f9"
        }else {
          this.currentColor = "#f3f4f5"
        }
      }, 1000)
    }

    aboutToDisappear(): void {
      clearInterval(this.timer)
    }

    @Builder
    getSingleItem() {
      Column() {
        Row({ space: 10 }) {
          if (this.showAvatar) {
            Row()
              .width(40)
              .height(40)
              .borderRadius(20)
              .backgroundColor(this.currentColor)
          }
          Column({ space: 10 }) {
            Row()
              .width("30%")
              .height(26)
              .backgroundColor(this.currentColor)
            Row()
              .width("100%")
              .height(26)
              .backgroundColor(this.currentColor)
            Row()
              .width("100%")
              .height(26)
              .backgroundColor(this.currentColor)
            Row()
              .width("50%")
              .height(26)
              .backgroundColor(this.currentColor)
          }
          .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

        }
        .alignItems(VerticalAlign.Top)
          .width('100%')
      }
      .width('100%')

    }

    build() {
      Column({ space: 30 }) {
        ForEach(Array.from(Array(this.count)), () => {
          this.getSingleItem()
        })
      }
      .alignItems(HorizontalAlign.Start)
        .padding(20)
        .width('100%')
        .height('100%')
        .backgroundColor($r("app.color.white"))
    }
  }

在页面中调用

效果:

21. 设计任务模块tabs

entry模块

任务模块分析

  • 任务模块又是一个tabs,位置居于上方
  • 里面三个待提货 在途 已完成
  • 先要完成任务的基本布局,才可以往下推进
  1. 新建一个TaskTabs组件-位置 pages/Index/Task/TaskTabs.ets(组件-非Page)
import { TabClass } from '@hm/basic'
//任务标签tab
@Component
  struct TaskTabs {
    tabController:TabsController=new TabsController()
    @State
    tabsData: TabClass[] = [{
      name: 'waiting',
      title: '待提货'
    },{
      name: 'line',
      title: '在途'
    },{
      name: 'finish',
      title: '已完成'
    }]
    @State
    currentIndex: number  = 0
    @Builder
    getTabBar(item:TabClass){
      Column(){
        Text(item.title)
          .fontSize(16)
          .margin({bottom:10})
          .animation({
            duration:300
          })
          .fontWeight(this.tabsData[this.currentIndex].name===item.name?600:FontWeight.Regular)
          .fontColor(this.tabsData[this.currentIndex].name===item.name? $r("app.color.text_primary"):$r("app.color.text_secondary"))
        Divider()
          .strokeWidth(4)
          .width(this.tabsData[this.currentIndex].name===item.name?23:0)
          .color($r("app.color.primary"))
          .lineCap(LineCapStyle.Round)
          .animation({
            duration:300
          })
      }
      .onClick(()=>{
        //通过name找到索引
        const index = this.tabsData.findIndex(obj=>obj.name===item.name)
        this.tabController.changeIndex(index)//切换索引
      })
    }
    build() {
      Stack({alignContent:Alignment.Top}){
        Tabs({index:$$this.currentIndex,controller:this.tabController}){
          ForEach(this.tabsData,(item:TabClass)=>{
            TabContent(){
              Text(item.title)
            }.tabBar(item.title)
                  })
        }
        //自定义渲染盖住原有的内容
        Row({space:30}){
          //有多少个就生成多少个页签
          ForEach(this.tabsData,(item:TabClass)=>{
            this.getTabBar(item)
          })
        }
        .padding({left:40,right:40})
          .height(50)
          .width('100%')
          .backgroundColor($r("app.color.white"))
      }
    }
  }
export default TaskTabs

显示

效果:

22. 获取用户任务列表信息

entry模块

分析

  • 待提货是一个列表
  • 每一项都是单独的一个任务
  • 每一项的结构较为个性-而且在其他位置有可能用到-要考虑封装组件
  • 有下拉刷新和上拉加载的需求- 难点

该如何下手

  • 无外乎就是数据视图的展示
  • 先把提货的组件建好,
  • 把数据加载出来
  • 再去实现后面的内容

总结

  • 标准的流程
  • 定义类型
  • 封装api
  • 调用接口
  • 获取数据
  • 显示数据-待实现

  1. 在pages/Index/Task下新建TaskList.ets组件

  1. 在TaskTabs中导入并使用

  1. 定义任务数据的类型-接口文档

新建一个models/task.ts文件,负责管理所有任务数据的类型

步骤

  • 拷贝请求参数类型和响应数据
  • 这里将响应数据的Data改成TaskListData,将其Item[]改成TaskListItem名称-为了后续更好的辨别

export interface TaskListParams {
  /** 结束时间 */
  endTime: string | null;

  /** 页码 */
  page: number;

  /** 页面大小 */
  pageSize: number;

  /** 开始时间 */
  startTime: string | null;

  /** 作业状态,1为待提货)、2为在途(在途和已交付)、3为改派、5为已作废、6为已完成(已回车登记) */
  status: number;

  /** 运输任务id */
  transportTaskId: string | null;
}

/** 响应数据,分页数据统一对象 */
export interface TaskListData {
  /** 总条目数 */
  counts: number;

  /** 数据列表 */
  items: TaskInfoItem[];

  /** 页码 */
  page: number;

  /** 总页数 */
  pages: number;

  /** 页尺寸 */
  pageSize: number;
}

export interface TaskInfoItem {
  /** 实际到达时间 */
  actualArrivalTime: string;

  /** 实际发车时间 */
  actualDepartureTime: string;

  /** 创建时间 */
  created: string;

  /** 司机id */
  driverId: string;

  /** 是否可提货 */
  enablePickUp: boolean;

  /** 目的机构地址 */
  endAddress: string;

  /** 目的机构id */
  endAgencyId: number;

  /** 交付对接人 */
  finishHandover: string;

  /** 司机作业单id */
  id: string;

  /** 计划到达时间 */
  planArrivalTime: string;

  /** 计划发车时间 */
  planDepartureTime: string;

  /** 起始机构地址 */
  startAddress: string;

  /** 起始机构id */
  startAgencyId: number;

  /** 提货对接人 */
  startHandover: string;

  /** 作业状态,作业状态,1为待提货)、2为在途)、3为改派)、4为已交付)、5为已作废 */
  status: TaskTypeEnum;

  /** 运输任务id */
  transportTaskId: string;
}
// 查询状态的枚举
export enum TaskTypeEnum {
  Waiting = 1,
  Line = 2,
  Finish = 6
}
export class TaskListParamsModel implements TaskListParams {
  endTime: string | null = null
  page: number = 0
  pageSize: number = 0
  startTime: string | null = null
  status: number = 0
  transportTaskId: string | null = null

  constructor(model: TaskListParams) {
    this.endTime = model.endTime
    this.page = model.page
    this.pageSize = model.pageSize
    this.startTime = model.startTime
    this.status = model.status
    this.transportTaskId = model.transportTaskId
  }
}
export class TaskListDataModel implements TaskListData {
  counts: number = 0
  items: TaskInfoItem[] = []
  page: number = 0
  pages: number = 0
  pageSize: number = 0

  constructor(model: TaskListData) {
    this.counts = model.counts
    this.items = model.items
    this.page = model.page
    this.pages = model.pages
    this.pageSize = model.pageSize
  }
}
export class TaskInfoItemModel implements TaskInfoItem {
  actualArrivalTime: string = ''
  actualDepartureTime: string = ''
  created: string = ''
  driverId: string = ''
  enablePickUp: boolean = false
  endAddress: string = ''
  endAgencyId: number = 0
  finishHandover: string = ''
  id: string = ''
  planArrivalTime: string = ''
  planDepartureTime: string = ''
  startAddress: string = ''
  startAgencyId: number = 0
  startHandover: string = ''
  status: TaskTypeEnum = TaskTypeEnum.Waiting
  transportTaskId: string = ''

  constructor(model: TaskInfoItem) {
    this.actualArrivalTime = model.actualArrivalTime
    this.actualDepartureTime = model.actualDepartureTime
    this.created = model.created
    this.driverId = model.driverId
    this.enablePickUp = model.enablePickUp
    this.endAddress = model.endAddress
    this.endAgencyId = model.endAgencyId
    this.finishHandover = model.finishHandover
    this.id = model.id
    this.planArrivalTime = model.planArrivalTime
    this.planDepartureTime = model.planDepartureTime
    this.startAddress = model.startAddress
    this.startAgencyId = model.startAgencyId
    this.startHandover = model.startHandover
    this.status = model.status
    this.transportTaskId = model.transportTaskId
  }
}
  • 封装请求列表api - 新建api/task.ets

在tasklist中调用api获取数据并显示

//任务列表
import { getTaskList } from '../../../api'
import { TaskInfoItem, TaskListParams, TaskListParamsModel, TaskTypeEnum } from '../../../modals'
@Component
  struct TaskList {
    @State
    queryParams:TaskListParamsModel=new TaskListParamsModel({
      status:TaskTypeEnum.Waiting,
      page:1,//第几页
      pageSize:5//每页几条数据
    }as  TaskListParams)
    @State
    taskListData:TaskInfoItem[]=[]

    aboutToAppear(): void {
      this.getTaskList()
    }
    async getTaskList(){
      const  result =  await getTaskList(this.queryParams)
      this.taskListData = result.items//拿到返回的数组数据
    }

    build() {
      Text("列表组件")
    }
  }
export default TaskList

效果:

23. 使用列表循环渲染数据

  • 首先需要一个统一的卡片

  • 静态结构-新建一个组件pages/Index/Task/TaskItemCard.ets
//任务小卡片
@Preview
  @Component
  struct TaskItemCard {
    build() {
      Column() {
        Row() {
          Text(`任务编号:213893928399283924`)
            .fontSize(16)
            .fontColor($r("app.color.text_primary"))
            .fontWeight(500)
            .lineHeight(22)
        }.justifyContent(FlexAlign.SpaceBetween).width('100%')

        Row() {
          Text("起")
            .fontSize(12)
            .fontColor($r("app.color.white"))
            .backgroundColor($r("app.color.text_primary"))
            .width(22)
            .height(22)
            .borderRadius(11)
            .textAlign(TextAlign.Center)
          Text("北京市昌平区回龙观街道西三旗桥东金燕龙写字楼8877号")
            .margin({ left: 11.5 })
            .fontColor($r('app.color.text_secondary'))
            .fontSize(14)
        }.margin({ top: 21 }).width('100%')

        Row() {
          Text("止")
            .fontSize(12)
            .fontColor($r("app.color.white"))
            .backgroundColor($r('app.color.primary'))
            .width(22)
            .height(22)
            .borderRadius(11)
            .textAlign(TextAlign.Center)
          Text("河南省郑州市路北区北清路99号")
            .margin({ left: 11.5 })
            .fontColor($r('app.color.text_secondary'))
            .fontSize(14)
        }.margin({ top: 14.5 }).width('100%')

        Divider()
          .vertical(true)
          .height(2)
          .color($r('app.color.background_divider'))
          .opacity(0.6)
          .margin({ left: 8, right: 8, top: 21 })
        Row() {
          Column() {
            Text('提货时间').fontSize(14).fontColor($r('app.color.text_secondary'))
            Text("2022.05.04 13:00").fontSize(14).fontColor($r('app.color.text_secondary')).margin({ top: 4 })
          }.alignItems(HorizontalAlign.Start)

          Button("提货", { type: ButtonType.Capsule })
            .backgroundColor($r('app.color.primary'))
            .fontColor($r("app.color.white"))
            .fontSize(14)
            .height(32)

        }.justifyContent(FlexAlign.SpaceBetween).width('100%')
      }
      .margin({ left: 15, right: 15, top: 15 })
        .padding({ left: 19.5, right: 19.5, bottom: 18.5, top: 18.5 })
        .borderRadius(10)
        .backgroundColor($r('app.color.white'))

    }
  }
export default TaskItemCard
  • 使用List和ListItem配合生成N个TaskItemCard

效果:但此时数据是写死的

24. 任务卡片数据显示

  • 把任务卡片的数据变成真实的

之前在获取任务列表数据时,已经声明了TaskListItem的类型可以直接使用,可以用interface也可以用class

1. 在TaskItemCard中声明一个属性,接收单个任务的数据

  • TaskItemCard.ets

总结

子组件TaskItemCard中声明数据,接受数据,并没有使用修饰符,因为此数据是只读的,父级数据只会被渲染,不会操作发生删除和修改

  • 父组件TaskList传入item数据给TaskItemCard

替换写死的数据

效果:

25. 根据状态控制提货按钮


分析
当前司机只能有一个任务, 必须完成提货,交货,回车登记才可以继续下一个提货
我们的接口中有一个字段可以帮助我们识别该任务用户可以提取


我们根据这个字段true/false来控制一下提货按钮的显示颜色

绑定给按钮

效果:

- 2024/8/22
 

26. Refresh组件包裹List 实现下拉刷新下拉加载

basic模块

HmList组件要完成的功能

  • 支持下拉刷新
  • 支持上拉加载
  • 支持自定义结构
  • 支持传入数据渲染内容
  • 支持设置加载提示文本
  • 支持设置加载完成文本
  • 支持控制是否显示加载的进度条

结构依然采用之前可以使用的Refresh包裹List的基本架构

  • 只不过我们需要向外暴露更多的属性
  • finished 是否加载结束(布尔值)
  • dataSource 数据源 (Link修饰符)
  • renderItem 渲染单个Item的(BuilderParam)
  • onLoad 执行上拉加载的逻辑(函数)
  • onRefresh 执行下拉刷新的逻辑(函数)
  • loadingText 加载时的文本(文本)
  • finishText 结束时的文本(文本)
  • showLoadingIcon 显示加载进度 (布尔值)
  • refreshIng 控制下拉刷新(State修饰 布尔值)
  • loading 控制上拉加载(State布尔值)

在components下新建HmList.ets组件

//上拉加载下拉刷新组件
@Component
  struct HMList {
    @State
    refreshIng:boolean=false//控制下拉刷新的变量
    @Prop
    dataSource:object[]=[]//数据源
    //上拉加载方法
    onLoad:()=>void = ()=>{} //由调用者传入
    //下拉刷新的方法
    onRefresh:()=>void =()=>{}
    //还有没有数据的标记
    @Prop
    finished:boolean=false//是否还有下一页数据
    @State
    loading:boolean=false//是否正在加载中,第一:显示‘加载中’文本,第二:用来做阀门,当前这次请求没结束之前,下次不允许
    loadingText:string="加载中..."
    finishText:string="没有数据啦,傻逼"//加载完成
    @BuilderParam
    renderItem:(item:object)=>void //由调用者传入
    @Builder
    getBottomDislay(){
      //获取底部的展示内容
      Row({space:10}){
        if (this.finished){
          //此时加载完了
          Text(this.finishText)
            .fontSize(14)
            .fontColor($r("app.color.text_secondary"))
        }else {
          Text(this.loadingText)
            .fontSize(14)
            .fontColor($r("app.color.text_secondary"))
          LoadingProgress()
            .width(20)
            .aspectRatio(1)
            .color($r("app.color.text_secondary"))
        }
      }
      .width('100%')
        .height(50)
        .justifyContent(FlexAlign.Center)
    }
    build() {
      Refresh({ refreshing:$$this.refreshIng }) {
        //往下拉的时候会触发一个事件
        List(){
          ForEach(this.dataSource,(item:object)=>{
            //每一项的结构的UI内容,不是由我的列表决定,而是由使用者决定
            //传入builderParam
            if (this.renderItem) {
              this.renderItem(item)
            }
          })
          //放置提示文本的地方
          ListItem(){
            this.getBottomDislay()
          }
        }
        .onReachEnd(async ()=>{
          //实现上拉加载
          //需要一个标记,是否已经加载完所有数据
          if (!this.finished && !this.loading) {
            this.loading=true//关闭阀门
            await this.onLoad()//实现上拉加载
            this.loading=false//执行完await才可以打开阀门
          }
        })
      }
      .onStateChange(async (state)=>{
        //实现下拉刷新
        if (state===RefreshStatus.Refresh) {
          //松手加载状态
          await this.onRefresh()//调用刷新方法
          this.refreshIng=false//关闭下拉的动画效果
          this.loading=false//关闭上拉刷新的loading
          //下来刷新意味着所有的数据全不要了重新来过
        }
      })
      /*.onRefreshing(()=>{
      this.str="松手啦"
      setTimeout(()=>{ this.refreshIng=false},1000)
    })*/
      /*    }.onStateChange(state=>{
      //往下拉的状态
      //往下拉超过一定值的状态
      //松手状态
      if (state===RefreshStatus.Drag) {
        this.str="往下拉"
      }
      else if (state===RefreshStatus.OverDrag) {
        this.str="超过了"
      }
      else if (state===RefreshStatus.Refresh) {
        setTimeout(()=>{
          this.refreshIng=false
        },1000)
        this.str="松手加载啦"
      }
      else if (state===RefreshStatus.Done) {
        this.str="完事啦"
      }
    })*/
    }
  }
export {HMList}

Entry模块

  • 在TaskList中应用组件, 实现加载下一页

效果:

  • 修改函数

  • 给HmList传入onRefresh函数,重新请求重新赋值

效果:

总结

下拉刷新要保证能够发出请求,所以设置allPage为1,查询页码为1

下拉刷新时覆盖数据,上拉加载是追加数据

27. 自定义下拉刷新的样式

basic模块

Refresh组件支持自定义构建样式

  • 声明变量记录当前下拉状态

效果:

28. 务列表跳转到任务详情

entry模块

分析

1. 在提货按钮可用情况下,点击提货,跳转到任务详情

2. 通过路由传入任务详情的id

3. 在任务详情接受任务id

  1. 新建一个TaskDetail的页面,因为是列表 => 详情,所以TaskDetail应该是个页面,在pages/TaskDetail/中新建一个TaskDetail.ets

  1. 在TaskItemCard组件中,监听onClick事件中,通过路由跳转传入任务id到任务详情页

  1. 在TaskDetail中接收id参数,并显示

basic模块

  • 声明一个统一的路由interface来接收Params参数
  • models/index.ets

29. 根据id获取任务详情数据

标准流程

  • 定义数据类型
  • 封装api
  • 获取数据
  • 赋值数据
  • 显示数据

basic模块

  • 定义一个公共的图片类型

entry模块

  1. 拷贝接口返回的详情类型 接口文档

apifox生成的接口类型太多重复,直接拷贝下面的接口

import { ImageList } from '@hm/basic/src/main/ets/models';

/** 响应数据,响应数据 */
export interface TaskDetailInfo {
  /** 实际到达时间 */
  actualArrivalTime: string;

  /** 实际发车时间 */
  actualDepartureTime: string;

  /** 提货凭证 */
  cargoPickUpPictureList: ImageList[];

  /** 提货图片 */
  cargoPictureList: ImageList[];

  /** 回单凭证 */
  certificatePictureList: ImageList[];

  /** 回单图片 */
  deliverPictureList: ImageList[];

  /** 司机id */
  driverId: string;

  /** 司机姓名 */
  driverName: string;

  /** 目的市 */
  endAddress: string;

  /** 目的机构id */
  endAgencyId: string;

  /** 目的机构详细地址 */
  endCity: string;

  /** 目的省份 */
  endProvince: string;
  exceptionList: ExceptionList[];

  /** 交付对接人 */
  finishHandoverName: string;

  /** 交付对接人电话 */
  finishHandoverPhone: string;

  /** 司机作业单id */
  id: string;

  /** 车牌号码 */
  licensePlate: string;

  /** 计划到达时间 */
  planArrivalTime: string;

  /** 计划发车时间 */
  planDepartureTime: string;

  /** 起始机构详细地址 */
  startAddress: string;

  /** 起始机构id */
  startAgencyId: string;

  /** 起始市 */
  startCity: string;

  /** 提货对接人 */
  startHandoverName: string;

  /** 提货对接人电话 */
  startHandoverPhone: string;

  /** 起始省份 */
  startProvince: string;

  /** 作业状态,1为待提货)、2为在途)、3为改派)、4为已交付)、5为已作废、6为已完成(已回车登记) */
  status: number;

  /** 运输任务id */
  transportTaskId: string;
}


export interface ExceptionList {
  /** 异常描述 */
  exceptionDescribe: string;

  /** 异常图片 */
  exceptionImagesList: ImageList[];

  /** 上报的位置 */
  exceptionPlace: string;

  /** 异常时间 */
  exceptionTime: string;

  /** 异常类型(中文) */
  exceptionType: string;

  /** 处理结果 */
  handleResult: string;
}
export class TaskDetailInfoModel implements TaskDetailInfo {
  actualArrivalTime: string = ''
  actualDepartureTime: string = ''
  cargoPickUpPictureList: ImageList[] = []
  cargoPictureList: ImageList[] = []
  certificatePictureList: ImageList[] = []
  deliverPictureList: ImageList[] = []
  driverId: string = ''
  driverName: string = ''
  endAddress: string = ''
  endAgencyId: string = ''
  endCity: string = ''
  endProvince: string = ''
  exceptionList: ExceptionList[] = []
  finishHandoverName: string = ''
  finishHandoverPhone: string = ''
  id: string = ''
  licensePlate: string = ''
  planArrivalTime: string = ''
  planDepartureTime: string = ''
  startAddress: string = ''
  startAgencyId: string = ''
  startCity: string = ''
  startHandoverName: string = ''
  startHandoverPhone: string = ''
  startProvince: string = ''
  status: number = 0
  transportTaskId: string = ''

  constructor(model: TaskDetailInfo) {
    this.actualArrivalTime = model.actualArrivalTime
    this.actualDepartureTime = model.actualDepartureTime
    this.cargoPickUpPictureList = model.cargoPickUpPictureList
    this.cargoPictureList = model.cargoPictureList
    this.certificatePictureList = model.certificatePictureList
    this.deliverPictureList = model.deliverPictureList
    this.driverId = model.driverId
    this.driverName = model.driverName
    this.endAddress = model.endAddress
    this.endAgencyId = model.endAgencyId
    this.endCity = model.endCity
    this.endProvince = model.endProvince
    this.exceptionList = model.exceptionList
    this.finishHandoverName = model.finishHandoverName
    this.finishHandoverPhone = model.finishHandoverPhone
    this.id = model.id
    this.licensePlate = model.licensePlate
    this.planArrivalTime = model.planArrivalTime
    this.planDepartureTime = model.planDepartureTime
    this.startAddress = model.startAddress
    this.startAgencyId = model.startAgencyId
    this.startCity = model.startCity
    this.startHandoverName = model.startHandoverName
    this.startHandoverPhone = model.startHandoverPhone
    this.startProvince = model.startProvince
    this.status = model.status
    this.transportTaskId = model.transportTaskId
  }
}
export class ExceptionListModel implements ExceptionList {
  exceptionDescribe: string = ''
  exceptionImagesList: ImageList[] = []
  exceptionPlace: string = ''
  exceptionTime: string = ''
  exceptionType: string = ''
  handleResult: string = ''

  constructor(model: ExceptionList) {
    this.exceptionDescribe = model.exceptionDescribe
    this.exceptionImagesList = model.exceptionImagesList
    this.exceptionPlace = model.exceptionPlace
    this.exceptionTime = model.exceptionTime
    this.exceptionType = model.exceptionType
    this.handleResult = model.handleResult
  }
}

封装aip

  1. 在任务详情封装方法,调用,赋值状态显示数据

效果:

30. 任务详情结构-封装折叠容器

任务详情结构-封装折叠容器-HmToggleCard

basic模块

  • 容器可以展开-可以折叠,
  • 可以存放子组件内容
  • 可以显示标题

在components下新建 HmToggleCard.ets

//手风琴组件
@Component
  struct HmToggleCard {
    title: string="基本信息"
    @State//是否展开
    toggleCart:boolean=true
    //尾随闭包
    @BuilderParam
    CartContent:()=>void
    build() {
      Column(){
        Column(){
          Row(){
            Text(this.title)
              .fontSize(16)
              .fontColor($r("app.color.text_primary"))
              .fontWeight(500)
            Image(this.toggleCart?$r("app.media.ic_btn_cut"):$r("app.media.ic_btn_add"))
              .width(24)
              .aspectRatio(1)
              .onClick(()=>{
                animateTo({duration:300},()=>{
                  this.toggleCart=!this.toggleCart
                })
              })
          }
          .width('100%')
            .height(50)
            .justifyContent(FlexAlign.SpaceBetween)
          if (this.toggleCart&&this.CartContent){
            this.CartContent()
          }
        }
        .backgroundColor($r("app.color.white"))
          .borderRadius(10)
          .padding({left:18,right:18})
      }
      .padding(10)
    }
  }
export {HmToggleCard}

调用手风琴组件:

效果:

31. Scroll组件实现滚动

注意:

Scroll有且只能有一个子组件,不要设置Scroll的高度,让内容去自动撑满

效果:

32. @Builder解耦合-任务详情静态-基本信息展示

PushKit

mpass

准备任务详情的静态结构

设计图中的货物详情-因接口没有实际数据,所以不在实现。

  1. 实现基本信息数据展示-拷贝静态结构
@Extend(Text)
  function baseTextIconStyle() {
    .fontSize(12)
      .fontColor($r('app.color.white'))
      .backgroundColor($r('app.color.text_primary'))
      .width(22)
      .height(22)
      .borderRadius(11)
      .textAlign(TextAlign.Center)
  }

@Extend(Text)
  function baseTextStyle() {
    .margin({ left: 11.5 })
      .fontColor($r('app.color.text_secondary'))
      .fontSize(14)
      .lineHeight(20)
  }
  • 准备一个class提供给builder函数使用

builder里面的数据 如果用传参数的形式- 基础数据不响应 引用数据类型是响应式的

builder($$: { title: string, name: string }) . Next版本不允许这种声明类型的方式

同学们问: 为什么这里不在models中声明了,因为这里只是在我们业务组件内容进行一个简单的使用,不涉及以后的数据通用型,所以在这里使用是可以的

自定义builder

@Builder
  getBaseContentItem(item: BaseBuilderClass) {
    Row() {
      Text(item.title).fontSize(14).fontColor($r('app.color.text_secondary'))
        .lineHeight(20)
      Row() {
        Text(item.value).fontSize(14).fontColor($r('app.color.text_secondary'))
        if (item.icon) {
          Image(item.icon).width(24).height(24)
        }
      }
    }.justifyContent(FlexAlign.SpaceBetween).width('100%').margin({
      top: 14
    })
  }
// 获取基础信息
@Builder
  getBaseContent() {
    Row() {
      Column() {
        Row() {
          Text("起").baseTextIconStyle()
          Text("北京市昌平区回龙观街道西三旗桥东金燕龙写字楼8877号").baseTextStyle()
        }.margin({ top: 21 })

        Row() {
          Text("止").baseTextIconStyle().backgroundColor($r('app.color.primary'))
          Text("河南省郑州市路北区北清路99号").baseTextStyle()
        }.margin({ top: 14.5 })
      }
      .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ right: 20 })
      Column() {
        Image($r("app.media.ic_navigation")).width(22).height(22)
        Text("开始导航").fontSize(14).margin({ top: 10, bottom: 10 })
      }.justifyContent(FlexAlign.SpaceBetween)
        .margin({
          top: 20
        })
    }.justifyContent(FlexAlign.SpaceBetween).alignItems(VerticalAlign.Center).width('100%')
    Divider().vertical(false).height(2).color($r('app.color.background_divider')).margin({ left: 8, right: 8, top: 21 })
    this.getBaseContentItem({
      title: '任务编号',
      value: '2132324324343434'
    })
    this.getBaseContentItem({
      title: '联系人',
      value: '2132324324343434'
    })
    this.getBaseContentItem({
      title: '联系电话',
      value: '2132324324343434',
      icon: $r('app.media.ic_phone')
    })
    this.getBaseContentItem({
      title: '提货时间',
      value: '2132324324343434'
    })
    this.getBaseContentItem({
      title: '预计送达时间',
      value: '2132324324343434'
    })
  }

放置builkder

效果:

换上真实数据:

效果:

准备司机信息和运输路线的builder

// 司机信息
@Builder
  getDriverContent() {

    this.getBaseContentItem({
      title: '车牌号',
      value: "京A123456"
    })

    this.getBaseContentItem({
      title: '司机姓名',
      value: "张三"
    })

  }

// 运输路线
@Builder
  getTransLineContent() {
    Row() {
      Column() {
        Text("北京市").fontSize(16).fontColor($r('app.color.text_primary')).lineHeight(22).fontWeight(600)
        Text("北京市").fontSize(14).lineHeight(22)
      }.width(50)

      Image($r("app.media.ic_right_arrow")).width(36).height(16)
      Column() {
        Text("河南省").fontSize(16).fontColor($r('app.color.text_primary')).lineHeight(22).fontWeight(600)
        Text("郑州市").fontSize(14).lineHeight(22)
      }.width(50)
    }.justifyContent(FlexAlign.SpaceBetween).alignItems(VerticalAlign.Center).width('100%').padding({
      left: 60,
      right: 60
    })
  }

数据改为真实后并绑定给手风琴组件

效果:

33. 提货信息-上传组件基本封装


分析
需要上传组件进行图片的上传,我们需要封装一个公共的组件HmUpload
先完成基本的布局
1新建components下的HmUpload.ets文件, 完成最基本布局

//上传组件
@Component
  struct HmUpload {
    title:string = ""
    build() {
      Column(){
        Text(this.title)
          .fontSize(14)
          .fontColor($r("app.color.text_secondary"))
          .margin({
            top:16,
            bottom:16
          })
        Row(){
          Image($r("app.media.ic_add_img"))
            .width(30)
            .height(30)
        }
        .justifyContent(FlexAlign.Center)
          .backgroundColor($r("app.color.background_page"))
          .width(100)
          .height(100)
      }
      .alignItems(HorizontalAlign.Start)
        .width('100%')
    }
  }
export {HmUpload}

在页面中显示

效果:


 

34. 任务详情-底部提货按钮结构

实现上传组件前,先把底部的提交结构实现

  1. 从静态页面中拷贝结构
// 底部按钮结构
@Builder
  getBottomBtn() {
    //已完成不显示任何按钮
    Row() {
      Button("延迟收货", { type: ButtonType.Capsule })
        .backgroundColor($r('app.color.btn_gray'))
        .fontColor($r('app.color.text_primary'))
        .fontSize(16)
        .height(50)
        .width(125)
      Button("提货", { type: ButtonType.Capsule })
        .backgroundColor($r('app.color.primary_disabled'))
        .fontColor($r('app.color.white'))
        .height(50)
        .flexGrow(1)
        .margin({ left: 13 })
    }
    .width('100%')
      .padding({ left: 15, right: 15 })
      .height(70).
      justifyContent(FlexAlign.SpaceBetween).
      alignItems(VerticalAlign.Center)
      .backgroundColor($r('app.color.white'))
  }

放置

35. 添加骨架屏判断

36. 在其他项目上新建一个工具

因为模拟器不让拖动图片到相册目录,所以我们只能自己手动的将线上的图片下载到相册

  • 在pages/Index.ets中实现

  • 点击按钮拷贝图片

项目仓库地址: copy_image: 用来解决鸿蒙Next版本的模拟器中无法拖入图片到相册的一个解决方案

37. SaveButton 唤出相册选择图片

basic模块

  1. 点击上传区域,弹出图片选择器
  2. 总结

1. 通过file.picker的图片选择器选择图片,得到了一些临时路径

2. 临时路径要传到我们自己的服务器-待实现

38. 拿到图片先将图片显示到预览区

  • 声明一个数组来管理选择的图片

  • 将选择的图片赋值给状态

  • 循环渲染图片

39. @CustomDialog封装图片预览

basic模块

分析,当我们上传的图片时,我们希望可以去放大预览该图片

并且图片还支持多张一起, 比如上传了三张,点第二张可以预览,左右滑可以切到第一张和第三张

  1. 新建components/HmPreview.ets文件
//预览图片弹层
@CustomDialog
  @Component
  struct HmPreview {
    controller: CustomDialogController
    // 支持多张图片预览  给多张地址 给一个需要预览的索引
    urls: string[] = [] // 多张地址
    selectIndex:number = 0 // 当前索引
    build() {
      Column() {
        Swiper() {
          ForEach(this.urls, (url: string) => {
            Image(url)
              .width("100%") // 只给宽度 不给高度 让自己撑开
              .onClick(() => {
                this.controller.close()
              })
          })
        }
        .indicator(false) // 去掉点的显示
          .index(this.selectIndex) // 当前要看的是第几张图片

      }
      .justifyContent(FlexAlign.Center)
        .width("100%")
        .height("100%")
        .backgroundColor($r("app.color.black"))
    }
  }
export { HmPreview }
  • 在HmUpload中引入,并初始化dialogController

  • 切换点击图片时,切换索引, 打开弹层

效果:

40. 接收父组件传入

因为图片的来源有可能是已经有的图片,所以我们需要外部传入该图片列表

  • 将State修饰符变成Prop修饰符

entry模块

  • 在提货详情的页面中进行传入

当图片地址发生变化时,通知父组件更新

basic模块

  • 在HmUpload中定义一个外部传入的函数

  • 在selectImage方法中选择完毕图片之后调用该方法

  • 根据提货图片存在判断是否可点击按钮
  • 根据提货图片存在判断是否可点击按钮
  • 根据提货图片存在判断是否可点击按钮

41. 根据提货图片存在判断是否可点击按钮

  • 根据提货图片存在判断是否可点击按钮

42. fs.copyFileSync 把相册文件存入沙箱

官网上传案例-链接

  1. 上传文件我们需要使用官方的uploadFile方法

我们需要在点击提货按钮时,将所有的图片进行上传, 并且监听上传进度,显示在页面上

第一个参数 context可以直接使用 getContext(this)获得

第二个参数的config参数为

其中File的参数为

综上分析,我们需要得到File中的uri这个属性,uri属性仅支持“internal”协议类型

但是我们通过弹窗发现,前面所得到的图片路径为

格式不匹配 !!!

需要转化

怎么转?

我们需要将选择的图片列表 一个个的拷贝到cache目录下,得到cache目录后新的目标路径

应用沙箱文件官网介绍

basic模块

  • 在HmUpload中导出一个上传方法

点击事件,存入了沙箱中

总结

  • 文件上传必须先把文件拷贝到沙箱文件中
  • 沙箱文件最终要上传时需要需要internal://cache目录 + 自己的文件路径

43. request.uploadFile 封装上传文件的api

现在上传组件已经准备好了参数,我们封住一个统一的上传api

在api下新建upload.ets

//上传api
import { request } from '@kit.BasicServicesKit'
import { BASE_URL, TOKEN_KEY } from '../constants'
import { ImageList, ResponseData } from '../models'
//返回一个列表Imagelist[]
export const uploadImage=async (context:Context,files:request.File[])=>{
  let config:request.UploadConfig={
    url:BASE_URL+'/files/imageUpload', //拼接上传的完整地址
    method:'POST',
    header:{
      Authorization:AppStorage.get(TOKEN_KEY)||"",//应用中的token
      "Content-Type":"multipart/form-data"//上传文件的参数类型
    },
    files,
    data:[]//批量上传需要携带的参数 用不上
  }
  return new Promise<ImageList[]>(async (resolve,reject)=>{
    try{
      let arr:ImageList[]=[]
      const task = await request.uploadFile(context,config)//返回上传任务创建成功
      task.on("fail",()=>{
        AlertDialog.show({
          message:"上传失败"
        })
        reject(new Error("上传失败"))
      })
      //每上传成功一次,就会进来
      task.on("headerReceive",(headers:object)=>{
        if (headers["body"]) {
          const result = JSON.parse(headers["body"]) as ResponseData<string>
          if (result.code===200) {
            arr.push({
              url:result.data as string
            })
          }
        }
      })
      task.on("complete",()=>{
        resolve(arr) //成功了,返回arr数组
      })
    }catch (error){
      AlertDialog.show({
        message:error.message
      })
      reject(error)
    }
  })
}
  • 在HmUpload组件的UploadFile方法中调用

44. 点击提货时上传

entry模块

  1. 拷贝接口类型-接口文档
  • 新建一个pickup提货的ets类型声明文件- models/pickup.ets
import { ImageListModel } from '@hm/basic'
export interface PickUpParams {
  /** 提货凭证照片数组 */
  cargoPickUpPictureList: ImageListModel[];
  /** 提货照片数组 */
  cargoPictureList: ImageListModel[];
  /** 司机作业id */
  id: string;
}
export class PickUpParamsModel implements PickUpParams {
  cargoPickUpPictureList: ImageListModel[] = []
  cargoPictureList: ImageListModel[] = []
  id: string = ''

  constructor(model: PickUpParams) {
    this.cargoPickUpPictureList = model.cargoPickUpPictureList
    this.cargoPictureList = model.cargoPictureList
    this.id = model.id
  }
}
  • 封装提货接口

  1. 提货点击事件-调用api

45. stack删除角标删除图片

在图片中放入右上角的图标

46. 加载进度遮罩

  • 封装一个HmLoading的组件,用过弹层
//加载进度遮罩
@CustomDialog
  @Component
  export struct HmLoading {
    controller: CustomDialogController
    title: string = '数据处理中..'

    build() {
      Row({ space: 6 }) {
        Text(this.title)
          .fontSize(16)
          .fontColor($r("app.color.text_primary"))
        LoadingProgress()
          .width(30)
          .height(30)
          .color($r("app.color.primary"))
      }
      .justifyContent(FlexAlign.Center)
        .width('100%')
        .height('100%')
        .backgroundColor("rgba(0,0,0,0.1)")
    }
  }
  • 在TaskDetail中定义loading弹出层的对象

  • 提货前后打开和关闭弹窗

  • 加载前后打开和关闭

效果:

设置全屏沉浸式

47. 交货列表加载

分析:

一个任务,如果已经提货,那么下一步应该就是司机去送货,完成交货,上一节我们完成了提货, 那么完成的这个任务应该在在途里面,所以我来先把在途的数据进行查询一下

  • 在途和待提货的区别就是查询的状态不同,刚刚好,我们前面定义了枚举,所以这里可以直接复用前面提货的TaskList的组件

entry模块

接下来,根据状态去调整按钮的显示和文本内容

  • 当任务的状态为待提货并且可提货时,按钮显示提货并且可用
  • 当任务的状态为待交货时,按钮显示提货并且可用
  • 当任务的状态为待回车登记时,按钮显示回车登记并且可用
  • 给之前的任务状态枚举再加一个属性
  • 在TaskItemCard组件中根据状态获取按钮的文本及可用状态

如果enablePickup为true 表示可提货

如果status 为 2 表示为待交货 显示交货

如果status 为 4表示 待回车登记

  • 按钮显示文本

⚠️: 这里的去提货的逻辑不用发生任何变化,因为交货的业务也是到详情页去提货,完成可以复用一摸一样的逻辑

效果:

48. 交货详情内容控制

根据状态显示不同的按钮


当一个任务提完货后,status应该是Line(在途),如果是Waiting(待提货)的话显示提货按钮,如果是Line(在途),显示交货按钮
1提货


2交货

  1. 交货时,隐藏提货上传组件,显示交货上传组件
  • 实现一个Builder函数来放置交货的上传

显示组件

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值