简言:
gitee地址:https://gitee.com/whltaoin_admin/money-controller-app.git
端云一体化开发在线文档:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/agc-harmonyos-clouddev-view-0000001700053733-V5
注:此App参照此教程进行二次修改:https://www.bilibili.com/video/BV1q5411v7o7
一、简介
moneyControllerApp(MCA)
这款精心打造的个人财务管理应用,是您理财路上的智慧伙伴。凭借前沿的智能化技术与直观易用的界面设计,它将化繁为简,让您的财务状况一目了然。无论是日常收支的记录,还是复杂财务的分析,都能轻松应对。它不仅帮助您有效掌控每一笔收入与支出,更助您洞悉财务趋势,科学规划未来,让财富增长之路更加清晰可见。从此,财务管理不再是难题,而是通往财务自由的桥梁。
在鸿蒙HarmonyOS Next版本的加持下,这款个人财务管理应用的性能与体验再度升级,成为您理财旅程中的超级智慧伙伴。鸿蒙系统的分布式技术,使得应用运行更加流畅稳定,数据同步更加快速准确,即使在多设备间切换,也能无缝衔接,确保您的财务信息实时更新,安全无忧。
二、什么是端云一体化开发
为丰富HarmonyOS对云端开发的支持、实现端云联动,DevEco Studio以Cloud Foundation Kit(云开发服务)为底座、在传统的“端开发”基础上新增“云开发”能力,开发者在创建工程时选择合适的云开发工程模板,即可在DevEco Studio内同时完成HarmonyOS应用的端侧与云侧开发,体验端云一体化协同开发。
三、开发环境介绍
编辑器 | DevEco Studio NEXT Developer Beta1 |
---|---|
SDK | 11 |
操作系统 | Window 10 专业版 |
模拟器 | HarmonyOS Emulator Version: 5.0.3.405 HarmonyOS Version: HarmonyOS NEXT Developer Beta1 |
四、项目初始化
- 步骤一:
/*
1 create project
2 application选择>>>[cloudDev] Empty Ability>>>Next
*/
- 步骤二:输入图中信息后>>>点击Finish
- 注意:存放路径不建议使用中文字符
- 步骤三:进入项目主页>>>点击右上角的头像进行用户登录。
- 步骤四:
// 1 进入网址并进行登录:https://developer.huawei.com/consumer/cn/
// 2 登录后在网站首页点击管理中心
// 3 点击左侧边栏(生态服务-应用服务)>>>点击AppGallery Connect
// 4 进入到以下页面
- 步骤四:
// 1 点击我的项目>>>新建项目
// 2 数据处理位置选择中国并设置为默认
// 3 点击完成后并添加应用
// 4 注意:创建应用时如果想要自定义包名的话,定义的包名必须和新建项目时写的包名一致。
// 5 创建应用完成后,点击Next后,新建项目既可创建完成。
五、项目构建静态页面
登录注册页面
- 效果图
结构:
// 一个页面:Login.etc
// 两个组件:
// 头部标题组件:titleComponent.ets
// 表单组件:InputComponent.ets
- 代码
// Login.ets
import InputComponent from '../components/InputComponent';
import TitleComponent from '../components/TitleComponent';
import { typeNode } from '@ohos.arkui.node';
import { TESTTYPE } from '@ohos/hypium/src/main/Constant';
@Entry
@Component
struct Login {
@State message: string = 'Login';
// 倒计时
@State countDown :number = 60
timer :number=0
@State isRegister:boolean= false
// 发送验证码
sendCode(){
this.startCountDown()
}
// 开始倒计时
startCountDown(){
this.timer = setInterval(()=>{
this.countDown--
if(this.countDown===0){
this.countDown=60
clearInterval(this.timer)
}
},1000)
}
build() {
Column(){
// title
TitleComponent({title:"登录"})
// login_content
Stack({alignContent:Alignment.Top}){
Image($r("app.media.Login_icon")).width(88).height(88).offset({y:-44}).zIndex(999)
Column({space:10}){
// emial
InputComponent({title:"电子邮箱",inputIcon:$r("app.media.mail_icon"),placeholder:"请输入邮箱信息"})
// pwd
InputComponent({title:"密码",inputIcon:$r("app.media.pwd_icon"),placeholder:"请输入密码",inputType:InputType.Password})
// VCode
if(this.isRegister){
Column(){
Text("验证码").width("100%").textAlign(TextAlign.Start).fontWeight(500)
.fontSize(16).fontColor(Color.Black).margin({bottom:14})
Row(){
TextInput({placeholder:"请输入验证码"})
.layoutWeight(1)
.backgroundColor(Color.Transparent)
.border({
width:1,
color:"#ff9b9b9b"
}).borderRadius(10)
Button(this.countDown==60?"点击获取验证码":`${this.countDown}s`).fontSize("10").margin({left:10}).width(100).padding(0).onClick((event: ClickEvent) => {
if(this.countDown===60){
this.sendCode()
}else{
AlertDialog.show({
message:"正在获取验证码,请等待..."
})
}
})
}.width("100%").height(50)
}
}
// login_btn
Button(this.isRegister?"注册":"登录").width(228).backgroundColor("#ff09b19d").margin({top:50})
.onClick(()=>{
// 登录方法
})
// re_btn
Row(){
Text(this.isRegister?"去登录":"去注册").fontSize(12).onClick(()=>{
this.isRegister= !this.isRegister
})
Text("|").padding({left:10,right:10})
Text("忘记密码").fontSize(12)
}.width("100%").layoutWeight(1).justifyContent(FlexAlign.Center)
}.width("100%").height("100%").padding({left:14,right:14}).margin({top:44})
}.width("90%").backgroundColor(Color.White).margin({top:44}).layoutWeight(1)
.borderRadius(20)
}.width("100%").height("100%").backgroundColor($r("app.color.page_Color"))
}
}
// InputComponent.ets
@Component
export default struct InputComponent {
@Prop title:string
@Prop inputIcon:Resource
@Prop placeholder:string
@Prop inputType:InputType=InputType.Normal
@State changeStatus:boolean =false
build() {
Column(){
Text(this.title).width("100%").textAlign(TextAlign.Start).fontWeight(500)
.fontSize(16).fontColor(Color.Black).margin({bottom:14})
Row(){
Image(this.inputIcon).width(40).aspectRatio(1)
TextInput({placeholder:this.placeholder})
.onFocus(()=>{
// 聚焦
this.changeStatus=true
console.log("result>>>",this.changeStatus)
})
.onBlur(()=>{
// 失去
this.changeStatus=false
console.log("result>>>",this.changeStatus)
})
.layoutWeight(1)
.backgroundColor(Color.Transparent)
.type(this.inputType)
}.width("100%").height(50).padding({left:10,right:10}).borderRadius(10)
.border({
width:2,color:this.changeStatus?"#002884":Color.White
})
}
}
}
// 页面标题组件 TitleComponent.ets
@Component
export default struct TitleComponent {
@Prop title :string
build() {
Row(){
Image($r("app.media.Button_left")).width("44").height(41).objectFit(ImageFit.ScaleDown)
Text(this.title).fontColor("#ff403f3f").fontWeight(700).fontSize(20).height(40)
Text("")
}.width("100%").justifyContent(FlexAlign.SpaceBetween).padding({left:20,right:20,top:12,bottom:12})
}
}
主页框架及底部导航栏
- 效果图(点击底部图标后,可以切换到对应页面并修改选中图标的底色。)
- 功能点及编写思路
1 看着效果图像是多个页面编写而成的,其实就只有一个页面,通过tabs组件框架,嵌套其他组件从而形成多页面效果
2 框架编写思路:
整理和页面通用的数据并提取,在主页定义一个tabs组件,
分别定义5个页面的组件,和底部导航栏的组件
3 图标切换状态思路:
因为底部导航栏的数据是封装到了一个数组中,可以给每个对象定义一个ID属性,同时在主框架中定义一个
装饰器变量来监听tabs的onchange事件,因为ongchange事件会传递tab的下标,所有可以将传递的下标赋值给装饰器变量,
再将装饰器变量传递给底部导航栏图标组件,从而判断是否选中切换图标。
- 结构:
实体类:
BtnNavData
页面:
MainPage
组件:
CBtnNavImage
DataStatistics
Home
My
Wallet
代码:
// MainPage
import CBtnNavImage from './components/CBtnNavImage'
import { createBtnNavDataList,BtnNavData } from './model/BtnNavData'
@Entry
@Component
struct MainPage {
@State btnNavItemid :number=0
@State btnNavDataList:BtnNavData[] =createBtnNavDataList()
// tabBar
@Builder
tabItemBar(item :BtnNavData){
CBtnNavImage({btnNavData:item,isSelect:this.btnNavItemid})
}
build() {
Tabs({barPosition:BarPosition.End}){
ForEach(this.btnNavDataList,(item:BtnNavData,index)=>{
TabContent(){
Text(this.btnNavItemid.toString())
}.tabBar( this.tabItemBar(item))
})
}.onChange((index)=>{
// 切换图标
// console.log("result>>>>",index)
if(index !=2){
this.btnNavItemid =index
}
})
.backgroundImage($r("app.media.Subtract"))
.backgroundImagePosition(Alignment.BottomEnd)
.backgroundImageSize({
width:"100%",
height:50
})
}
}
// BtnNavData
interface IBtnNavData{
selectIcon:Resource
nowIcon:Resource
title:string
id:number
}
export class BtnNavData{
selectIcon:Resource
nowIcon:Resource
title:string
id:number
isQrcode:boolean
constructor(obj:IBtnNavData,isQrcode=false) {
this.selectIcon=obj.selectIcon
this.nowIcon=obj.nowIcon
this.title=obj.title
this.id=obj.id
this.isQrcode =isQrcode
}
}
export const createBtnNavDataList =():BtnNavData[]=>{
return [
new BtnNavData(
{
id:0,
title:"首页",
nowIcon:$r("app.media.home_icon_unselect"),
selectIcon:$r("app.media.home_icon_select"),
}
),
new BtnNavData(
{
id:1,
title:"数据展示",
nowIcon:$r("app.media.data_icon_unselect"),
selectIcon:$r("app.media.data_icon_select"),
}
),
new BtnNavData(
{
id:2,
title:"扫一扫",
nowIcon:$r("app.media.qrcode_icon"),
selectIcon:$r("app.media.qrcode_icon"),
},true
),
new BtnNavData(
{
id:3,
title:"钱包",
nowIcon:$r("app.media.wallet_icon_unselect"),
selectIcon:$r("app.media.wallet_icon_select"),
}
),
new BtnNavData(
{
id:4,
title:"我的",
nowIcon:$r("app.media.my_icon_unselect"),
selectIcon:$r("app.media.my_icon_select"),
}
)
]
}
// CBtnNavImage
import { createBtnNavDataList,BtnNavData } from '../model/BtnNavData'
@Component
export default struct CBtnNavImage {
@Prop btnNavData:BtnNavData
@Prop isSelect :number =0
build() {
Column(){
Image(this.isSelect ==this.btnNavData.id ?this.btnNavData.selectIcon:this.btnNavData.nowIcon).width(20).height(20)
.offset({ y:this.btnNavData.isQrcode? -15 :0 })
}.width("100%").justifyContent(FlexAlign.Center).height("100%")
}
}
// 其余文件均为占位,并未编写
day01持续更新中…