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

                                个人开发笔记,大佬留情

47. 交货列表加载

分析:

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

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

entry模块

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

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

如果enablePickup为true 表示可提货

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

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

  • 按钮显示文本

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

效果:

48. 交货详情内容控制

根据状态显示不同的按钮


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


2交货

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

显示组件



 

49. 交货按钮状态控制

控制按钮enable

50. 交货

Entry模块

标准流程

  1. 定义类型封装api
  2. 引入api
  3. 注册事件-调用api重新加载
  1. 定义请求参数类型
import { ImageList } from '@hm/basic'
export interface DeliverParamsType {
  /** 交付凭证列表 */
  certificatePictureList: ImageList[];
  /** 交付图片列表 */
  deliverPictureList: ImageList[];
  /** 司机作业id */
  id: string;
}
export class DeliverParamsTypeModel implements DeliverParamsType {
  certificatePictureList: ImageList[] = []
  deliverPictureList: ImageList[] = []
  id: string = ''

  constructor(model: DeliverParamsType) {
    this.certificatePictureList = model.certificatePictureList
    this.deliverPictureList = model.deliverPictureList
    this.id = model.id
  }
}
  1. 封装API

  1. 调用

51. 底部显示回车登记按钮

在已经交货的情况下,显示提货信息-交货信息- 底部的回车登记按钮,全部显示

52. 回车登记模式下-上传组件只读

因为货已经交了,所以回车登记模式下,此时只可以看,不能再传图片了

basic模块

  1. 给HmUpload一个属性来控制是否可上传和删除

  1. 通过该属性来控制上传部分的显示和隐藏

  1. 在TaskDetail中传入该属性

53. 回车登记页面

2024.8.24

  • 新建回车登记页面 pages/CardRecord/Record
import { HmNavBar, HmCard, HmCardItem } from '@hm/basic'
@Entry
  @Component
  struct CarRecord {
    build() {
      Column() {
        HmNavBar({ title: '回车登记' })
        Scroll() {
          Column() {
            HmCard(){
              HmCardItem({
                leftTitle: '出车时间',
                rightText: '2022.05.04 13:00',
                showRightIcon:false
              })
              HmCardItem({
                leftTitle: '回车时间',
                rightText: '请选择',
                showBottomBorder: false
              })
            }
          }
          .height('100%')
        }
        .layoutWeight(1)
        // 底部内容
        Row() {
          Button("交车",{ type: ButtonType.Capsule })
            .backgroundColor($r('app.color.primary'))
            .width(207)
            .height(50)
        }
        .backgroundColor($r('app.color.white'))
          .height(70)
          .width('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(VerticalAlign.Center)
      }
      .backgroundColor($r('app.color.background_page'))
        .height('100%')
    }
  }
  • TaskDetail页面跳转到回车登记

  • 在CarRecord接收参数id,并且获取任务的详情,赋值出车时间

54. 封装选择组件

basic模块

分析

  • 可以控制显示文本
  • 右侧的是否文本可以控制
  • 值改变时通知外部组件
  • 这个组件通用性不是那么大,简单封装一下
  • 在components下新建HmCheckBox.ets - 直接复制
@Component
  struct HmCheckBox {
    title: string = "测试"
    confirmText: string = "是"
    cancelText: string = '否'
    @Prop
    value: boolean = true // 决定选中左侧还是右侧\
    checkChange: (value: boolean) => void = () => {}
    build() {
      Row () {
        Row () {
          Text(this.title)
            .fontSize(14)
            .fontColor($r("app.color.text_primary"))

          // 右侧内容
          Row ({ space: 10 }) {
            Row () {
              Image(this.value ? $r("app.media.ic_radio_true") : $r("app.media.ic_radio_false"))
                .width(32)
                .aspectRatio(1)
              Text(this.confirmText)
            }
            .onClick(() => {
              this.value = true
              this.checkChange(this.value)
            })
            Row () {
              Image(!this.value ? $r("app.media.ic_radio_true") : $r("app.media.ic_radio_false"))
                .width(32)
                .aspectRatio(1)
              Text(this.cancelText)
            }
            .onClick(() => {
              this.value = false
              this.checkChange(this.value)
            })
          }
        }
        .width("100%")
          .borderRadius(10)
          .height(60)
          .padding({
            left: 15,
            right: 15
          })
          .justifyContent(FlexAlign.SpaceBetween)
          .backgroundColor($r("app.color.white"))

      }
      .width("100%")
        .padding({
          left: 15,
          right: 15
        })
        .margin({
          top: 15
        })
    }
  }
export { HmCheckBox }
  • 在回车登记中放置三个组件

效果:

55. DatePickerDialog时间选择器弹窗

效果:

56. 回车登记提交

  1. 定义提交数据类型
  2. 绑定数据到数据
  3. 提交回车登记

entry模块

  • 声明回车登记类型
import { ImageList } from '@hm/basic'
export interface CarRecordType {
  /** 事故说明,类型为“其他”时填写 */
  accidentDescription: string | null;
  /** 事故图片列表 */
  accidentImagesList: ImageList[] | null;
  /** 事故类型,1-直行事故,2-追尾事故,3-超车事故,4-左转弯事故,5-右转弯事故,6-弯道事故,7-坡道事故,8-会车事故,9-其他, */
  accidentType: string | null;
  /** 违章说明,类型为“其他”时填写 */
  breakRulesDescription: string | null;
  /** 违章类型,1-闯红灯,2-无证驾驶,3-超载,4-酒后驾驶,5-超速行驶,6-其他,可用 */
  breakRulesType: string | null;
  /** 扣分数据 */
  deductPoints: number | null;
  /** 回车时间,回车时间,格式yyyy-MM-dd HH:mm:ss,比如:2023-07-18 17:00:00 */
  endTime: string;
  /** 故障说明,类型为“其他”时填写 */
  faultDescription: string | null;
  /** 故障图片列表 */
  faultImagesList: ImageList[] | null;
  /** 故障类型,1-发动机启动困难,2-不着车,3-漏油,4-漏水,5-照明失灵,6-有异响,7-排烟异常,8-温度异常,9-其他,可用 */
  faultType: string | null;
  /** 运输任务id */
  id: string;
  /** 是否出现事故 */
  isAccident: boolean | null;
  /** 车辆是否可用 */
  isAvailable: boolean | null;
  /** 车辆是否违章 */
  isBreakRules: boolean | null;
  /** 车辆是否故障 */
  isFault: boolean | null;
  /** 罚款金额 */
  penaltyAmount: string | null;
  /** 出车时间,出车时间,格式yyyy-MM-dd HH:mm:ss,比如:2023-07-18 17:00:00 */
  startTime: string;
}
export class CarRecordTypeModel implements CarRecordType {
  accidentDescription: string | null = null
  accidentImagesList: ImageList[] | null = null
  accidentType: string | null = null
  breakRulesDescription: string | null = null
  breakRulesType: string | null = null
  deductPoints: number | null = null
  endTime: string = ''
  faultDescription: string | null = null
  faultImagesList: ImageList[] | null = null
  faultType: string | null = null
  id: string = ''
  isAccident: boolean | null = null
  isAvailable: boolean | null = null
  isBreakRules: boolean | null = null
  isFault: boolean | null = null
  penaltyAmount: string | null = null
  startTime: string = ''

  constructor(model: CarRecordType) {
    this.accidentDescription = model.accidentDescription
    this.accidentImagesList = model.accidentImagesList
    this.accidentType = model.accidentType
    this.breakRulesDescription = model.breakRulesDescription
    this.breakRulesType = model.breakRulesType
    this.deductPoints = model.deductPoints
    this.endTime = model.endTime
    this.faultDescription = model.faultDescription
    this.faultImagesList = model.faultImagesList
    this.faultType = model.faultType
    this.id = model.id
    this.isAccident = model.isAccident
    this.isAvailable = model.isAvailable
    this.isBreakRules = model.isBreakRules
    this.isFault = model.isFault
    this.penaltyAmount = model.penaltyAmount
    this.startTime = model.startTime
  }
}
  • 在CarRecord中定义数据

转化时间

获取时间

获取三个选择

  • 封装交车api

回车登记方法

绑定按钮

同学们到现在,我们已经将神领物流中的 提货-交货-回车登记业务跑通,剩下的将完成 对整个项目的一些核心点的优化

57. 延迟收货业务

entry模块

  1. 新建pages/Delay/Delay.ets -(Page)
  • 复制静态-快速布局

定义延迟提货提交类型参数

export interface DelayParamsType {
  /** 延迟原因 */
  delayReason: string;
  /** 延迟时间,格式:yyyy-MM-dd HH:mm */
  delayTime: string;
  /** 司机作业单id */
  id: string;
}
export class DelayParamsTypeModel implements DelayParamsType {
  delayReason: string = ''
  delayTime: string = ''
  id: string = ''

  constructor(model: DelayParamsType) {
    this.delayReason = model.delayReason
    this.delayTime = model.delayTime
    this.id = model.id
  }
}

封装一个转化时间函数

export  const  DateFormat =(value:Date)=>{
  //2023-12-23 05:12
  return value.getFullYear()+"-"+(value.getMonth()+1).toString().padStart(2,"0")+"-"
    +(value.getDate()).toString().padStart(2,"0")+""
    +value.getHours().toString().padStart(2,"0")+":"
    +value.getMinutes().toString().padStart(2,"0")
}

显示时间

效果:

封装延迟提货api

调用api

效果:

58. 没有数据页面

因为item有可能为null,所以加一个短路表达式

效果;

59. 上报异常页面基础布局

59. 上报异常页面基础布局

新建上报异常页面-复制静态-pages/ExceptionReport/ExceptionReport.ets

import { CommonRouterParams, DateFormat, HmCard, HmCardItem, HmNavBar, HmUpload } from '@hm/basic'
import { router } from '@kit.ArkUI'
import { ExceptionListModel, ExceptionParamsType, ExceptionParamsTypeModel } from '../../modals'

@Entry
  @Component
  struct ExceptionReport {
    @State
    exceptionForm: ExceptionParamsTypeModel = new ExceptionParamsTypeModel({}as ExceptionParamsType)
    aboutToAppear() {
      const params = router.getParams() as CommonRouterParams
      if (params.id) {
        this.exceptionForm.transportTaskId = params.id
      }
    }

    build() {
      Column() {
        HmNavBar({ title: '上报异常' })
        Scroll() {
          Column() {
            HmCard() {
              HmCardItem({
                leftTitle: '异常时间', rightText: this.exceptionForm.exceptionTime || '请选择',
                onRightClick: () => {
                  DatePickerDialog.show({
                    showTime: true,
                    useMilitaryTime: true,
                    onDateAccept: (value) => {
                      this.exceptionForm.exceptionTime=DateFormat(value)
                      AlertDialog.show({message:DateFormat(value)})
                    }
                  })
                }
              })
              HmCardItem({ leftTitle: '上报位置', rightText: '请选择' })
              HmCardItem({ leftTitle: '异常类型', rightText: '请选择' })
              HmCardItem({
                leftTitle: '异常描述',
                rightText: '',
                showRightIcon: false,
                showBottomBorder: false
              })
              TextArea({
                placeholder: '请输入异常描述'
              }).height(130).borderRadius(8).placeholderColor($r('app.color.text_secondary')).fontSize(14)
              Text(`0/50`)
                .margin({
                  top: -30
                })
                .textAlign(TextAlign.End)
                .width('100%')
                .padding({ right: 15 })
                .fontColor($r('app.color.text_secondary'))
              Row().height(20)

            }

            HmCard() {
              HmUpload({
                title: '上传图片(最多6张)',
                imageList: []
                , canUpLoad: true
              })
              Row().height(20)
            }
          }
        }.padding({
          bottom: 80
        })
          .layoutWeight(1)


        Row() {
          Button("提交").height(50).width(207).backgroundColor($r('app.color.primary_disabled'))
        }
        .position({
          y: '100%'
        })
          .height(70)
          .translate({
            y: -70
          })
          .width('100%')
          .justifyContent(FlexAlign.Center)
          .backgroundColor($r('app.color.white'))
      }
      .height('100%').backgroundColor($r('app.color.background_page'))
    }
  }
  • 声明数据类型-新建models/exception.ets
import { ImageList } from '@hm/basic'
export interface ExceptionParamsType {
  /** 异常描述,200字以内 */
  exceptionDescribe: string | null;
  /** 异常图片 */
  exceptionImagesList: ImageList[] | null;
  /** 上报异常位置 */
  exceptionPlace: string;
  /** 异常时间,精确到分钟 */
  exceptionTime: string;
  /** 异常类型(传中文),发动机启动困难,不着车,漏油,漏水,照明失灵,有异响,排烟异常,温度异常,其他 */
  exceptionType: string;
  /** 运输任务id */
  transportTaskId: string;
}
export class ExceptionParamsTypeModel implements ExceptionParamsType {
  exceptionDescribe: string | null = null
  exceptionImagesList: ImageList[] | null = null
  exceptionPlace: string = ''
  exceptionTime: string = ''
  exceptionType: string = ''
  transportTaskId: string = ''

  constructor(model: ExceptionParamsType) {
    this.exceptionDescribe = model.exceptionDescribe
    this.exceptionImagesList = model.exceptionImagesList
    this.exceptionPlace = model.exceptionPlace
    this.exceptionTime = model.exceptionTime
    this.exceptionType = model.exceptionType
    this.transportTaskId = model.transportTaskId
  }
}

效果;

60. @CustomDialog封装选择异常弹层组件

basic模块

  • 顶部内容可以设置标题,可以设置关闭按钮显示隐藏
  • 可以设置顶部按钮显示隐藏,可以设置底部按钮的文本和颜色
  • 可以自定义传入的结构
  • 支持自定义Dialog的形式弹出
  • 在components下新建HmSelectCard.ets组件

粘贴静态结构

@CustomDialog
  @Component
  struct HmSelectCard {
    //设置一些参数,当上层组件调用时传入相应的参数可以自己自定义组件了。
    controller: CustomDialogController
    title:string="请选择"
    showClose:boolean=true//是否显示关键按钮
    showButton:boolean=true
    buttonText:string="确定"
    confirm:()=>void =()=>{}//点击确定事件,由上层组件定义


    //接收内部要渲染的结构:
    @BuilderParam
    cardContent:()=>void


    build() {
      Column() {
        Row() {
          Text(this.title).fontSize(16).fontColor($r('app.color.text_primary'))
          if (this.showClose){
            Image($r("app.media.ic_btn_close")).width(13).height(13).onClick(()=>{
              this.controller.close()//关闭弹层
            })
          }
        }
        .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(VerticalAlign.Center)
          .height(60)
          .borderRadius({
            topLeft: 16,
            topRight: 16
          })
        if (this.cardContent){
          this.cardContent()//渲染内容
        }
        // 渲染内容
        if (this.showButton){
          Row() {
            Button(this.buttonText, { type: ButtonType.Capsule }).width(200).backgroundColor($r('app.color.primary')).height(45)
              .onClick(()=>{
                this.confirm()
              })
          }.width('100%').justifyContent(FlexAlign.Center)
        }
      }.backgroundColor($r('app.color.white')).justifyContent(FlexAlign.SpaceBetween).padding({
        left: 21.36,
        right: 21.36
      })
    }
  }
export { HmSelectCard }

控制弹层打开,选择弹层

传入渲染内容:

效果;

点击确定后 获取选择的类型 在上层组件显示

效果:

61. MapComponent组件-上报异常定位当前位置

需要在点击上报位置时,跳转到选择位置页面

  1. 点击上报位置跳转到选择位置

  1. 新建pages/SelectLocation/SelectLocation页面
@CustomDialog
  @Component
  struct HmSelectCard {
    //设置一些参数,当上层组件调用时传入相应的参数可以自己自定义组件了。
    controller: CustomDialogController
    title:string="请选择"
    showClose:boolean=true//是否显示关键按钮
    showButton:boolean=true
    buttonText:string="确定"
    confirm:()=>void =()=>{}//点击确定事件,由上层组件定义


    //接收内部要渲染的结构:
    @BuilderParam
    cardContent:()=>void


    build() {
      Column() {
        Row() {
          Text(this.title).fontSize(16).fontColor($r('app.color.text_primary'))
          if (this.showClose){
            Image($r("app.media.ic_btn_close")).width(13).height(13).onClick(()=>{
              this.controller.close()//关闭弹层
            })
          }
        }
        .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(VerticalAlign.Center)
          .height(60)
          .borderRadius({
            topLeft: 16,
            topRight: 16
          })
        if (this.cardContent){
          this.cardContent()//渲染内容
        }
        // 渲染内容
        if (this.showButton){
          Row() {
            Button(this.buttonText, { type: ButtonType.Capsule }).width(200).backgroundColor($r('app.color.primary')).height(45)
              .onClick(()=>{
                this.confirm()
              })
          }.width('100%').justifyContent(FlexAlign.Center)
        }
      }.backgroundColor($r('app.color.white')).justifyContent(FlexAlign.SpaceBetween).padding({
        left: 21.36,
        right: 21.36
      })
    }
  }
export { HmSelectCard }

62. 地图权限配置

官网文档: 文档中心

按照微信案例中的签名规则配置如下签名

p12

p7b

cer

csr

  • 在agc中开启地图使用

  • 在module.json5中配置client_id

  • 添加公钥指纹

  • 配置手动签名

windows模拟器无法显示地图

63. geoLocationManager 获取当前位置的经纬度

官方位置文档

  • 获取用户的地理位置必须经过用户同意-需要在初始化UIAbility的时候就发起请求
  • 并且需要在module.json5中配置地址位置的权限
  • module.json5
{
  "name": "ohos.permission.LOCATION",
    "reason": "$string:LOCATION_REASON",
    "usedScene": {
    "abilities": ["EntryAbility"],
      "when": "always"
  }
},
{
  "name": "ohos.permission.APPROXIMATELY_LOCATION",
    "reason": "$string:LOCATION_REASON",
    "usedScene": {
    "abilities": ["EntryAbility"],
      "when": "always"
  }
}
  • 在string.json中填写原因
{
  "name": "LOCATION_REASON",
    "value": "申请地理位置"
}
  • src/main/ets/entryability/EntryAbility.ts

效果:

获取一下当前位置

因为模拟器没有真实的位置,想要给一个真实的地址的话可以在模拟器位置心里填入一个经纬度如图

高德地图坐标拾取器

填入

测试

提交代码

64. convertCoordinate 转化经纬度坐标

我们需要将GCJ02转成WGS84

您可以通过map命名空间下的convertCoordinate方法进行坐标转换:

  • 转化代码
import { map, mapCommon } from '@kit.MapKit';
let wgs84Position: mapCommon.LatLng = { 
  latitude: 30, 
  longitude: 118 
};
let gcj02Posion: mapCommon.LatLng = await map.convertCoordinate(mapCommon.CoordinateType.WGS84, mapCommon.CoordinateType.GCJ02,wgs84Position);

突发!!!

目前获取的地理定位华为改为了GCJ02, 如果我们是显示在花瓣地图上,我们不需要再转化了

高德地图的定位其实需要逆向转化。

65. 设置当前位置

  • 声明一个controller
mapController: map.MapComponentController = new map.MapComponentController()
  • 回调赋值controller
        MapComponent({
          mapOptions: {
            position: {
              target: {
                latitude: 39.9,
                longitude: 116.4
              },
              zoom: 10
            }
          },
          mapCallback: (err, controller) => {
            if(!err) {
              this.mapController = controller
              this.getLocation()
            }
          }
        })
  • 获取位置转化坐标-调整照相机位置
async getLocation() {
    try {
      const rightPostion = await geoLocationManager.getCurrentLocation()

      //  通过控制器移动焦点到用户的经纬度位置
      this.mapController.moveCamera(map.newCameraPosition({
        target: {
          longitude: rightPostion.longitude,
          latitude: rightPostion.latitude
        },
        zoom: 16 // 缩放级别
      }))

      // 添加一个标记
       this.mapController.addPointAnnotation({
         position: {
           longitude: rightPostion.longitude,
           latitude: rightPostion.latitude
         },
         titles: [{
           content: '您当前的位置'
         }],
       })
    } catch (error) {
      AlertDialog.show({
        message: error.message
      })
    }

  }

66. nearbySearch 根据坐标点搜索周围地址

  • 声明一个class
  • 声明一个class
class SiteClass {
  name: string = ""
  distance: number = 0
}
  • 获取方法
// site  是@kit.MapKit提供的一个模块

const res = await site.nearbySearch({
    location: {
      longitude: rightResult.longitude,
      latitude: rightResult.latitude
    },
    pageSize: 4,
    pageIndex: 1,
    radius: 50
  })
  this.list = res.sites?.slice(0,4) as SiteClass[] // 只拿4条数据
  • 声明状态变量
  @State
  list: SiteClass[] = []
  • 赋值变量
 const res = await site.nearbySearch({
      location: {
        longitude: rightResult.longitude,
        latitude: rightResult.latitude
      },
      pageSize: 4,
      pageIndex: 1,
      radius: 50
    })
    this.list = res.sites as SiteClass[] // 只拿4条数据
  • 循环地址
       ForEach(this.list, (item: SiteClass) => {
          HmCardItem({ leftText: item.name, rightText: `${item.distance}m` })
            .onClick(() => {
              router.back({
                url: "pages/ExceptionReport/ExceptionReport",
                params: {
                  location: item.name
                }
              })
            })
        })

67. 点击返回地址

  • 点击周围地址,返回地址参数
              HmCardItem({ leftText: item.name, rightText: `${item.distance}m` })
                .onClick(() => {
                  router.back({
                    url: "pages/ExceptionReport/ExceptionReport",
                    params: {
                      location: item.name
                    }
                  })
                })
  1. 在上报异常中获取数据

basic模块

  • 在公共路由参数中再添加一个address字段
export interface CommonRouterParams {
  id?: string 
  oldTime?: string  
  location?: string 
}

entry模块

- aboutToAppear
+ onPageShow(): void {
    const params = router.getParams() as CommonRouterParams
+    if (params &&  params.location) {
+      this.exceptionForm.exceptionPlace = params.location
+    }

    if (params && params.id) {
      this.exceptionForm.transportTaskId = params.id
    }
  }
  1. 显示位置到CardItem组件上
 HmCardItem({ 
   leftText: '上报位置', 
   rightText: this.exceptionForm.exceptionPlace || '请选择', 
   onRightClick: () => {
   router.pushUrl({
     url: 'pages/SelectLocation/SelectLocation'
   })
 }})

68. 处理异常图片的赋值-控制按钮

  • 上传图片的赋值
  • 上传图片的赋值
      HmUpload({
        title: '上传图片(最多6张)',
        maxNumber: 6,
        canUpload: true,
        imageList: this.exceptionForm.exceptionImagesList || [],
        onImageListChange: (list: ImageList[]) => {
          this.exceptionForm.exceptionImagesList = []
        }
      })
  • 处理描述的双向绑定
  maxNumber: number = 50
   TextArea({
          placeholder: '请输入异常描述',
          text: this.exceptionForm.exceptionDescribe!
    })
     .height(130).borderRadius(8).placeholderColor($r('app.color.text_secondary')).fontSize(14).onChange((value) => {
            this.exceptionForm.exceptionDescribe = value
        }).maxLength(this.maxNumber)
        Text(`${this.exceptionForm.exceptionDescribe?.length || 0}/${this.maxNumber}`)
          .margin({
            top: -30
          })
          .textAlign(TextAlign.End)
          .width('100%')
          .padding({ right: 15 })
          .fontColor($r('app.color.text_secondary'))
  • 处理按钮状态

  getBtnEnable () {
    return !!(this.exceptionForm.exceptionDescribe &&
    this.exceptionForm.exceptionPlace &&
    this.exceptionForm.exceptionTime &&
    this.exceptionForm.exceptionType &&
    this.exceptionForm.exceptionDescribe)
  }
  • 绑定按钮
   Row() {
        Button("提交")
          .height(50)
          .width(207)
          .backgroundColor( $r('app.color.primary') )
          .enabled(this.getBtnEnable())
      }

提交代码

69. 上报异常提交

  1. 封装api
// 上报异常
export const exceptionReportAPI = (data: ExceptionParamsTypeModel) => {
  return Request.post("/driver/tasks/reportException", data)
}
  1. 按钮点击提交-返回上一个页面
async btnReport () {
    await exceptionReportAPI(this.exceptionForm)
    promptAction.showToast({
      message: '上报异常成功'
    })
    router.back()
  }

  • 提交代码

70. 根据上报异常数据进行显示

entry模块

如果任务详情中,有上报异常数据,则显示控制

  • 直接拷贝现成的结构
  // 获取异常信息
  @Builder
  getExceptionContent() {
    ForEach(this.taskDetailData.exceptionList, (item: ExceptionListModel) => {
      Row() {
        Column() {
          Row() {
            Text("上报时间").fontSize(14).fontColor($r('app.color.text_primary'))
            Text(item.exceptionTime).margin({ left: 20 }).fontColor($r('app.color.text_secondary'))
          }.height(50).alignItems(VerticalAlign.Center).width('100%')

          Row() {
            Text("异常类型").fontSize(14).fontColor($r('app.color.text_primary'))
            Text(item.exceptionType).margin({ left: 20 }).fontColor($r('app.color.text_secondary'))
          }.height(50).alignItems(VerticalAlign.Center).width('100%')

          Row() {
            Text("处理结果").fontSize(14).fontColor($r('app.color.text_primary'))
            Text("继续运输").margin({ left: 20 }).fontColor($r('app.color.text_secondary'))
          }.height(50).alignItems(VerticalAlign.Center).width('100%')
        }
        // 跳转到详情
        Image($r("app.media.ic_btn_more")).width(24).height(24)
      }
      .width('100%')
      .padding({ left: 15, right: 15 })
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.SpaceBetween)

    })
  }
  • 放置到任务详情的内容区
        // 💥💥 初始数据为空 记得加可选链
        if (this.taskDetailData.exceptionList?.length > 0) {
           HmToggleCard({ title: '异常信息' }) {
             this.getExceptionContent()
           }
         }

提交代码

71. 回显上报异常组件

需求

点击右侧的箭头-需要回到异常详情页去显示内容

因为没有查询异常详情的接口,我们可以把点击当前行的整行数据传到详情页

basic模块

  • 在公共路由参数添加一个新的参数

export class CommonRouterParams {
  id?: string = ''
  oldTime?: string = ''
  address?: string = ''
  addExcept?: boolean = false
  formData?: object
}

entry模块

  1. 新建pages/Exception/ExceptionDetail.ets

这里做过多操作已经无意,直接拷贝下方结构即可

  • 直接拷贝结构
import { HmNavBar, HmCardItem, HmCard, CommonRouterParams, ImageListModel } from '@hm/basic'
import router from '@ohos.router'
import {  ExceptionListModel } from '../../models'

@Entry
@Component
struct ExceptDetail {
  @State submitForm: ExceptionListModel = {} as ExceptionListModel
  aboutToAppear() {
    const params = router.getParams() as CommonRouterParams
    if (params && params.formData) {
      // 检查formData
      this.submitForm = params.formData as ExceptionListModel
    }
  }
  @Builder
  getCardChildren() {
    HmCardItem({ leftText: '异常时间', rightText: this.submitForm.exceptionTime, showRightIcon: false, })
    HmCardItem({ leftText: '上报位置', rightText: this.submitForm.exceptionPlace, showRightIcon: false, })
    HmCardItem({ leftText: '异常类型', rightText: this.submitForm.exceptionType, showRightIcon: false })
    HmCardItem({ leftText: '异常描述', showBottomBorder: false, showRightIcon: false, rightText: '' })
    Row() {
      Text(this.submitForm.exceptionDescribe).fontSize(14).fontColor($r('app.color.text_primary'))
    }.padding(15).justifyContent(FlexAlign.Start).width('100%')
  }
  @Builder
  getUpload() {
    if (this.submitForm.exceptionImagesList?.length) {
      Text("异常图片").width('100%').padding(10)
      Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
        ForEach(this.submitForm.exceptionImagesList, (item: ImageListModel) => {
          Image(item.url)
            .width(95)
            .height(95)
            .borderRadius(4)
            .margin({ right: 15 })
        })
      }
      .width('100%').margin({ top: 16.5, bottom: 16.5 })
    }
  }

  build() {
    Column() {
      HmNavBar({ title: '异常详情' })
      HmCard() {
        this.getCardChildren()
      }
      HmCard() {
        this.getUpload()
      }
    }.height('100%').backgroundColor($r('app.color.background_page'))
  }
}

export default ExceptDetail
  1. 点击TaskDetail的右侧箭头跳转-传递formData

        // 跳转到详情
        Image($r("app.media.ic_btn_more")).width(24).height(24)
        .onClick(() => {
          router.pushUrl({
            url: "pages/ExceptionReport/ExceptionDetail",
            params: {
              formData: item
            }
          })
        })

72. 导航功能

只在在途的状态下显示导航

  // 在途情况下 才显示导航
      if (this.taskDetailData.status === TaskTypeEnum.Line) {
        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
        })
      }
  • 点击开始导航
 // 获取基础信息
  // 开始导航
  async beginNav() {
    try {
      let context = getContext(this) as common.UIAbilityContext
      context.startAbility({
        action: 'ohos.want.action.viewData',
        entities: ['entity.system.browsable'],
        uri: encodeURI('https://gaode.com/search?query=' + this.taskDetailData.endAddress)
      })
    } catch (error) {
      AlertDialog.show({
        message: JSON.stringify(error)
      })
    }

  }

目前高德地图的导航调用方式无法得知,无法打开导航路线,只能用浏览器唤起打开高德页面,传入我们的地址

73. 打电话功能

  1. 导入使用包
import call from '@ohos.telephony.call'
  • 给之前的builder类型添加一个图标点击事件
interface BaseBuilderClass {
  title: string
  value: string 
  icon?: ResourceStr 
  iconClick?: () => void 
}
  1. 点击图标打电话
@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)
            .onClick(() => {
             item.iconClick && item.iconClick()
            })
        }
      }
    }.justifyContent(FlexAlign.SpaceBetween).width('100%').margin({
      top: 14
    })
  }
 this.getBaseContentItem({
      title: '联系电话',
      value: this.taskDetailData.startHandoverPhone,
      icon: $r('app.media.ic_phone'),
      iconClick: () => {
        call.makeCall(this.taskDetailData.startHandoverPhone);
      }
    })

74. 已完成任务列表加入搜索条件结构

  • 在TaskTabs中复用TaskList
else if(item.name === "finish") {
  TaskList({ 
    queryParams: {
      page: 1,
      pageSize: 5,
      status: TaskTypeEnum.Finish
    } as TaskListParamsModel 
  })
  }
  • 在TaskList中新增搜索条件

// 添加装饰器
 @Prop
  queryParams: TaskListParamsModel = {
    page: 1, // 表示查询第几页的数据 ++
    pageSize: 10, // 表示每页查几条数据
    status: TaskTypeEnum.Waiting,
  } as TaskListParamsModel

// 搜索条件builder
  @Builder
  getSearchForm() {
    Column() {
      Row() {
        Search({ placeholder: '请输入任务编号' }).backgroundColor($r('app.color.background_page')).height(32)
      }
      .justifyContent(FlexAlign.Center)
      .padding({ left: 15, right: 15, bottom: 5 })

      Row() {
        // 完成搜索页需要测试点击之后键盘和弹层同时弹出的情况
        Button(this.queryParams.startTime || '开始时间')
          .fontSize(14)
          .width(106)
          .height(32)
          .padding({ left: 0, right: 0 })
          .fontColor('#999')
          .backgroundColor($r('app.color.background_page'))
          .onClick(() => {
            DatePickerDialog.show({
              selected: new Date(),
              onDateAccept: (value) => {
                this.queryParams.startTime = dayjs(value).format('YYYY-MM-DD')
              }
            })
          })

        Text("至")
        Button(this.queryParams.endTime || '结束时间')
          .fontSize(14)
          .width(110)
          .height(32)
          .padding({ left: 0, right: 0 })
          .fontColor('#999')
          .backgroundColor($r('app.color.background_page'))
          .onClick(() => {
            DatePickerDialog.show({
              selected: new Date(),
              onDateAccept: (value) => {
                this.queryParams.endTime = dayjs(value).format('YYYY-MM-DD')
              }
            })
          })

        Button("筛选")
          .backgroundColor($r('app.color.primary'))
          .height(32)
          .width(60)
      }.width('100%').alignItems(VerticalAlign.Center).justifyContent(FlexAlign.SpaceAround)
    }
    .backgroundColor($r('app.color.white'))
    .padding(15)
    .justifyContent(FlexAlign.Center)
    .width('100%')
  }
  • TaskList中显示搜索表单
build() {
    Column() {
      if(this.queryParams.status === TaskTypeEnum.Finish) {
        this.getSearchForm()
      }
      HmList({
        onLoad: async () => {
          await this.getTaskList(true)
        },
        onRefresh: async () => {
          await this.onRefresh()
        },
        dataSource: $taskListData,
        renderItem: this.renderItem,
        finished: this.allPage < this.queryParams.page,
        finishText: '没啦没啦',
        loadingText: '拼命加载中'
      })
        .layoutWeight(1)
      
    }
    .height('100%')
  }
  • TaskItem-在已完成状态,不显示按钮
 if (this.taskItem.status !== TaskTypeEnum.Finish) {
    Button(this.getBtnText(), { type: ButtonType.Capsule })
      .backgroundColor($r('app.color.primary'))
      .fontColor($r("app.color.white"))
      .fontSize(14)
      .height(32)
      .enabled(this.getBtnEnable())
      .onClick(() => {
        this.toPickUp()
      })
  }
  • 点击卡片,当已完成时,可以进去查看详情
 .onClick(() => {
      if (this.taskItem.status === TaskTypeEnum.Finish) {
        router.pushUrl({
          url: 'pages/TaskDetail/TaskDetail',
          params: {
            id: this.taskItem.id
          }
        })
      }
    })
  • TaskDetail中当已完成时,可以展示图片

  • 当已完成时,不显示底部内容
 if(this.taskDetailData.status !== TaskTypeEnum.Finish) {
          this.getBottomBtn() // 底部按钮结构
        }

提交代码

75. 已完成搜索-选择日期

entry模块

  • 加入loading进度条
  loading: CustomDialogController = new CustomDialogController({
    builder: HmLoading({
      title: '搜索查询中'
    }),
    customStyle: true,
    autoCancel: false,
    alignment: DialogAlignment.Center
  })

控制能否点击,点击事件,查询数据

         Button("筛选")
          .backgroundColor( $r('app.color.primary'))
          .height(32)
          .width(60)
          .enabled(!!(this.queryParams.startTime && this.queryParams.endTime))
          .onClick(async() => {
            this.loading.open()
            this.allPage = 1
            this.queryParams.page = 1
            await this.getTaskList(false)
            this.loading.close()
          })

提交代码

76. 清空状态控制

目前我们发现一个问题,选择完开始时间和结束时间,没有办法取消了,所以我们定义一个状态来控制下

  • 定义一个状态
  @State
  reset: boolean = false // 用于控制重置状态
  • 如果查询完一次之后,将状态设置为true
   Button(this.reset ? "重置" : "筛选")
          .backgroundColor(this.getSearchEnable() ? $r('app.color.primary') : $r('app.color.primary_disabled'))
          .height(32)
          .width(60)
          .enabled(this.getSearchEnable())
          .onClick(async() => {
            if(this.reset){
              this.queryParams.startTime = ''
              this.queryParams.endTime = ''
            }
            
            this.reset = !this.reset
            this.loading.open()
            this.allPage = 1
            this.queryParams.page = 1
            await this.getTaskList(false)
            this.loading.close()
          })

77. 输入单号搜索

输入单号不影响分页及其他,只覆盖数据,不影响下拉刷新的逻辑

  • 注意参数传递不是id, 是transportTaskId。任务编号显示的是id
  • 如果有值,那么意味着即使查也只能查出一条,那么不能触发上拉加载
  • 有值情况下,如果已经有开始时间和结束时间,要清空,
  • 如果有没有值,要清空查询参数中的transportTaskId
 Search({ placeholder: '请输入任务编号', value: this.queryParams.transportTaskId || '' }).backgroundColor($r('app.color.background_page')).height(32)
         .onSubmit(async value => {
            this.loading.open()
            this.queryParams.page = 1
            if(value) {
              this.queryParams.startTime = ''
              this.queryParams.endTime = ''
              this.reset = false
              this.allPage = 0 // 有值意味着只有一条 因为是根据单号传的,所以不让它继续往后查 否则会重复
              this.queryParams.transportTaskId =  value 
            }else {
              this.allPage = 1 // 没值意味着查不到 重新加载
              this.queryParams.transportTaskId = ""
            }
            const result = await getTaskList(this.queryParams)
            this.taskListData = result.items || []
            this.loading.close()
          })
  • 这里逻辑捋一捋

整体代码

import { HmList, HmLoading } from '@hm/basic/Index'
import { getTaskList } from '../../../api'
import { TaskInfoItem, TaskInfoItemModel, TaskListParams, TaskListParamsModel, TaskTypeEnum } from '../../../models'
import TaskItemCard from './TaskItemCard'
import { promptAction } from '@kit.ArkUI'

// 待提货
@Component
struct TaskList {
  loading: CustomDialogController = new CustomDialogController({
    builder: HmLoading(),
    customStyle: true
  })
  @State
  queryParams: TaskListParamsModel = new TaskListParamsModel(
    {
      status: TaskTypeEnum.Waiting, // 待提货的类型
      page: 1, // 第几页
      pageSize: 5 // 每页几条数据
    } as TaskListParams
  )
  @State
  taskListData: TaskInfoItem[] = []
  @State
  allPage: number = 1 // 默认只有一页
  @State
  reset: boolean = false // 用来控制重置状态

  async getTaskList(append: boolean) {
    const result = await getTaskList(this.queryParams)
    // 追加数据
    // this.taskListData = this.taskListData.concat(result.items) // 拿到返回的数组
    if (append) {
      this.taskListData.push(...result.items || []) // 延展运算符的写法
    } else {
      this.taskListData = result.items // 直接赋值
    }

    this.allPage = result.pages // 总页数
    this.queryParams.page++ // 下次请求的页码
  }

  @Builder
  renderItem(item: object) {
    TaskItemCard({ taskItem: item as TaskInfoItemModel })
  }

  // 下拉刷新函数
  async onRefresh() {
    // 重新请求第一页数据
    this.queryParams.page = 1 // 重置第一页
    await this.getTaskList(false) // 直接赋值
  }

  // 补零函数
  addZero(value: number) {
    return value.toString().padStart(2, "0")
  }

  // 获取筛选按钮是否可用
  getSearchEnable() {
    return !!(this.queryParams.startTime && this.queryParams.endTime)
  }

  @Builder
  getSearchForm() {
    Column() {
      Row() {
        Search({ placeholder: '请输入任务编号', value: this.queryParams.transportTaskId || "" })
          .backgroundColor($r('app.color.background_page'))
          .height(32)
          .onSubmit(async (value) => {
            // 点击键盘的右下角的提交
            // 4042715413936324752
            this.loading.open()
            this.allPage = 1 // 总页数为1
            this.queryParams.page = 1 // 查第一页
            if (value) {
              // 有单号的情况 如果有只有一条记录
              // 后台业务缺陷- 按道理来说 如果传单号的了 应该开始时间和结束时间自动忽略
              this.queryParams.startTime = ""
              this.queryParams.endTime = ""
              this.queryParams.transportTaskId = value
            } else {
              // 没有单号的情况下恢复之前的查询
              this.queryParams.transportTaskId = ""
            }
            await this.getTaskList(false) // 不追加数据
            this.loading.close()
          })
      }
      .justifyContent(FlexAlign.Center)
      .padding({ left: 15, right: 15, bottom: 5 })

      Row() {
        // 完成搜索页需要测试点击之后键盘和弹层同时弹出的情况
        Button(this.queryParams.startTime || '开始时间')
          .fontSize(14)
          .width(106)
          .height(32)
          .padding({ left: 0, right: 0 })
          .fontColor('#999')
          .backgroundColor($r('app.color.background_page'))
          .onClick(() => {
            DatePickerDialog.show({
              selected: new Date(),
              onDateAccept: (value) => {
                this.queryParams.startTime =
                  `${value.getFullYear()}-${this.addZero(value.getMonth() + 1)}-${this.addZero(value.getDate())}`
              }
            })
          })

        Text("至")
        Button(this.queryParams.endTime || '结束时间')
          .fontSize(14)
          .width(110)
          .height(32)
          .padding({ left: 0, right: 0 })
          .fontColor('#999')
          .backgroundColor($r('app.color.background_page'))
          .onClick(() => {
            DatePickerDialog.show({
              selected: new Date(),
              onDateAccept: (value) => {
                this.queryParams.endTime =
                  `${value.getFullYear()}-${this.addZero(value.getMonth() + 1)}-${this.addZero(value.getDate())}`
              }
            })
          })
        Button(this.reset ? "重置" : "筛选")
          .backgroundColor($r('app.color.primary'))
          .height(32)
          .width(60)
          .enabled(this.getSearchEnable())
          .onClick(async () => {
            if (this.reset) {
              // 表示现在需要重置 将开始时间和结束时间清空
              this.queryParams.startTime = ""
              this.queryParams.endTime = ""
            }
            this.reset = !this.reset // 状态取反

            this.loading.open()
            // 搜索
            // 处理总页数 处理第几页
            this.allPage = 1 // 默认有一页
            this.queryParams.page = 1
            // 获取数据
            await this.getTaskList(false) // true是追加 false是不追加

            this.loading.close()

          })
      }.width('100%').alignItems(VerticalAlign.Center).justifyContent(FlexAlign.SpaceAround)
    }
    .backgroundColor($r('app.color.white'))
    .padding(15)
    .justifyContent(FlexAlign.Center)
    .width('100%')
  }

  build() {
    Column() {
      // 是否显示搜索条件
      if (this.queryParams.status === TaskTypeEnum.Finish) {
        // 显示搜索条件
        this.getSearchForm()
      }
      HmList({
        dataSource: this.taskListData, // 数据源
        finished: this.allPage < this.queryParams.page, // 是否还有下一页
        // 上拉加载的函数
        onLoad: async () => {
          // 上拉加载
          await this.getTaskList(true) // 追加逻辑
        },
        onRefresh: async () => {
          // 下拉刷新
          await this.onRefresh()
        },
        renderItem: (item: object) => {
          // 如果需要的是builderParams的参数 可以用普通函数包裹一个Builder的函数
          this.renderItem(item)
        },
        loadingText: '拼命加载中',
        finishText: '没啦没啦'
      }).height("100%")
    }

  }
}

export default TaskList

  • 提交代码

78. 多线程处理图片压缩

性能优化

  • 资源-图片不要全用原图
  • 请求-尽可能延后-最好不要把所有请求都放在入口处
  • 组件-尽可能采用拆分组件-延展组件- 低开门-高入户
  • 设计-多层架构-高内聚低耦合-公用har-包共用hsp
  • 多任务处理-多线程-耗时任务-IO操作-文件压缩-解压缩-裁剪-位移
  • 主线程-子线程处理耗时任务(诸多限制

  1. 建立多线程 子线程
  2. 将主线程拿到的图片给到子线程
  3. 子线程进行图片的压缩
  4. 压缩完成 将完成的图片信息回传给主线程
  5. 子线程自动关闭销毁
  6. 主线程继续上传操作
  • 新建一个UploadWorker
import { ImageList } from '@hm/basic/Index';
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, util, worker } from '@kit.ArkTS';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

export class PostParams {
  files: ImageList[] = []
  filePath: string = "" // 沙箱目录
}

/**
 * Defines the event handler to be called when the worker thread receives a message sent by the host thread.
 * The event handler is executed in the worker thread.
 *
 * @param e message data
 */
workerPort.onmessage = async (e: MessageEvents) => {
  // 处理耗时任务e
  const params = e.data as PostParams
  // 进行图片压缩
  if (params && params.files) {
     // 拿到原图
    // 对原图进行压缩
    // 需要将原图压缩成新的图片 存到一个位置
    const imagePackerAPI = image.createImagePacker() // 创建图片压缩API
    let packOpts: image.PackingOption = { format: "image/jpeg", quality: 20 };
    let arr: ImageList[] = []
    while (params.files.length) {
      const obj = params.files.pop() // 每次取一条
      const sourceFile = fileIo.openSync(obj?.url, fileIo.OpenMode.READ_ONLY) // 打开来源文件
      // 对来源文件进行压缩
      const newFileName = params.filePath + "/" + util.generateRandomUUID() + ".jpg"
      const targetFile = fileIo.openSync(newFileName, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
      // 目标文件的fd
      await imagePackerAPI.packToFile(image.createImageSource(sourceFile.fd), targetFile.fd, packOpts)
      fileIo.closeSync(sourceFile.fd) // 关闭来源文件
      fileIo.closeSync(targetFile.fd) // 关闭目标文件
      arr.push({
        url: newFileName
      })
    }
    // 图片压缩完毕 将图片传出去
    // 这里不需要序列化
    workerPort.postMessage({ files: arr }) // 子线程往主线程发消息
    workerPort.close() // 结束子线程

  }


}

/**
 * Defines the event handler to be called when the worker receives a message that cannot be deserialized.
 * The event handler is executed in the worker thread.
 *
 * @param e message data
 */
workerPort.onmessageerror = (e: MessageEvents) => {
}

/**
 * Defines the event handler to be called when an exception occurs during worker execution.
 * The event handler is executed in the worker thread.
 *
 * @param e error message
 */
workerPort.onerror = (e: ErrorEvent) => {
}
  • 提货交货时处理图片压缩

  // 交货
  async onDeliver() {
    this.loading.open()
    // 建立子线程
    const imageCompress1 = new worker.ThreadWorker("entry/ets/workers/UploadWorker.ets") // 子线程1
    const imageCompress2 = new worker.ThreadWorker("entry/ets/workers/UploadWorker.ets") // 子线程2
    let certificatePictureList: ImageList[] = []
    let deliverPictureList: ImageList[] = []

    // 用来检查是否全部都压缩完毕
    const checkFile = async () => {
      if (certificatePictureList.length && deliverPictureList.length) {
        // 此时此刻才可以进行下一步操作
        const result = await Promise.all([UploadFile(certificatePictureList), UploadFile(deliverPictureList)])
        await deliver(new DeliverParamsTypeModel({
          id: this.taskDetailData.id,
          certificatePictureList: result[0],
          deliverPictureList: result[1]
        }))
        this.getTaskDetail(this.taskDetailData.id) // 重新拉取数据
        this.scroller.scrollEdge(Edge.Top)
        promptAction.showToast({ message: '交货成功' })
        this.loading.close()
      }
    }
    
    imageCompress1.onmessage = (e: MessageEvents) => {
      const params = e.data as PostParams
      certificatePictureList = params.files // 已经压缩完毕的图片
      checkFile() // 检查是否都完成了
    }
    imageCompress2.onmessage = (e: MessageEvents) => {
      const params = e.data as PostParams
      deliverPictureList = params.files // 已经压缩完毕的图片
      checkFile()
    }
    imageCompress1.postMessage({
      files: [...this.taskDetailData.certificatePictureList],
      filePath: getContext().filesDir
    }) // 给子线程发消息
    imageCompress2.postMessage({
      files: [...this.taskDetailData.deliverPictureList],
      filePath: getContext().filesDir
    }) // 给子线程发消息
    
   
    // this.loading.open()
    // const certificatePictureList = await UploadFile(this.taskDetailData.certificatePictureList) // 传入提货凭证
    // const deliverPictureList = await UploadFile(this.taskDetailData.deliverPictureList) // 货品照片
    // // 交货操作
    // await deliver(new DeliverParamsTypeModel({
    //   id: this.taskDetailData.id,
    //   certificatePictureList,
    //   deliverPictureList
    // }))
    // // 只需要重新获取数据
    // // 重新获取数据
    // this.getTaskDetail(this.taskDetailData.id) // 重新拉取数据
    //
    // this.scroller.scrollEdge(Edge.Top)
    // this.loading.close()
    // // 滚动到顶部
    // promptAction.showToast({ message: '交货成功' })

  }

79. 集成消息模块

消息模块内容较为简单

同学们将老师提供的模块导入到项目中结成即可,也可以自己尝试自己去编写

entry模块

  1. 新建models/message.ts
export interface MessageQueryType {
  /** 消息类型,200:司机端公告,201:司机端系统通知 */
  contentType: number;
  page: number;
  pageSize: number;
}


export interface MessageData {
  /** 总条目数 */
  counts: number;
  items: MessageItem[];
  /** 页码 */
  page: number;
  /** 总页数 */
  pages: number;
  /** 页尺寸 */
  pageSize: number;
}

/** 数据列表 */
export interface MessageItem {
  /** 1:用户端,2:司机端,3:快递员端,4:后台管理系统 */
  bussinessType: number;
  /** 消息内容 */
  content: string;
  /** 消息类型,300:快递员端公告,301:寄件相关消息,302:签收相关消息,303:快件取消消息,200:司机端公告,201:司机端系统通知 */
  contentType: number;
  /** 创建时间 */
  created: string;
  /** 创建者 */
  createUser: number;
  /** 主键 */
  id: number;
  /** 消息是否已读,0:未读,1:已读 */
  isRead: number;
  /** 读时间 */
  readTime: string;
  /** 相关id */
  relevantId: number;
  /** 消息标题 */
  title: string;
  /** 更新时间 */
  updated: string;
  /** 更新者 */
  updateUser: number;
  /** 消息接受者 */
  userId: number;
}
interface  DetailType {
  id: string
  content: string
  created: string
}

export class MessageQueryTypeModel implements MessageQueryType {
  contentType: number = 0
  page: number = 0
  pageSize: number = 0

  constructor(model: MessageQueryType) {
    this.contentType = model.contentType
    this.page = model.page
    this.pageSize = model.pageSize
  }
}
export class MessageDataModel implements MessageData {
  counts: number = 0
  items: MessageItem[] = []
  page: number = 0
  pages: number = 0
  pageSize: number = 0

  constructor(model: MessageData) {
    this.counts = model.counts
    this.items = model.items
    this.page = model.page
    this.pages = model.pages
    this.pageSize = model.pageSize
  }
}
export class MessageItemModel implements MessageItem {
  bussinessType: number = 0
  content: string = ''
  contentType: number = 0
  created: string = ''
  createUser: number = 0
  id: number = 0
  isRead: number = 0
  readTime: string = ''
  relevantId: number = 0
  title: string = ''
  updated: string = ''
  updateUser: number = 0
  userId: number = 0

  constructor(model: MessageItem) {
    this.bussinessType = model.bussinessType
    this.content = model.content
    this.contentType = model.contentType
    this.created = model.created
    this.createUser = model.createUser
    this.id = model.id
    this.isRead = model.isRead
    this.readTime = model.readTime
    this.relevantId = model.relevantId
    this.title = model.title
    this.updated = model.updated
    this.updateUser = model.updateUser
    this.userId = model.userId
  }
}
export class DetailTypeModel implements DetailType {
  id: string = ''
  content: string = ''
  created: string = ''

  constructor(model: DetailType) {
    this.id = model.id
    this.content = model.content
    this.created = model.created
  }
}
  • 在models/index.ts中导出
export * from './message'
  • 新建api/message.ets
import { Request } from '@hm/basic'
import {  MessageQueryTypeModel, MessageDataModel } from '../models'

// 获取信息
export const getMessage = (params: MessageQueryTypeModel) => {
  return Request.get<MessageDataModel>("/driver/messages/page", params)
}

// 标记已读
export const readMessage = (id: string) => {
  return Request.put<null>(`/driver/messages/${id}`)
}

// 全部已读
export const readAllMessage = (contentType: string) => {
  return Request.put<null>(`/driver/messages/readAll/${contentType}`)
}
  • 在api/index.ets导出
export * from './message'
  • 在pages/Index下新建Message/Message.ets组件
import { HmNavBar, TabClass } from '@hm/basic'
import MessageTemplate from './MessageTemplate'
@Component
struct Message {
  @State currentTabName: string = 'information'
  @State tabBarData: TabClass[] = [{
    title: '公告',
    name: 'information'
  }, {
    title: '任务通知',
    name: 'notice'
  }]
  @Builder
  TabBuilder(item: TabClass) {
    Column() {
      Text(item.title)
        .fontSize(16)
        .fontColor(this.currentTabName === item.name ? $r('app.color.text_primary') : $r('app.color.text_secondary'))
        .fontWeight(this.currentTabName === item.name ? 500 : 400)
        .lineHeight(50)
        .height(50)
      Divider()
        .strokeWidth(4)
        .color($r('app.color.primary'))
        .opacity(this.currentTabName === item.name ? 1 : 0)
        .width(this.currentTabName === item.name ? 23 : 0)
        .animation({
          duration: 300,
          curve: Curve.EaseOut,
          iterations: 1,
          playMode: PlayMode.Normal
        })
    }
  }

  build() {
    Column() {
      HmNavBar({ title: '消息', showBackIcon: false })
      Tabs({
        barPosition: BarPosition.Start,
        controller: new TabsController()
      }) {
        ForEach(this.tabBarData, (item: TabClass) => {
          TabContent() {
            Flex() {
              MessageTemplate({ type: item.name || '' })
            }.height('100%').backgroundColor($r('app.color.background_page'))
          }.tabBar(this.TabBuilder(item))
        })
      }.animationDuration(300).onChange((index) => {
        this.currentTabName = index === 0 ? 'information' : 'notice'
      })
    }.width('100%').height('100%')
  }
}

export default Message
  • 在Message.ets旁新建MessageTemplate.ets
import { getMessage, readAllMessage, readMessage } from '../../../api'
import { MessageQueryTypeModel, MessageItemModel } from '../../../models'
import router from '@ohos.router';
import { HmList } from '../../../components'
import promptAction from '@ohos.promptAction';

@Component
struct MessageTemplate {
  @Prop
  type: string
  @State
  queryParams: MessageQueryTypeModel = new MessageQueryTypeModel({
    page: 1,
    pageSize: 10,
    contentType: this.type === "information" ? 200 : 201
  })
  @State
  allPage: number = 1
  @State
  messageList: MessageItemModel[] = []

  async getMessage(append: boolean) {
    if (this.allPage < this.queryParams.page) {
      return
    }
    const result = await getMessage(this.queryParams)
    if (append) {
      this.messageList = this.messageList.concat(result.items)   // 追加数据
    } else {
      this.messageList = result.items // 覆盖数据
    }
    this.queryParams.page++
    this.allPage = result.pages
  }
  async refreshData() {
    this.allPage = 1 // 只要下拉就默认有一页
    this.queryParams.page = 1
    await this.getMessage(false)
    promptAction.showToast({ message: '刷新成功' })
  }

  @Builder
  renderItem(item: object) {
    if(this.type === "information") {
      this.renderInformationItem(item as MessageItemModel)
    }
    if(this.type === "notice") {
      this.renderNoticeItem(item as MessageItemModel)
    }
  }
  // 读取
  readSuccess() {
    this.messageList = this.messageList.map(item => {
      item.isRead = 1
      return item
    })
  }
  @Builder
  renderInformationItem (item: MessageItemModel) {
    Row() {
      Row() {
        if (item.isRead === 0) {
          Text("").width(8).height(8).backgroundColor($r('app.color.primary')).borderRadius(4)
        }
        Text(item.content)
          .fontSize(14)
          .fontColor($r("app.color.text_primary"))
          .margin({ left: 6 })
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .maxLines(1)
      }.width(230)

      Text(item.created).fontSize(12).fontColor($r("app.color.text_secondary"))
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .alignItems(VerticalAlign.Center)
    .padding({ left: 22, right: 22 })
    .height(60)
    .backgroundColor($r('app.color.white'))
    .border({ width: { bottom: 1 }, color: $r("app.color.background_page") })
    .width('100%')
    .onClick(() => {
      // 先更改数据状态
      item.isRead = 1
      this.messageList = [...this.messageList]
      router.pushUrl({
        url: 'pages/Index/Message/MessageDetail',
        params: {
          content: item.content,
          created: item.created,
          id: item.id
        }
      })
    })
  }
  @Builder
  renderNoticeItem(message: MessageItemModel) {
    Column() {
      Row() {
        Text("您有新的运输任务")
        if (message.isRead === 0) {
          Text("")
            .width(8)
            .height(8)
            .backgroundColor($r('app.color.primary'))
            .borderRadius(4)
            .margin({ left: 10 })
        }
      }

      Divider().color($r('app.color.background_page')).margin({ top: 13 })
      Text(message.content.replace(new RegExp("/\/n/") , ""))
        .margin({ top: 11, bottom: 22.5 })
        .fontSize(13)
        .fontColor($r('app.color.text_secondary'))
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .lineHeight(22)
      Row() {
        Text(message.updated).fontSize(12).fontColor($r('app.color.text_secondary'))
        Button("查看详情", { type: ButtonType.Capsule })
          .fontColor($r('app.color.primary'))
          .border({ width: 1, color: $r('app.color.primary') })
          .height(24)
          .width(76)
          .backgroundColor('#fff')
          .onClick(() => {
            readMessage(message.id + "")
            // 跳转到任务详情
            router.pushUrl({
              url: 'pages/TaskDetail/TaskDetail',
              params: {
                id: message.relevantId
              }
            })
          })
      }.justifyContent(FlexAlign.SpaceBetween).alignItems(VerticalAlign.Center).width('100%')
    }
    .width('100%')
    .backgroundColor($r('app.color.white'))
    .borderRadius(10)
    .padding(16)
    .margin({ bottom: 15 })
    .alignItems(HorizontalAlign.Start)
  }

  build() {
    Column() {
      Row() {
        Image($r("app.media.ic_yidu")).width(16).height(16)
        Text("全部已读").fontSize(14).fontColor($r('app.color.text_secondary')).margin({
          left: 9
        }).onClick(async () => {
          this.queryParams.contentType && await readAllMessage(this.queryParams.contentType.toString())
          promptAction.showToast({ message: '全部已读' })
          this.readSuccess && this.readSuccess()
        })
      }.padding({
        left: 17,
        right: 17,
        top: 14,
        bottom: 14
      }).width('100%')// 全部已读
      // 公告内容
      HmList({
        finished: this.allPage < this.queryParams.page,
        dataSource: $messageList,
        onRefresh: async () => {
          await this.refreshData()
        },
        onLoad: async () => {
          await this.getMessage(true)
        } ,
        renderItem: (item: object) => {
          this.renderItem(item)
        }
      })
    }.height('100%')
  }
}

export default MessageTemplate
  • 在Index/Index.ets中引入Message
import Message from './Message/Message'


build() {
    Tabs({ barPosition: BarPosition.End }){
      ForEach(this.tabsData, (item: TabClass) => {
        TabContent(){
          if(item.name === 'task') {
           TaskTabs()
          }
          else if(item.name === 'message') {
             Message()
          }
          else {
             My()
          }
        }.tabBar(this.getTabBar(item))
      })
    }
    .onChange(index => {
      this.currentName = this.tabsData[index].name
    })
    .animationDuration(300)
  }
  • 在pages/Index/Message下新建MessageDetail.ets(page)
import { HmNavBar } from '@hm/basic'
import router from '@ohos.router';
import { readMessage } from '../../../api/message'
import { DetailTypeModel } from '../../../models'
@Entry
@Component
struct MessageDetail {
  @State detailForm: DetailTypeModel = new DetailTypeModel({
    content: '',
    created: '',
    id: ''
  })

  async aboutToAppear() {
    const params = router.getParams()
    this.detailForm = params as DetailTypeModel
    readMessage(this.detailForm.id)
  }

  build() {
    Flex({ direction: FlexDirection.Column }) {
      HmNavBar({ title: '详情' })
      Column() {
        Text("系统公告").fontSize(16).fontColor($r('app.color.text_primary')).lineHeight(22)
        Text(this.detailForm.created).fontSize(12).fontColor($r('app.color.text_secondary')).lineHeight(17).margin({
          top: 7,
          bottom: 17
        })
        Text(this.detailForm.content).fontSize(14).lineHeight(22)
      }.alignItems(HorizontalAlign.Start).padding({
        top: 20,
        left: 16,
        right: 16
      })
    }.height('100%').backgroundColor($r('app.color.white'))
  }
}

export default MessageDetail

80. 集成车辆信息

entry模块

在models/car.ets中加入车辆信息类型

import { ImageList } from '@hm/basic'
/** 响应数据 */
export interface UserCarDataType {
  /** 载重 */
  allowableLoad: string;
  /** 所属机构名称 */
  currentOrganName: string;
  /** 车辆编号 */
  id: string;
  /** 车牌号码 */
  licensePlate: string;
  /** 图片 */
  pictureList: ImageList[];
  /** 车辆类型名称 */
  truckType: string;
}
export class UserCarDataTypeModel implements UserCarDataType {
  allowableLoad: string = ''
  currentOrganName: string = ''
  id: string = ''
  licensePlate: string = ''
  pictureList: ImageList[] = []
  truckType: string = ''

  constructor(model: UserCarDataType) {
    this.allowableLoad = model.allowableLoad
    this.currentOrganName = model.currentOrganName
    this.id = model.id
    this.licensePlate = model.licensePlate
    this.pictureList = model.pictureList
    this.truckType = model.truckType
  }
}
  • 在models/index.ets统一导出
export * from './car'

在api/user.ts中加入封装获取车辆信息api

// 获取用户车辆信息
export const getUserCarInfo = () => {
  return Request.get<UserCarDataTypeModel>("/driver/users/truck")
}

新建pages/Car/Car.ets - Page

import { HmNavBar } from '@hm/basic'
import { getUserCarInfo } from '../../api'
import { UserCarDataTypeModel, UserCarDataType, ImageList } from '../../models'
@Entry
@Component
struct Car {
  @State
  userCarInfo: UserCarDataTypeModel = new UserCarDataTypeModel({} as UserCarDataType)
  aboutToAppear() {
    this.getUserCarInfo()
  }
  async getUserCarInfo() {
    this.userCarInfo = await getUserCarInfo()
  }
  @Builder
  getContentItem (item: CarItem) {
    Row() {
      Text(item.leftText).fontSize(14).fontWeight(400).fontColor($r('app.color.text_secondary'))
      Text(item.rightValue).fontSize(14).fontColor($r('app.color.text_primary')).fontWeight(400)
    }.justifyContent(FlexAlign.SpaceBetween).alignItems(VerticalAlign.Center).width('100%').height(40)
  }
  build() {
    Column() {
      HmNavBar({ title: '车辆信息' })
      Swiper(new SwiperController()) {
        ForEach(this.userCarInfo.pictureList, (item: ImageList) => {
          Row() {
            Image(item.url).width('100%').height('100%').objectFit(ImageFit.Cover).
            borderRadius(8)
          }.height(201).padding({ left: 15, right: 15, top: 15  })
        })
      }
      .loop(false)
      .cachedCount(3)
      .indicator(true)
      .curve(Curve.Linear)

      // 信息列表
      Column() {
        this.getContentItem({ leftText: '车辆编号', rightValue: this.userCarInfo.id })
        this.getContentItem({ leftText: '车辆号牌', rightValue: this.userCarInfo.licensePlate })
        this.getContentItem( { leftText: '车型', rightValue: this.userCarInfo.truckType })
        this.getContentItem({ leftText: '所属机构', rightValue: this.userCarInfo.currentOrganName })
        this.getContentItem({ leftText: '载重', rightValue: this.userCarInfo.allowableLoad })
      }
      .padding({
        top: 19.5,
        bottom: 19.5,
        left: 20,
        right: 20
      })
      .backgroundColor($r('app.color.white'))
      .margin(15)
      .borderRadius(8)
    }.height('100%').backgroundColor($r('app.color.background_page')).width('100%')
  }
}


class  CarItem {
  leftText: string = ""
  rightValue: string = ""
}
export default Car
  • 点击我的-车辆信息跳转过去
 HmCardItem({ leftText: '车辆信息', rightText: '', onRightClick: () => {
          router.pushUrl({
            url: 'pages/Car/Car'
          })
        } })

提交代码

81. 集成任务信息

获取任务数据之前已经封装过,直接使用即可

  • 新建pages/UserTask/UserTask.ets
import { HmNavBar, HmLoading } from '@hm/basic'
import { UserTaskInfoModel, UserTaskInfo, UserTaskInfoParamsModel } from '../../models'
import { getUserTaskInfo } from '../../api'
@Entry
@Component
struct UserTask {
  @State TaskInfo: UserTaskInfoModel = new UserTaskInfoModel({} as UserTaskInfo)
  @State list: string[] = []
  @State queryParams: UserTaskInfoParamsModel = new UserTaskInfoParamsModel({
    year: new Date().getFullYear()+"",
    month: new Date().getMonth() + 1 +""
  })
  @State
  currentDate: Date = new Date()
  layer: CustomDialogController = new CustomDialogController({
    builder: HmLoading(),
    customStyle: true,
    alignment: DialogAlignment.Center
  })
  async aboutToAppear() {
    this.getUserTask()
  }
  async getUserTask () {
    this.layer.open()
    this.TaskInfo = await getUserTaskInfo(this.queryParams)
    this.layer.close()
  }
  build() {
    Column() {
      HmNavBar({ title: '任务数据' })

      // 本月任务
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceAround }) {
        Text("- 本月任务 -").fontSize(14).fontColor($r('app.color.text_secondary')).lineHeight(20)
        Row() {
          Column() {
            Text(this.TaskInfo.taskAmounts+ "").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)

          Column() {
            Text(this.TaskInfo.completedAmounts+"")
              .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)

          Column() {
            Text(this.TaskInfo.transportMileage+"")
              .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)
        }.justifyContent(FlexAlign.SpaceEvenly).width('100%').flexGrow(1)

      }
      .backgroundColor($r('app.color.white'))
      .margin({ left: 14.5, right: 14.5 })
      .height(100).margin({
        top: 20,
        bottom: 20
      })
      Row() {
        Button("切换月份")
          .backgroundColor($r("app.color.primary"))
          .onClick(() => {
            CalendarPickerDialog.show({
              selected: this.currentDate,
              onDateAccept: (value) => {
                this.queryParams.year = value.getFullYear().toString()
                this.queryParams.month = (value.getMonth() + 1).toString()
                this.getUserTask()
              }
            })
          })
      }
      .justifyContent(FlexAlign.Center)
      .width('100%')

    }
    .height('100%').backgroundColor($r('app.color.background_page'))
  }
}
  • 在我的页面跳转过去
 HmCardItem({ leftText: '任务设置', rightText: '',
         onRightClick: () => {
           router.pushUrl({
             url: 'pages/UserTask/UserTask'
           })
         }
  })

提交代码

82. 做一个截图效果

  • 实现一个转发按钮
 Row() {
              Image($r("app.media.share"))
                .width(20)
                .height(20)
                .fillColor($r("app.color.primary"))
                .onClick(async () => {
                  this.snapImg = await componentSnapshot.get("detail")
                  this.showSnap = true
                })
            }
            .width("100%")
            .justifyContent(FlexAlign.End)
            .padding(10)

  • 定义变量
 @State
  snapImg: image.PixelMap | null = null
  @State
  showSnap: boolean = false
  • 使用bindContentCover
 .bindContentCover($$this.showSnap, this.getSnapContent(), {
      modalTransition: ModalTransition.NONE
    })
  • 实现弹出内容
@Builder
  getSnapContent () {
    Column() {
      Image(this.snapImg)
        .width("100%")
        .height("100%")
        .objectFit(ImageFit.Auto)
        .borderRadius(6)
    }
    .padding("10%")
    .width("100%")
    .height("100%")
    .backgroundColor("rgba(0,0,0,0.2)")
    .onClick(() => {
      this.showSnap = false
    })
  }

83. LazyForEach应用

LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

  • 接口描述
LazyForEach(
    dataSource: IDataSource,             // 需要进行数据迭代的数据源
    itemGenerator: (item: any, index: number) => void,  // 子组件生成函数
    keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void

本质上-LazyForEach和ForEach的用法基本一致,但是ForEach属于全量渲染,而LazyForEach属于只渲染可见区域

  • 限制
  • LazyForEach必须在容器组件内使用,仅有ListGridSwiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
  • LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
  • 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
  • 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
  • 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
  • LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新
  • 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
  • 具体案例

📎UI框架-List组件的使用之商品列表(ArkTS).zip

92.吸顶效果
●List吸顶

TypeScript复制代码

// xxx.ets
@Entry
@Component
struct ListItemGroupExample {
  private timeTable: TimeTable[] = [
    {
      title: '星期一',
      projects: ['语文', '数学', '英语']
    },
    {
      title: '星期二',
      projects: ['物理', '化学', '生物']
    },
    {
      title: '星期三',
      projects: ['历史', '地理', '政治']
    },
    {
      title: '星期四',
      projects: ['美术', '音乐', '体育']
    }
  ]

  @Builder
  itemHead(text: string) {
    Text(text)
      .fontSize(20)
      .backgroundColor(0xAABBCC)
      .width("100%")
      .padding(10)
  }

  @Builder
  itemFoot(num: number) {
    Text('共' + num + "节课")
      .fontSize(16)
      .backgroundColor(0xAABBCC)
      .width("100%")
      .padding(5)
  }

  build() {
    Column() {
      List({ space: 20 }) {
        ForEach(this.timeTable, (item: TimeTable) => {
          ListItemGroup({ header: this.itemHead(item.title), footer: this.itemFoot(item.projects.length) }) {
            ForEach(item.projects, (project: string) => {
              ListItem() {
                Text(project)
                  .width("100%")
                  .height(100)
                  .fontSize(20)
                  .textAlign(TextAlign.Center)
                  .backgroundColor(0xFFFFFF)
              }
            }, (item: string) => item)
          }
          .divider({ strokeWidth: 1, color: Color.Blue }) // 每行之间的分界线
        })
      }
      .width('90%')
      .sticky(StickyStyle.Header | StickyStyle.Footer)
      .scrollBar(BarState.Off)
    }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
  }
}

interface TimeTable {
  title: string;
  projects: string[];
}

完结 

                                                                         

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值