- 接口文档
【apifox】面试宝典 个人中心-保存用户数据信息 - 教学练测项目-面试宝典-鸿蒙
登录固定账号和密码:
账号:hmheima
密码:Hmheima%123
UI设计稿
【腾讯 CoDesign】面试宝典 CoDesign - 腾讯自研设计协作平台 访问密码:2YDD
【腾讯 CoDesign】面试宝典 https://codesign.qq.com/app/s/449325994072442 ON9V
✔️我的页面-沉浸式显示模式处理
课程目标
- 我的页面要突破
手机安全区域
来全屏显示(沉浸式模式显示
),其他页面不需要 - 在Mine组件的aboutToAppear中使用
window
模块的getLastWindow
和setWindowLayoutFullScreen
两个方法来处理全屏显示
设置沉浸式模式的特点:
- 在任何一个页面中设置过一次之后,其他页面也会跟着全屏显示
这么处理会出现问题:从其他tab标签进入我的tab标签时,会出现底部tab栏闪动
-
- 解决:在进入应用时(index.tes页面)就开启所有页面的全屏模式
- 带来新的问题:不需要全屏显示的页面内容会突破安全区域显示,导致内容显示不正常
import { window } from '@kit.ArkUI'@Component
export struct Mine {aboutToAppear(): void {// 1.0 利用了系统的window 这个api的getLastWindow方法获取到了当前的窗口window.getLastWindow(getContext()).then(win=>{// 2.0 利用当前窗口的setWindowLayoutFullScreen方法设置全屏: true:设置全屏 false:取消全屏win.setWindowLayoutFullScreen(true) // 开启了当前页面的沉浸式模式(开启全屏模式)})}build() {Column(){Text('我的' + Math.random().toFixed(2))}.height('100%').width('100%').backgroundColor(Color.Pink)}
}
✔️index.ets中统一开启整个应用的沉浸式模式
课程目标
- 在index.ets页面的aboutToAppear中使用
window
模块的getLastWindow
和setWindowLayoutFullScreen
两个方法来处理整个应用的全屏显示
由于整个应用都开启了全屏显示,那些不需要全屏显示的页面也一并开启了,导致出现了问题:
问题:内容突破了整个安全区域,靠手机顶部显示了内容
解决:需要通过获取手机的安全高度来结合 padding来适配顶部内容的正常显示
import hilog from '@ohos.hilog'
import { Logger } from '../common/utils/Logger'
import { Home } from '../views/home/home'
import { InterView } from '../views/interview/interview'
import { Mine } from '../views/mine/mine'
import { Project } from '../views/project/project'
import { window } from '@kit.ArkUI'@Entry
@Component
struct Index {@State currentIndex:number = 0 // 表示默认选中的是第一个tab栏aboutToAppear(): void {// 1.0 利用了系统的window 这个api的getLastWindow方法获取到了当前的窗口window.getLastWindow(getContext()).then(win=>{// 2.0 利用当前窗口的setWindowLayoutFullScreen方法设置全屏: true:设置全屏 false:取消全屏win.setWindowLayoutFullScreen(true) // 开启了当前页面的沉浸式模式(开启全屏模式)})}// 自定义tab的标签栏@Builder tabBuilder(text:string,icon:ResourceStr,selectedIcon:ResourceStr,index:number){Column({space:5}){Image(this.currentIndex === index?selectedIcon:icon ).height(28).aspectRatio(1)Text(text)}}build() {Row() {Column() {// 使用Tab组件完成导航栏的使用// $$this.currentIndex 双向数据绑定,自定更新currentIndex的值,功能等同于onChange事件Tabs({index:$$this.currentIndex}){TabContent(){// Text('首页')if(this.currentIndex === 0){Home()}}.tabBar(this.tabBuilder('首页',$r('app.media.home'),$r('app.media.home_select'),0))TabContent(){// Text('项目')if(this.currentIndex === 1) {Project()}}.tabBar(this.tabBuilder('项目',$r('app.media.tabbar_project'),$r('app.media.ic_tabbar_project_select'),1))TabContent(){// Text('面经')if(this.currentIndex === 2) {InterView()}}.tabBar(this.tabBuilder('面经',$r('app.media.interview'),$r('app.media.interview_select'),2))TabContent(){// Textif(this.currentIndex === 3) {Mine()}}.tabBar(this.tabBuilder('我的',$r('app.media.tabbar_mine'),$r('app.media.ic_tabbar_mine_select'),3))}.animationDuration(0) // 去掉切换动画.barPosition(BarPosition.End) // tabs在底部显示}.width('100%')}.height('100%')}
}
✔️Index.ets中获取安全高度保存到AppStorage中
课程目标
- 通过
window
模块的getLastWindow
和getWindowAvoidArea
来获取安全高度
window.AvoidAreaType.TYPE_SYSTEM 获取导航栏安全高度,但是获取不到底部指示器的高度
window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR -> 获取底部指示器的高度
- 安全高度获取到的是px单位,需要使用px2vp函数转换成vp单位
- 将安全区域高度保存到AppStorage中共享给其他页面使用
import { Home } from '../views/home/Home'
import { InterviewList } from '../views/interview/InterviewList'
import { my } from '../views/my/my'
import { ProjectList } from '../views/project/ProjectList'
import { window } from '@kit.ArkUI'@Entry
@Component
struct Index {@State currentIndex: number = 0@State bottomHeight:number = 0async aboutToAppear() {const win = await window.getLastWindow(getContext())// 1. 设置沉浸式模式(开启全屏)win.setWindowLayoutFullScreen(true)// 2. 获取安全区域的高度const area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)let topHeight = px2vp(area.topRect.height) //获取到了顶部安全区域的高度,但是单位是px,为了显示正常要转成vp// area.bottomRect.height -> 0// 使用AppStroage存储起来,将来所有页面都可以使用AppStorage.setOrCreate('topHeight', topHeight)// 3. 获取底部的导航条的高度(Home键)const areaIndictor = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)let bottomHeight = px2vp(areaIndictor.bottomRect.height)this.bottomHeight = bottomHeightAppStorage.setOrCreate('bottomHeight', bottomHeight)// 设置当前页面中的容器padding底部预留出bottomHeight// AlertDialog.show({ message: area.bottomRect.height + '----' + areaIndictor.bottomRect.height })}build() {Column() {// 用Tabs来完成首页底部导航栏的实现Tabs({ index: $$this.currentIndex }) {TabContent() {if (this.currentIndex == 0) {Home()}}.tabBar(this.tabbarBuilder('首页', $r('app.media.home'), $r('app.media.home_select'), 0))TabContent() {if (this.currentIndex == 1) {ProjectList()}}.tabBar(this.tabbarBuilder('项目', $r('app.media.tabbar_project'), $r('app.media.ic_tabbar_project_select'), 1))TabContent() {if (this.currentIndex == 2) {InterviewList()}}.tabBar(this.tabbarBuilder('面经', $r('app.media.interview'), $r('app.media.interview_select'), 2))TabContent() {if (this.currentIndex == 3) {my()}}.tabBar(this.tabbarBuilder('我的', $r('app.media.tabbar_mine'), $r('app.media.ic_tabbar_mine_select'), 3))}.barPosition(BarPosition.End).animationDuration(0)}.height('100%').width('100%').padding({bottom:this.bottomHeight})}// 1. 实现底部导航栏的自定义构建函数@BuildertabbarBuilder(text: string, icon: ResourceStr, selectedIcon: ResourceStr, index: number) {Column({ space: 5 }) {Image(this.currentIndex == index ? selectedIcon : icon).height(30)Text(text).fontColor(this.currentIndex == index ?Color.Orange :Color.Black)}}
}
@Component
export struct Home {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight:number = 0build() {Column(){Text('首页' + Math.random().toFixed(2))}.padding({top:this.topHeight}).height('100%').width('100%')}}
✔️首页、项目、面经、我的页面安全区域高度适配
课程目标
- 从AppStorage中获取安全区域高度,结合padding适配页面内容的顶部显示
@Component
export struct Home {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight:number = 0build() {Column(){Text('首页' + Math.random().toFixed(2))}.padding({top:this.topHeight}).height('100%').width('100%')}}
@Component
export struct InterView {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight:number = 0build() {Column(){Text('面经' + Math.random().toFixed(2))}.padding({top:this.topHeight}).height('100%').width('100%')}
}
@Component
export struct Project {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight:number = 0build() {Column(){Text('项目' + Math.random().toFixed(2))}.padding({top:this.topHeight}).height('100%').width('100%').backgroundColor(Color.Pink)}
}
@Component
export struct Mine {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight:number = 0build() {Column(){Text('我的' + Math.random().toFixed(2))}.padding({top:this.topHeight}).height('100%').width('100%').backgroundColor(Color.Pink)}
}
✔️设置安全区域文字颜色
课程目标
- 通过
window
模块的getLastWindow
和setWindowSystemBarProperties({statusBarContentColor:颜色})
来设置安全区域文字颜色
-
- #FFFFFF -> 设置白色
- #000000 -> 设置黑色
import { window } from '@kit.ArkUI'@Component
export struct Mine {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight:number = 0// 进入页面触发aboutToAppear(): void {window.getLastWindow(getContext()).then(win=>{win.setWindowSystemBarProperties({statusBarContentColor:'#FFFFFF'}) // 设置安全区域内容的颜色为白色})}build() {Column(){Text('我的' + Math.random().toFixed(2))}.padding({top:this.topHeight}).height('100%').width('100%').backgroundColor(Color.Pink)}
}
一旦执行了颜色设置代码,所有页面都会是同一个颜色,如果需要改颜色,需要在指定页面重新设置一次新颜色
练习发现的问题:
- 颜色代码 一定要设置成大写的 #FFFFFF #fff #ffffff有些模拟器不生效
✔️沉浸式模式类封装
课程目标
- 封装安全区域工具类windowManager.ets,用来设置沉浸式模式以及设置安全区域的字体颜色
-
- enableFullScreen - 开启全屏
- disableFullScreen - 关闭全屏
- getAvoidAreaTop获取顶部安全区域高度,并返回该值
- getAvoidAreaBottom获取底部导航条安全区域高度,并返回该值
- settingStatusBarContentColor -设置安全区域文字为白色或者黑色
- 在index.ets中调用windowManager.ets完成沉浸模式的开启
- 利用AppStorage对安全区域高度做不同页面的兼容性处理
const win = await window.getLastWindow(getContext())// 1. 设置沉浸式模式(开启全屏)win.setWindowLayoutFullScreen(true)
async aboutToAppear() {const win = await window.getLastWindow(getContext())win.setWindowSystemBarProperties({statusBarContentColor:'#ffffff'})}
// 2. 获取安全区域的高度
const area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
let topHeight = px2vp(area.topRect.height) //获取到了顶部安全区域的高度,但是单位是px,为了显示正常要转成vp
// area.bottomRect.height -> 0
// 使用AppStroage存储起来,将来所有页面都可以使用
AppStorage.setOrCreate('topHeight', topHeight)// 3. 获取底部的导航条的高度(Home键)
const areaIndictor = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
let bottomHeight = px2vp(areaIndictor.bottomRect.height)
this.bottomHeight = bottomHeight
AppStorage.setOrCreate('bottomHeight', bottomHeight)
最终封装完整代码
import { window } from '@kit.ArkUI'export class windowManager {// 1. 开启全屏static async enableFullScreen() {const win = await window.getLastWindow(getContext())win.setWindowLayoutFullScreen(true)}// 2. 关闭全屏static async disableFullScreen() {const win = await window.getLastWindow(getContext())win.setWindowLayoutFullScreen(false)}// 3. 获取顶部安全区域高度static async getAvoidAreeTop() {const win = await window.getLastWindow(getContext())const area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)// 在Appstroage中保存起来const topHeight = px2vp(area.topRect.height)AppStorage.setOrCreate('topHeight', topHeight)return topHeight}// 4. 获取底部导航条高度static async getAvoidAreeBottom() {const win = await window.getLastWindow(getContext())const area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)// 在Appstroage中保存起来const bottomHeight = px2vp(area.bottomRect.height)AppStorage.setOrCreate('bottomHeight', bottomHeight)return bottomHeight}// 5. 设置安全区域文字的颜色static async settingStatusBarContentColor(color: '#FFFFFF' | '#000000') {const win = await window.getLastWindow(getContext())win.setWindowSystemBarProperties({ statusBarContentColor: color })}
}
使用封装好的类方法:
import { Home } from '../views/home/Home'
import { InterviewList } from '../views/interview/InterviewList'
import { my } from '../views/my/my'
import { ProjectList } from '../views/project/ProjectList'
import { window } from '@kit.ArkUI'
import { windowManager } from '../utils/windowManager'@Entry
@Component
struct Index {@State currentIndex: number = 0@State bottomHeight: number = 0async aboutToAppear() {// 1. 开启全屏windowManager.enableFullScreen()// 2. 获取顶部安全区域高度windowManager.getAvoidAreeTop()// 2. 获取底部的导航栏的高度,赋值给this.bottomHeightthis.bottomHeight = await windowManager.getAvoidAreeBottom()}build() {Column() {// 用Tabs来完成首页底部导航栏的实现Tabs({ index: $$this.currentIndex }) {TabContent() {if (this.currentIndex == 0) {Home()}}.tabBar(this.tabbarBuilder('首页', $r('app.media.home'), $r('app.media.home_select'), 0))TabContent() {if (this.currentIndex == 1) {ProjectList()}}.tabBar(this.tabbarBuilder('项目', $r('app.media.tabbar_project'), $r('app.media.ic_tabbar_project_select'), 1))TabContent() {if (this.currentIndex == 2) {InterviewList()}}.tabBar(this.tabbarBuilder('面经', $r('app.media.interview'), $r('app.media.interview_select'), 2))TabContent() {if (this.currentIndex == 3) {my()}}.tabBar(this.tabbarBuilder('我的', $r('app.media.tabbar_mine'), $r('app.media.ic_tabbar_mine_select'), 3))}.barPosition(BarPosition.End).animationDuration(0)}.height('100%').width('100%').padding({ bottom: this.bottomHeight })}// 1. 实现底部导航栏的自定义构建函数@BuildertabbarBuilder(text: string, icon: ResourceStr, selectedIcon: ResourceStr, index: number) {Column({ space: 5 }) {Image(this.currentIndex == index ? selectedIcon : icon).height(30)Text(text).fontColor(this.currentIndex == index ?Color.Orange :Color.Black)}}
}
import { window } from '@kit.ArkUI'
import { windowManager } from '../../utils/windowManager'@Component
export struct my {@StorageProp('topHeight') topHeight: number = 0async aboutToAppear() {windowManager.settingStatusBarContentColor('#FFFFFF')}build() {Column() {Text('我的组件')}.height('100%').width('100%').backgroundColor(Color.Pink).padding({ top: this.topHeight })}
}
- 接口文档
【apifox】面试宝典 个人中心-保存用户数据信息 - 教学练测项目-面试宝典-鸿蒙
登录固定账号和密码:
账号:hmheima
密码:Hmheima%123
✔️腾讯 CoDesignUI设计稿查看
课程目标
- 鸿蒙开发:因为腾讯CoDesignUI设计稿中还没有鸿蒙平台,所以暂时 选择Android平台,设定375宽 来适配鸿蒙界面的开发
- 使用鼠标右键,可以通过弹层选中需要查看的元素
- 设计稿
【腾讯 CoDesign】面试宝典 CoDesign - 腾讯自研设计协作平台 访问密码:2YDD
✔️【登录】静态结构搭建
pages/LoginPage.ets
课程目标
- 学会【腾讯 CoDesign】UI设计稿查看方式(选择android,自定义倍率0.5 -> 375宽)
- 结合ArkUI组件完成静态结构的搭建
import { router } from '@kit.ArkUI'@Entry
@Component
struct LoginPage {@State username:string = 'hmheima'@State password:string = 'Hmheima%123'@State isAgree:boolean = false@State islogin:boolean = falsebuild() {Column() {// logoColumn({ space: 10 }) {Image($r('app.media.icon')).height(55).aspectRatio(1)Text('面试宝典').fontSize(28).fontColor('#121826')Text('搞定企业面试真题,就用面试宝典').fontSize(14).fontColor('#6F6F6F')}.margin({ top: 170 })// 登录区域Column({space:20}) {TextInput({text:$$this.username}).backgroundColor(Color.White).width('90%').borderRadius(0)TextInput({text:$$this.password}).type(InputType.Password).backgroundColor(Color.White).width('90%')Row(){Checkbox().select(this.isAgree).selectedColor('#FA6D1D').onChange(value=>{this.isAgree = value})Text('已阅读并同意').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('用户协议').fontSize(14).padding({ right: 4})Text('和').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('隐私政策').fontSize(14).onClick(() => {router.pushUrl({url: 'pages/PreviewWebPage'})})}.width('90%')Button({type:ButtonType.Normal}){Row(){if(this.islogin){LoadingProgress().height(28).aspectRatio(1).color(Color.White)}Text('登录')}}.borderRadius(4).width(328).height(45).fontColor(Color.White).linearGradient({angle:135,colors:[['#FCA21C',0],['#FA6D1D',1]]})}.margin({top:50})}.width('100%').height('100%')}
}
✔️【登录】隐私政策页面加载
pages/PreviewWebPage.ets
课程目标
- 创建PreviewWebPage.ets
- router.pushUrl从LoginPage.ets跳转到PreviewWebPage.ets
- PreviewWebPage.ets中利用webView加载rawfile中的html文件
webViewController:webview.WebviewController = new webview.WebviewController()Web({src:$rawfile('index.html'),//src:"http://www.xxx.com/index.html",controller:this.webViewController})
import { webview } from '@kit.ArkWeb'@Entry
@Component
struct PreviewWebPage {// 1. 实例化webview控制器对象webController = new webview.WebviewController()build() {Navigation() {// Web组件只能在模拟器或者真机中使用,预览器不支持// 2. 使用Web组件加载本地rawfile中的index.htmlWeb({src: $rawfile('index.html'), //这种不需要开启网络权限// src:'http://itheima.com', //这种访问需要开网络权限controller: this.webController //3. 设置第1步实例化的控制器对象})}.title('隐私政策').titleMode(NavigationTitleMode.Mini)}
}
注意:
Web第二个参数controller不能省略,必选
src: 可以指向本地的一个文件,也可以指向网络的一个html文件,如果是网络文件,则需要在src/main/module.json5 配置INTERNET网络访问权限
预览器不支持加载网页功能,需要到模拟器或者真机中验证功能结果
【登录】登录逻辑处理
课程目标
- 理解接口文档中接口鉴权的描述方式,知道如何翻译成代码实现
- 完成利用http模块进行登录接口的post请求 -> 返回一个token -> 本地保存token (AppStroage来保存)
- 完成利用AppStorage对登录成功后的token进行存储
// 所有接口返回数据的类型
export interface iResponseModel<T> {/*** 请求成功10000标志*/code: number;/*** 返回数据*/data: T;/*** 请求成功*/message: string;/*** 请求成功标志*/success: boolean;
}/*** 登录成功之后的 返回数据*/
export interface iLoginUserModel {/*** 用户头像地址*/avatar?: string;/*** 连续打卡天数*/clockinNumbers: number;/*** 用户id*/id: string;/*** 昵称*/nickName: string;/*** token过期后,刷新token使用*/refreshToken: string;/*** 分享加密串*/shareInfo: string;/*** 后续交互使用的token*/token: string;/*** 学习时长,单位s*/totalTime: number;/*** 昵称*/username?: string;
}
✔️【登录】- 参数合法性检查
课程目标
- 能对用户名和密码文本框做非空验证处理
- 对用户协议勾选做验证处理
import { promptAction, router } from '@kit.ArkUI'@Entry
@Component
struct LoginPage {@State username:string = 'hmheima'@State password:string = 'Hmheima%123'@State isAgree:boolean = falseasync login(){// 1.0 页面合法性检查// 1.0.1 进行用户名和密码的非空判断if(this.username === '' || this.password === ''){promptAction.showToast({message:'用户名和密码不能为空'})return}// 1.0.2 判断用户如果没有勾选同意协议,则验证失败if(!this.isAgree){promptAction.showToast({message:'请勾选协议'})this.isAgree = true // 设置勾选上协议return}}build() {Column() {// logoColumn({ space: 10 }) {Image($r('app.media.icon')).height(55).aspectRatio(1)Text('面试宝典').fontSize(28).fontColor('#121826')Text('搞定企业面试真题,就用面试宝典').fontSize(14).fontColor('#6F6F6F')}.margin({ top: 170 })// 登录区域Column({space:20}) {TextInput({text:$$this.username}).backgroundColor(Color.White).width('90%').borderRadius(0)TextInput({text:$$this.password}).type(InputType.Password).backgroundColor(Color.White).width('90%')Row(){Checkbox().select(this.isAgree).selectedColor('#FA6D1D').onChange(value=>{this.isAgree = value})Text('已阅读并同意').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('用户协议').fontSize(14).padding({ right: 4})Text('和').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('隐私政策').fontSize(14).onClick(() => {router.pushUrl({// 跳转到PreviewWebPageurl: 'pages/PreviewWebPage'})})}.width('90%')Button(){Row(){LoadingProgress().height(28).aspectRatio(1).color(Color.White)Text('登录')} }.onClick(()=>{// 调用登录方法this.login()}).borderRadius(4).width(328).height(45).fontColor(Color.White).linearGradient({angle:135,colors:[['#FCA21C',0],['#FA6D1D',1]]})}.margin({top:50})}.width('100%').height('100%')}
}
✔️【登录】- 使用axios模块请求登录接口
http请求需要先配置INTERNET权限,方法:
src/main/module.json5中增加:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
课程目标
- 能使用axios模块的reqeust方法以post方式请求登录接口,并做好异常处理
-
- 安装axios模块 ohpm i @ohos/axios
定义数据类型
// 所有接口返回数据的类型
export interface iResponseModel<T> {/*** 请求成功10000标志*/code: number;/*** 返回数据*/data: T;/*** 请求成功*/message: string;/*** 请求成功标志*/success: boolean;
}/*** 登录成功之后的 返回数据*/
export interface iLoginUserModel {/*** 用户头像地址*/avatar?: string;/*** 连续打卡天数*/clockinNumbers: number;/*** 用户id*/id: string;/*** 昵称*/nickName: string;/*** token过期后,刷新token使用*/refreshToken: string;/*** 分享加密串*/shareInfo: string;/*** 后续交互使用的token*/token: string;/*** 学习时长,单位s*/totalTime: number;/*** 昵称*/username?: string;
}
import { promptAction, router } from '@kit.ArkUI'
import axios, { AxiosResponse, AxiosError } from '@ohos/axios'
import { iLoginUserModel, iResponseModel } from '../models/datamodel'@Entry
@Component
struct LoginPage {@State username: string = 'hmheima'@State password: string = 'Hmheima%123'@State isAgree: boolean = false@State islogin: boolean = false// 负责登录async login() {// 1. 参数合法性检查/** ● 能对用户名和密码文本框做非空验证处理● 对用户协议勾选做验证处理* */if (this.username == '' || this.password == '') {return promptAction.showToast({ message: '用户名和密码必填' })}if (!this.isAgree) {promptAction.showToast({ message: '请先勾选协议' })this.isAgree = truereturn}// 2. axios请求服务器接口try {const req = axios.create()let res: AxiosResponse<iResponseModel<iLoginUserModel>> =await req.request<null, AxiosResponse<iResponseModel<iLoginUserModel>>, object>({method: 'POST',url: 'https://api-harmony-teach.itheima.net/hm/login',data: {username: this.username,password: this.password}})AlertDialog.show({ message: JSON.stringify(res.data.data, null, 2) })// 3. 将服务器响应回来的数据保存到AppStroage中// 3.1 判断服务器响应体中的code==10000的时候才保存数据,否则提示用户响应体中的message// 4. 跳转到首页} catch (err) {// 将来服务器的状态码是非200的,就会自动触发catchlet errObj: AxiosError = err //最终将err大错误对象,转为AxiosErrorAlertDialog.show({ message: errObj.message }) //最后提示给用户的是message字符串}}build() {Column() {// logoColumn({ space: 10 }) {Image($r('app.media.icon')).height(55).aspectRatio(1)Text('面试宝典').fontSize(28).fontColor('#121826')Text('搞定企业面试真题,就用面试宝典').fontSize(14).fontColor('#6F6F6F')}.margin({ top: 170 })// 登录区域Column({ space: 20 }) {TextInput({ text: $$this.username }).backgroundColor(Color.White).width('90%').borderRadius(0)TextInput({ text: $$this.password }).type(InputType.Password).backgroundColor(Color.White).width('90%')Row() {Checkbox().select(this.isAgree).selectedColor('#FA6D1D').onChange(value => {this.isAgree = value})Text('已阅读并同意').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('用户协议').fontSize(14).padding({ right: 4 })Text('和').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('隐私政策').fontSize(14).onClick(() => {router.pushUrl({url: 'pages/PreviewWebPage'})})}.width('90%')Button({ type: ButtonType.Normal }) {Row() {if (this.islogin) {LoadingProgress().height(28).aspectRatio(1).color(Color.White)}Text('登录')}}.borderRadius(4).width(328).height(45).fontColor(Color.White).linearGradient({angle: 135,colors: [['#FCA21C', 0],['#FA6D1D', 1]]}).onClick(() => {this.login()})}.margin({ top: 50 })}.width('100%').height('100%')}
}
✔️【登录】- 处理登录接口响应数据
课程目标
- 处理响应体中逻辑状态码10000的情况 -> 响应数据中如果code的值为10000则表示成功响应
- 处理响应体中逻辑状态码不为10000 -> 响应数据中如果code的值不为10000则表示失败响应
import { promptAction, router } from '@kit.ArkUI'
import axios, { AxiosResponse, AxiosError } from '@ohos/axios'
import { iLoginUserModel, iResponseModel } from '../models/datamodel'@Entry
@Component
struct LoginPage {@State username: string = 'hmheima'@State password: string = 'Hmheima%123'@State isAgree: boolean = false@State islogin: boolean = false// 负责登录async login() {// 1. 参数合法性检查/** ● 能对用户名和密码文本框做非空验证处理● 对用户协议勾选做验证处理* */if (this.username == '' || this.password == '') {return promptAction.showToast({ message: '用户名和密码必填' })}if (!this.isAgree) {promptAction.showToast({ message: '请先勾选协议' })this.isAgree = truereturn}// 2. axios请求服务器接口try {const req = axios.create()let res: AxiosResponse<iResponseModel<iLoginUserModel>> =await req.request<null, AxiosResponse<iResponseModel<iLoginUserModel>>, object>({method: 'POST',url: 'https://api-harmony-teach.itheima.net/hm/login',data: {username: this.username,password: this.password}})// AlertDialog.show({ message: JSON.stringify(res, null, 2) })// 3. 处理服务器响应回来的报文体的数据// 3.1 判断服务器响应体中的code==10000的时候才保存数据,否则提示用户响应体中的messageif (res.data.code != 10000) {return promptAction.showToast({ message: res.data.message })}// 3.2 将服务器响应回来的数据保存到AppStroage中AppStorage.setOrCreate('user', res.data.data)// 4. 跳转到首页router.replaceUrl({ url: 'pages/Index' })} catch (err) {// 将来服务器的状态码是非200的,就会自动触发catchlet errObj: AxiosError = err //最终将err大错误对象,转为AxiosErrorAlertDialog.show({ message: errObj.message }) //最后提示给用户的是message字符串}}build() {Column() {// logoColumn({ space: 10 }) {Image($r('app.media.icon')).height(55).aspectRatio(1)Text('面试宝典').fontSize(28).fontColor('#121826')Text('搞定企业面试真题,就用面试宝典').fontSize(14).fontColor('#6F6F6F')}.margin({ top: 170 })// 登录区域Column({ space: 20 }) {TextInput({ text: $$this.username }).backgroundColor(Color.White).width('90%').borderRadius(0)TextInput({ text: $$this.password }).type(InputType.Password).backgroundColor(Color.White).width('90%')Row() {Checkbox().select(this.isAgree).selectedColor('#FA6D1D').onChange(value => {this.isAgree = value})Text('已阅读并同意').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('用户协议').fontSize(14).padding({ right: 4 })Text('和').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('隐私政策').fontSize(14).onClick(() => {router.pushUrl({url: 'pages/PreviewWebPage'})})}.width('90%')Button({ type: ButtonType.Normal }) {Row() {if (this.islogin) {LoadingProgress().height(28).aspectRatio(1).color(Color.White)}Text('登录')}}.borderRadius(4).width(328).height(45).fontColor(Color.White).linearGradient({angle: 135,colors: [['#FCA21C', 0],['#FA6D1D', 1]]}).onClick(() => {this.login()})}.margin({ top: 50 })}.width('100%').height('100%')}
}
✔️【登录】保存token 并跳转到首页
课程目标
- 完成利用AppStorage保存登录成功后返回的token数据
- 利用router.pushUrl方法跳转到 pages/Index页面
// 将整个响应回来的data数据存储(选择的方案:AppStorage)AppStorage.setOrCreate('user', 登录后响应的数据)// 直接跳转到首页router.replaceUrl({ url: 'pages/Index' })
import { promptAction, router } from '@kit.ArkUI'
import axios, { AxiosResponse, AxiosError } from '@ohos/axios'
import { iLoginUserModel, iResponseModel } from '../models/datamodel'@Entry
@Component
struct LoginPage {@State username: string = 'hmheima'@State password: string = 'Hmheima%123'@State isAgree: boolean = false@State islogin: boolean = false// 负责登录async login() {// 1. 参数合法性检查/** ● 能对用户名和密码文本框做非空验证处理● 对用户协议勾选做验证处理* */if (this.username == '' || this.password == '') {return promptAction.showToast({ message: '用户名和密码必填' })}if (!this.isAgree) {promptAction.showToast({ message: '请先勾选协议' })this.isAgree = truereturn}// 2. axios请求服务器接口try {const req = axios.create()let res: AxiosResponse<iResponseModel<iLoginUserModel>> =await req.request<null, AxiosResponse<iResponseModel<iLoginUserModel>>, object>({method: 'POST',url: 'https://api-harmony-teach.itheima.net/hm/login',data: {username: this.username,password: this.password}})// AlertDialog.show({ message: JSON.stringify(res, null, 2) })// 3. 处理服务器响应回来的报文体的数据// 3.1 判断服务器响应体中的code==10000的时候才保存数据,否则提示用户响应体中的messageif (res.data.code != 10000) {return promptAction.showToast({ message: res.data.message })}// 3.2 将服务器响应回来的数据保存到AppStroage中AppStorage.setOrCreate('user', res.data.data)// 4. 跳转到首页router.replaceUrl({ url: 'pages/Index' })} catch (err) {// 将来服务器的状态码是非200的,就会自动触发catchlet errObj: AxiosError = err //最终将err大错误对象,转为AxiosErrorAlertDialog.show({ message: errObj.message }) //最后提示给用户的是message字符串}}build() {Column() {// logoColumn({ space: 10 }) {Image($r('app.media.icon')).height(55).aspectRatio(1)Text('面试宝典').fontSize(28).fontColor('#121826')Text('搞定企业面试真题,就用面试宝典').fontSize(14).fontColor('#6F6F6F')}.margin({ top: 170 })// 登录区域Column({ space: 20 }) {TextInput({ text: $$this.username }).backgroundColor(Color.White).width('90%').borderRadius(0)TextInput({ text: $$this.password }).type(InputType.Password).backgroundColor(Color.White).width('90%')Row() {Checkbox().select(this.isAgree).selectedColor('#FA6D1D').onChange(value => {this.isAgree = value})Text('已阅读并同意').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('用户协议').fontSize(14).padding({ right: 4 })Text('和').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('隐私政策').fontSize(14).onClick(() => {router.pushUrl({url: 'pages/PreviewWebPage'})})}.width('90%')Button({ type: ButtonType.Normal }) {Row() {if (this.islogin) {LoadingProgress().height(28).aspectRatio(1).color(Color.White)}Text('登录')}}.borderRadius(4).width(328).height(45).fontColor(Color.White).linearGradient({angle: 135,colors: [['#FCA21C', 0],['#FA6D1D', 1]]}).onClick(() => {this.login()})}.margin({ top: 50 })}.width('100%').height('100%')}
}
import { promptAction, router } from '@kit.ArkUI'
import axios, { AxiosResponse } from '@ohos/axios'
import { iLoginUserModel, iResponseModel } from '../models/datamodel'@Entry
@Component
struct LoginPage {@State username: string = 'hmheima'@State password: string = 'Hmheima%123'@State isAgree: boolean = falseasync login() {const req = axios.create()let res: AxiosResponse<iResponseModel<iLoginUserModel>> =await req.request<null, AxiosResponse<iResponseModel<iLoginUserModel>>>({method: 'POST',url: 'https://api-harmony-teach.itheima.net/hm/login',data: {username: this.username,password: this.password}})// AlertDialog.show({ message: JSON.stringify(res.data, null, 2) })// 3.0 处理响应数据// 数据转换let resData = res.data// 3.0.1 处理状态码不为10000的情况->失败if(resData.code !== 10000){promptAction.showToast({message:resData.message})return}// 3.0.2 处理成功的状态 ,将登录接口响应回来的整个数据保存到AppStorage中AppStorage.setOrCreate('user',resData.data)// 3.0.3 跳转到首页router.replaceUrl({url:'pages/Index'})}build() {Column() {// logoColumn({ space: 10 }) {Image($r('app.media.icon')).height(55).aspectRatio(1)Text('面试宝典').fontSize(28).fontColor('#121826')Text('搞定企业面试真题,就用面试宝典').fontSize(14).fontColor('#6F6F6F')}.margin({ top: 170 })// 登录区域Column({ space: 20 }) {TextInput({ text: this.username }).backgroundColor(Color.White).width('90%').borderRadius(0)TextInput({ text: this.password }).type(InputType.Password).backgroundColor(Color.White).width('90%')Row() {Checkbox().select(this.isAgree).selectedColor('#FA6D1D').onChange(value => {this.isAgree = value})Text('已阅读并同意').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('用户协议').fontSize(14).padding({ right: 4 })Text('和').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('隐私政策').fontSize(14).onClick(() => {router.pushUrl({url: 'pages/PreviewWebPage'})})}.width('90%')Button({ type: ButtonType.Normal }) {Row() {LoadingProgress().height(28).aspectRatio(1).color(Color.White)Text('登录')}}.borderRadius(4).width(328).height(45).fontColor(Color.White).linearGradient({angle: 135,colors: [['#FCA21C', 0],['#FA6D1D', 1]]}).onClick(() => {this.login()})}.margin({ top: 50 })}.width('100%').height('100%')}
}
在Home.ets组件获取AppStorage中的用户数据 ->来测试appstroage是否保存数据成功
import { iLoginUserModel } from '../../models/AccountModel'@Component
export struct Home {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight:number = 0// 获取登录成功后的用户信息,里面包含有token@StorageProp('user') currentUser:iLoginUserModel = {} as iLoginUserModelaboutToAppear(): void {AlertDialog.show({message:JSON.stringify(this.currentUser,null,2)})}build() {Column(){Text('首页' + Math.random().toFixed(2))}.padding({top:this.topHeight}).height('100%').width('100%')}}
✔️【登录】- 其他处理
课程目标
- 处理登录 loadingProgress的显示和隐藏
import { promptAction, router } from '@kit.ArkUI'
import http from '@ohos.net.http'
import { Logger } from '../common/utils/Logger'
import { iResponseModel } from '../models/AccountModel'@Entry
@Component
struct LoginPage {@State username: string = 'hmheima'@State password: string = 'Hmheima%123'@State isAgree: boolean = false@State isloading:boolean = false // 控制登录中的图标显示的async login() {// 打开loading组件this.isloading = true// 1.0 页面合法性检查// 1.0.1 进行用户名和密码的非空判断if (this.username === '' || this.password === '') {promptAction.showToast({ message: '用户名和密码不能为空' })return}// 1.0.2 判断用户如果没有勾选同意协议,则验证失败if (!this.isAgree) {promptAction.showToast({ message: '请勾选协议' })this.isAgree = true // 设置勾选上协议return}try {// 2.0 请求登录接口获取服务器的响应 https://api-harmony-teach.itheima.net/hm/loginconst req = axios.create()let res: AxiosResponse<iResponseModel<iLoginDataModel>> =await req.request<null, AxiosResponse<iResponseModel<iLoginDataModel>>>({method: 'POST',url: 'https://api-harmony-teach.itheima.net/hm/login',data: {username: this.username,password: this.password}})// AlertDialog.show({ message: JSON.stringify(res.data, null, 2) })// 3.0 处理响应数据// 数据转换let resData = res.data// 3.0.1 处理状态码不为10000的情况->失败if (resData.code !== 10000) {promptAction.showToast({ message: resData.message })return}// 3.0.2 处理成功的状态 ,将登录接口响应回来的整个数据保存到AppStorage中AppStorage.setOrCreate('user', resData.data)// 3.0.3 跳转到首页router.replaceUrl({ url: 'pages/Index' })} catch (err) {this.isloading = false // 如果代码错误则关闭Logger.error('登录页面-登录逻辑错误:', JSON.stringify(err))}}build() {Column() {// logoColumn({ space: 10 }) {Image($r('app.media.icon')).height(55).aspectRatio(1)Text('面试宝典').fontSize(28).fontColor('#121826')Text('搞定企业面试真题,就用面试宝典').fontSize(14).fontColor('#6F6F6F')}.margin({ top: 170 })// 登录区域Column({ space: 20 }) {TextInput({ text: $$this.username }).backgroundColor(Color.White).width('90%').borderRadius(0)TextInput({ text: $$this.password }).type(InputType.Password).backgroundColor(Color.White).width('90%')Row() {Checkbox().select(this.isAgree).selectedColor('#FA6D1D').onChange(value => {this.isAgree = value})Text('已阅读并同意').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('用户协议').fontSize(14).padding({ right: 4 })Text('和').fontSize(14).fontColor($r('app.color.ih_gray_color')).padding({ right: 4 })Text('隐私政策').fontSize(14).onClick(() => {router.pushUrl({// 跳转到PreviewWebPageurl: 'pages/PreviewWebPage'})})}.width('90%')Button() {Row() {if (this.isloading) {LoadingProgress().height(28).aspectRatio(1).color(Color.White)}Text('登录')}}.onClick(() => {// 调用登录方法this.login()}).borderRadius(4).width(328).height(45).fontColor(Color.White).linearGradient({angle: 135,colors: [['#FCA21C', 0],['#FA6D1D', 1]]})}.margin({ top: 50 })}.width('100%').height('100%')}
}
✔️【我的】用户信息展示
课程目标
- 完成利用@StorageProp 或 @StorageLink展示登录用户信息
-
- 因为在我的页面是纯展示用户头像和昵称,所以此处选择@StorageProp来获取数据即可
import { router } from '@kit.ArkUI'interface Nav {icon: ResourceStrname: stringonClick?: () => voidother?: string
}interface Tool {icon: ResourceStrname: stringvalue?: stringonClick?: () => void
}@Component
export struct Mine {@State clockCount: number = 0@BuildernavBuilder(nav: Nav) {GridCol() {Column() {Image(nav.icon).width(30).aspectRatio(1).margin({ bottom: 10 })Text(nav.name).fontSize(14).fontColor($r('app.color.common_gray_03')).margin({ bottom: 4 })if (nav.other) {Row() {Text(nav.other).fontSize(12).fontColor($r('app.color.common_gray_01'))Image($r('sys.media.ohos_ic_public_arrow_right')).width(12).aspectRatio(1).fillColor($r('app.color.common_gray_01'))}}}.onClick(() => {nav.onClick && nav.onClick()})}}@BuildertoolsBuilder(tool: Tool) {Row() {Image(tool.icon).width(16).aspectRatio(1).margin({ right: 12 })Text(tool.name).fontSize(14)Blank()if (tool.value) {Text(tool.value).fontSize(12).fontColor($r('app.color.common_gray_01'))}Image($r('sys.media.ohos_ic_public_arrow_right')).width(16).aspectRatio(1).fillColor($r('app.color.common_gray_01'))}.height(50).width('100%').padding({ left: 16, right: 10 })}build() {Column({ space: 16 }) {Row({ space: 12 }) {Image($r('app.media.ic_mine_avatar')).width(55).aspectRatio(1).borderRadius(55)Column({ space: 4 }) {Text('昵称').fontSize(18).fontWeight(600).width('100%').margin({ bottom: 5 })Row() {Text('编辑个人信息').fontColor($r('app.color.ih_gray_color')).fontSize(11).margin({ right: 4 })Image($r('app.media.icon_edit')).width(10).height(10).fillColor($r('app.color.ih_gray_color'))}.onClick(() => {router.pushUrl({url: '修改信息页面'})}).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text('放打卡组件')// HdClockIn({ clockCount: this.clockCount })}.width('100%').height(100)GridRow({ columns: 4 }) {this.navBuilder({ icon: $r('app.media.ic_mine_history'), name: '历史记录' })this.navBuilder({ icon: $r('app.media.ic_mine_collect'), name: '我的收藏' })this.navBuilder({ icon: $r('app.media.ic_mine_like'), name: '我的点赞' })this.navBuilder({icon: $r('app.media.ic_mine_study'), name: '累计学时', other: '4 小时', onClick: () => {router.pushUrl({ url: '跳转到学习记录页面' })}})}.backgroundColor(Color.White).padding(16).borderRadius(8)Column() {this.toolsBuilder({ icon: $r('app.media.ic_mine_notes'), name: '前端常用词' })this.toolsBuilder({ icon: $r('app.media.ic_mine_ai'), name: '面通AI' })this.toolsBuilder({ icon: $r('app.media.ic_mine_invite'), name: '推荐分享' })this.toolsBuilder({ icon: $r('app.media.ic_mine_file'), name: '意见反馈' })this.toolsBuilder({ icon: $r('app.media.ic_mine_info'), name: '关于我们' })this.toolsBuilder({ icon: $r('app.media.ic_mine_setting'), name: '设置' })}.backgroundColor(Color.White).borderRadius(8)}.padding($r('app.float.common_gutter')).backgroundColor($r('app.color.common_gray_bg')).linearGradient({colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]}).width('100%').height('100%')}
}
import { router } from '@kit.ArkUI'
import { iLoginUserModel } from '../../models/datamodel'interface Nav {icon: ResourceStrname: stringonClick?: () => voidother?: string
}interface Tool {icon: ResourceStrname: stringvalue?: stringonClick?: () => void
}@Component
export struct my {@State clockCount: number = 0@StorageProp('topHeight') topHeight: number = 0@StorageLink('user') currentUser: iLoginUserModel = {} as iLoginUserModel@BuildernavBuilder(nav: Nav) {GridCol() {Column() {Image(nav.icon).width(30).aspectRatio(1).margin({ bottom: 10 })Text(nav.name).fontSize(14).fontColor($r('app.color.common_gray_03')).margin({ bottom: 4 })if (nav.other) {Row() {Text(nav.other).fontSize(12).fontColor($r('app.color.common_gray_01'))Image($r('sys.media.ohos_ic_public_arrow_right')).width(12).aspectRatio(1).fillColor($r('app.color.common_gray_01'))}}}.onClick(() => {nav.onClick && nav.onClick()})}}@BuildertoolsBuilder(tool: Tool) {Row() {Image(tool.icon).width(16).aspectRatio(1).margin({ right: 12 })Text(tool.name).fontSize(14)Blank()if (tool.value) {Text(tool.value).fontSize(12).fontColor($r('app.color.common_gray_01'))}Image($r('sys.media.ohos_ic_public_arrow_right')).width(16).aspectRatio(1).fillColor($r('app.color.common_gray_01'))}.height(50).width('100%').padding({ left: 16, right: 10 })}build() {Column({ space: 16 }) {Row({ space: 12 }) {Image(this.currentUser.avatar).width(55).aspectRatio(1).borderRadius(55)Column({ space: 4 }) {Text(this.currentUser.nickName).fontSize(18).fontWeight(600).width('100%').margin({ bottom: 5 })Row() {Text('编辑个人信息').fontColor($r('app.color.ih_gray_color')).fontSize(11).margin({ right: 4 })Image($r('app.media.icon_edit')).width(10).height(10).fillColor($r('app.color.ih_gray_color'))}.onClick(() => {router.pushUrl({url: '修改信息页面'})}).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text('放打卡组件')// HdClockIn({ clockCount: this.clockCount })}.width('100%').height(100)GridRow({ columns: 4 }) {this.navBuilder({ icon: $r('app.media.ic_mine_history'), name: '历史记录' })this.navBuilder({ icon: $r('app.media.ic_mine_collect'), name: '我的收藏' })this.navBuilder({ icon: $r('app.media.ic_mine_like'), name: '我的点赞' })this.navBuilder({icon: $r('app.media.ic_mine_study'),name: '累计学时',other: '4 小时',onClick: () => {router.pushUrl({ url: '跳转到学习记录页面' })}})}.backgroundColor(Color.White).padding(16).borderRadius(8)Column() {this.toolsBuilder({ icon: $r('app.media.ic_mine_notes'), name: '前端常用词' })this.toolsBuilder({ icon: $r('app.media.ic_mine_ai'), name: '面通AI' })this.toolsBuilder({ icon: $r('app.media.ic_mine_invite'), name: '推荐分享' })this.toolsBuilder({ icon: $r('app.media.ic_mine_file'), name: '意见反馈' })this.toolsBuilder({ icon: $r('app.media.ic_mine_info'), name: '关于我们' })this.toolsBuilder({ icon: $r('app.media.ic_mine_setting'), name: '设置' })}.backgroundColor(Color.White).borderRadius(8)}.padding({top: this.topHeight,left: 16,right: 16,bottom: 16}).backgroundColor($r('app.color.common_gray_bg')).linearGradient({colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]}).width('100%').height('100%')}
}
✔️【首页】静态布局结构分析
src/main/ets/views/home/Home.ets
课程目标
- 理解首页的整个组件构成:
-
- 搜索组件 - HdSearch.ets
- 打卡组件 - HdClockIn.ets
- 题目分类组件 - HomeCategoryComp.ets
- 题目列表组件 - QuestionListComp.ets
- 题目列表单个内容item组件 - QuestionItemComp.ets
- 难易程度组件-HdTag.ets
- 创建搜索组件HdSearch.ets,并迁移静态结构 -静态结构直接拷贝-功能后面实现
- 创建打卡组件HdClockIn.ets,并迁移静态结构- 静态结构直接拷贝 - 功能后面实现
import { router } from '@kit.ArkUI'@Component
export struct HdSearch {@StatereadonlyMode: boolean = trueph: string = ''bg: string = ''color: string = ''build() {Row() {Row({ space: 4 }) {Image($r("app.media.ic_common_search")).width($r('app.float.hd_search_icon_size')).aspectRatio(1).fillColor(this.color || $r('app.color.common_gray_02'))Text(this.ph || $r('app.string.hd_search_placeholder')).fontColor(this.color || $r('app.color.common_gray_02')).fontSize($r('app.float.common_font14'))}.layoutWeight(1).height($r('app.float.hd_search_height')).backgroundColor(this.bg || $r('app.color.common_gray_border')).borderRadius($r('app.float.hd_search_radius')).justifyContent(FlexAlign.Center)}.onClick(() => {router.pushUrl({ url: 'pages/SearchPage' })})}
}
@Component
export struct HdClockIn {@StateclockCount: number = 0build() {Stack({ alignContent: Alignment.End }) {Image(this.clockCount > 0 ? $r('app.media.clocked') : $r('app.media.unclock')).objectFit(ImageFit.Fill)if (this.clockCount > 0) {Column() {Text('已连续打卡').fontSize(8)Text() {Span(this.clockCount.toString()).fontWeight(600).fontSize(12)Span(' 天').fontSize(10)}}.width('50')} else {Text('打卡').width('50').textAlign(TextAlign.Center).fontSize((18)).fontWeight(500).fontColor('#333C4F').margin({ bottom: (4) })}}.width((74)).height((28))}
}
【首页】轮播图
没有接口对接,直接拷贝静态结构实现占位
import { HdClockIn } from '../../common/components/HdClockIn'
import { HdSearch } from '../../common/components/HdSearch'
import { iLoginUserModel } from '../../models/AccountModel'@Component
export struct Home {// 获取AppStraoage中的数据@StorageProp("topHeight") topHeight: number = 0// 获取登录成功后的用户信息,里面包含有token@StorageProp('user') currentUser: iLoginUserModel = {} as iLoginUserModelaboutToAppear(): void {// AlertDialog.show({message:JSON.stringify(this.currentUser,null,2)})}build() {Column() {Row() {// 1.0 搜索组件静态HdSearch().layoutWeight(3) // 占3/4份// 2.0 打卡组件HdClockIn().layoutWeight(1) //占1/4}// 3.0 轮播图Swiper(){Image($r('app.media.banner_qa')).objectFit(ImageFit.Fill).height(200)Image($r('app.media.banner_pj')).objectFit(ImageFit.Fill).height(200)Image($r('app.media.banner_ai')).objectFit(ImageFit.Fill).height(200)}.padding(10).autoPlay(true).indicator(DotIndicator.dot().selectedColor(Color.White).bottom(20))}.padding({ top: this.topHeight }).height('100%').width('100%')}
}
✔️【首页】打卡组件功能实现
src/main/ets/common/components/HdClockIn.ets
课程目标
- ✔️完成在HdClockIn.ets中get请求
clockinInfo
(接口文档)获取打卡数据并完成判断显示最终结果
/*** 返回数据*/
export interface iClock {/*** 连续签到天数*/clockinNumbers: number;/*** 签到信息*/clockins: Clockin[];/*** 当天是否签到*/flag: boolean;/*** 累计签到天数*/totalClockinNumber: number;
}export interface Clockin {/*** 签到时间*/createdAt: string;id: string;
}
import axios, { AxiosResponse, AxiosError } from '@ohos/axios'
import { iClock } from '../models/clockinmodel'
import { iLoginUserModel, iResponseModel } from '../models/datamodel'@Component
export struct HdClockIn {@State clockCount: number = 0 // >0显示已连续打卡xx天 <=0 显示打卡@StorageProp('user') currentUser: iLoginUserModel = {} as iLoginUserModelaboutToAppear(): void {// 请求服务器接口 https://api-harmony-teach.itheima.net/hm/clockinInfo 获取打卡天数this.getClockCount()}async getClockCount() {try {// 1. 导入axios相关包发请求const req = axios.create()let res: AxiosResponse<iResponseModel<iClock>> = await req.request({url: 'https://api-harmony-teach.itheima.net/hm/clockinInfo',// 根据接口文档携带token请求服务器数据,否则抛出401异常代码,拿不到正常数据headers: {'Authorization': `Bearer ${this.currentUser.token}`}})// 2. 请求url获得数据赋值this.clockCountthis.clockCount = res.data.data.clockinNumbers} catch (err) {let errObj: AxiosError = errAlertDialog.show({ message: errObj.message })}}build() {Stack({ alignContent: Alignment.End }) {Image(this.clockCount > 0 ? $r('app.media.clocked') : $r('app.media.unclock')).objectFit(ImageFit.Fill)if (this.clockCount > 0) {Column() {Text('已连续打卡').fontSize(8)Text() {Span(this.clockCount.toString()).fontWeight(600).fontSize(12)Span(' 天').fontSize(10)}}.width('50')} else {Text('打卡').width('50').textAlign(TextAlign.Center).fontSize((18)).fontWeight(500).fontColor('#333C4F').margin({ bottom: (4) })}}.width((74)).height((28))}
}
- ✔️HdClockIn.ets 内部点击自己容器时,post请求接口
clockin
(接口文档) 完成打卡功能
import axios, { AxiosResponse, AxiosError } from '@ohos/axios'
import { iClock } from '../models/clockinmodel'
import { iLoginUserModel, iResponseModel } from '../models/datamodel'@Component
export struct HdClockIn {@State clockCount: number = 0 // >0显示已连续打卡xx天 <=0 显示打卡@StorageProp('user') currentUser: iLoginUserModel = {} as iLoginUserModelaboutToAppear(): void {// 请求服务器接口 https://api-harmony-teach.itheima.net/hm/clockinInfo 获取打卡天数this.getClockCount()}// 1. 获取打卡天数async getClockCount() {try {// 1. 导入axios相关包发请求const req = axios.create()let res: AxiosResponse<iResponseModel<iClock>> = await req.request({url: 'https://api-harmony-teach.itheima.net/hm/clockinInfo',headers: {'Authorization': `Bearer ${this.currentUser.token}`}})// 2. 请求url获得数据赋值this.clockCountthis.clockCount = res.data.data.clockinNumbers} catch (err) {let errObj: AxiosError = errAlertDialog.show({ message: errObj.message })}}// 2. 打卡操作async ClockIn() {try {// 1. 发送post请求https://api-harmony-teach.itheima.net/hm/clockin// 记得带上Authorization对应的token(务必要先登录)const req = axios.create()let res: AxiosResponse<iResponseModel<iClock>> =await req.request({method: 'POST',url: 'https://api-harmony-teach.itheima.net/hm/clockin',headers: {'Authorization': `Bearer ${this.currentUser.token}`}})// AlertDialog.show({ message: JSON.stringify(res.data) })// 2. 获取服务器的数据赋值给this.clockCountthis.clockCount = res.data.data.clockinNumbers} catch (err) {let errObj: AxiosError = errAlertDialog.show({ message: errObj.message })}}build() {Stack({ alignContent: Alignment.End }) {Image(this.clockCount > 0 ? $r('app.media.clocked') : $r('app.media.unclock')).objectFit(ImageFit.Fill)if (this.clockCount > 0) {Column() {Text('已连续打卡').fontSize(8)Text() {Span(this.clockCount.toString()).fontWeight(600).fontSize(12)Span(' 天').fontSize(10)}}.width('50')} else {Text('打卡').width('50').textAlign(TextAlign.Center).fontSize((18)).fontWeight(500).fontColor('#333C4F').margin({ bottom: (4) }).onClick(() => {this.ClockIn()})}}.width((74)).height((28))}
}