个人开发笔记,大佬留情
47. 交货列表加载
分析:
一个任务,如果已经提货,那么下一步应该就是司机去送货,完成交货,上一节我们完成了提货, 那么完成的这个任务应该在在途里面,所以我来先把在途的数据进行查询一下
- 在途和待提货的区别就是查询的状态不同,刚刚好,我们前面定义了枚举,所以这里可以直接复用前面提货的TaskList的组件
entry模块
接下来,根据状态去调整按钮的显示和文本内容
- 当任务的状态为待提货并且可提货时,按钮显示提货并且可用
- 当任务的状态为待交货时,按钮显示提货并且可用
- 当任务的状态为待回车登记时,按钮显示回车登记并且可用
- 给之前的任务状态枚举再加一个属性
- 在TaskItemCard组件中根据状态获取按钮的文本及可用状态
如果enablePickup为true 表示可提货
如果status 为 2 表示为待交货 显示交货
如果status 为 4表示 待回车登记
- 按钮显示文本
⚠️: 这里的去提货的逻辑不用发生任何变化,因为交货的业务也是到详情页去提货,完成可以复用一摸一样的逻辑
效果:
48. 交货详情内容控制
根据状态显示不同的按钮
当一个任务提完货后,status应该是Line(在途),如果是Waiting(待提货)的话显示提货按钮,如果是Line(在途),显示交货按钮
1提货
2交货
- 交货时,隐藏提货上传组件,显示交货上传组件
- 实现一个Builder函数来放置交货的上传
显示组件
49. 交货按钮状态控制
控制按钮enable
50. 交货
Entry模块
标准流程
- 定义类型封装api
- 引入api
- 注册事件-调用api重新加载
- 定义请求参数类型
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
}
}
- 封装API
- 调用
51. 底部显示回车登记按钮
在已经交货的情况下,显示提货信息-交货信息- 底部的回车登记按钮,全部显示
52. 回车登记模式下-上传组件只读
因为货已经交了,所以回车登记模式下,此时只可以看,不能再传图片了
basic模块
- 给HmUpload一个属性来控制是否可上传和删除
- 通过该属性来控制上传部分的显示和隐藏
- 在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. 回车登记提交
- 定义提交数据类型
- 绑定数据到数据
- 提交回车登记
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模块
- 新建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组件-上报异常定位当前位置
需要在点击上报位置时,跳转到选择位置页面
- 点击上报位置跳转到选择位置
- 新建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
}
})
})
- 在上报异常中获取数据
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
}
}
- 显示位置到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. 上报异常提交
- 封装api
// 上报异常
export const exceptionReportAPI = (data: ExceptionParamsTypeModel) => {
return Request.post("/driver/tasks/reportException", data)
}
- 按钮点击提交-返回上一个页面
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模块
- 新建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
- 点击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. 打电话功能
- 导入使用包
import call from '@ohos.telephony.call'
- 给之前的builder类型添加一个图标点击事件
interface BaseBuilderClass {
title: string
value: string
icon?: ResourceStr
iconClick?: () => void
}
- 点击图标打电话
@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操作-文件压缩-解压缩-裁剪-位移
- 主线程-子线程处理耗时任务(诸多限制)
- 建立多线程 子线程
- 将主线程拿到的图片给到子线程
- 子线程进行图片的压缩
- 压缩完成 将完成的图片信息回传给主线程
- 子线程自动关闭销毁
- 主线程继续上传操作
- 新建一个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模块
- 新建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必须在容器组件内使用,仅有List、Grid、Swiper以及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[];
}
完结