目录
原型分析
加入一些素材(如:图片,图标,样式等)
欢迎页面Ul实现
// src/main/ets/pages/WelcomePage.ets
@Extend(Text) function opacityWhiteText(opacity: number, fontSize: number = 10) {
.fontSize(fontSize)
.opacity(opacity)
.fontColor(Color.White)
}
@Entry
@Component
struct WelcomePage {
@State message: string = 'Hello World'
build() {
Column({space: 10}){
// 1.中央Slogan
Row(){
Image($r('app.media.home_slogan')).width(260)
}
.layoutWeight(1)
// 2.logo
Image($r('app.media.home_logo')).width(150)
// 3.文字描述
Row() {
Text('黑马健康支持').opacityWhiteText(0.8, 12)
Text('IPv6')
.opacityWhiteText(0.8)
.border({ style: BorderStyle.Solid, width: 1, color: Color.White, radius: 15 })
.padding({ left: 5, right: 5 })
Text('网络').opacityWhiteText(0.8, 12)
}
Text(`'减更多'指黑马健康App希望通过软件工具的形式,帮助更多用户实现身材管理`)
.opacityWhiteText(0.6)
Text('浙ICP备0000000号-36D')
.opacityWhiteText(0.4)
.margin({ bottom: 35 })
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.welcome_page_background'))
}
}
欢迎页面业务
import { CommonConstants } from '../../common/constants/CommonConstants'
// 自定义弹窗
// @Preview 这个注解可以在预览器预览
@CustomDialog
export default struct UserPrivacyDialog {
controller: CustomDialogController
confirm: () => void
cancel: () => void
build() {
Column({space: CommonConstants.SPACE_10}){
// 1.标题
Text($r('app.string.user_privacy_title'))
.fontSize(20)
.fontWeight(CommonConstants.FONT_WEIGHT_700)
// 2.内容
Text($r('app.string.user_privacy_content'))
// 3.按钮
Button($r('app.string.agree_label'))
.width(150)
.backgroundColor($r('app.color.primary_color'))
.onClick(() => {
this.confirm()
this.controller.close()
})
Button($r('app.string.refuse_label'))
.width(150)
.backgroundColor($r('app.color.lightest_primary_color'))
.fontColor($r('app.color.light_gray'))
.onClick(() => {
this.cancel()
this.controller.close()
})
}
.width('100%')
.padding(10)
}
}
使用弹窗
import common from '@ohos.app.ability.common'
import router from '@ohos.router'
import PreferenceUtil from '../common/utils/PreferenceUtil'
import UserPrivacyDialog from '../view/welcome/UserPrivacyDialog'
@Extend(Text) function opacityWhiteText(opacity: number, fontSize: number = 10) {
.fontSize(fontSize)
.opacity(opacity)
.fontColor(Color.White)
}
const PREF_KEY = 'userPrivacyKey'
@Entry
@Component
struct WelcomePage {
context = getContext(this) as common.UIAbilityContext
controller: CustomDialogController = new CustomDialogController({
builder: UserPrivacyDialog({
confirm: () => this.onConfirm(),
cancel: () => this.exitApp()
})
})
async aboutToAppear(){
// 1.加载首选项
let isAgree = await PreferenceUtil.getPreferenceValue(PREF_KEY, false)
// 2.判断是否同意
if(isAgree){
// 2.1.同意,跳转首页
this.jumpToIndex()
}else{
// 2.2.不同意,弹窗
this.controller.open()
}
}
jumpToIndex(){
setTimeout(() => {
router.replaceUrl({
url: 'pages/Index'
})
}, 1000)
}
onConfirm(){
// 1.保存首选项
PreferenceUtil.putPreferenceValue(PREF_KEY, true)
// 2.跳转到首页
this.jumpToIndex()
}
exitApp(){
// 退出APP
this.context.terminateSelf()
}
build() {
Column({ space: 10 }) {
// 1.中央Slogan
Row() {
Image($r('app.media.home_slogan')).width(260)
}
.layoutWeight(1)
// 2.logo
Image($r('app.media.home_logo')).width(150)
// 3.文字描述
Row() {
Text('黑马健康支持').opacityWhiteText(0.8, 12)
Text('IPv6')
.opacityWhiteText(0.8)
.border({ style: BorderStyle.Solid, width: 1, color: Color.White, radius: 15 })
.padding({ left: 5, right: 5 })
Text('网络').opacityWhiteText(0.8, 12)
}
Text(`'减更多'指黑马健康App希望通过软件工具的形式,帮助更多用户实现身材管理`)
.opacityWhiteText(0.6)
Text('浙ICP备0000000号-36D')
.opacityWhiteText(0.4)
.margin({ bottom: 35 })
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.welcome_page_background'))
}
}
详细代码在gitee:https://gitee.com/cheng_yong_xu/ho_healthy
首页Tabs
vertical(false)横向布局 BarPosition.End 就表示下面
vertical(true)纵向布局 BarPosition.End 就表示右
// src/main/ets/pages/Index.ets
import { CommonConstants } from '../common/constants/CommonConstants'
@Entry
@Component
struct Index {
@State currentIndex: number = 0
// 定义一个构建函数,自定义tabar
@Builder TabBarBuilder(title:ResourceStr, image: ResourceStr, index: number){
Column({ space: CommonConstants.SPACE_8 }) {
Image(image)
.width(22)
.fillColor(this.selectColor(index))
Text(title)
.fontSize(14)
.fontColor(this.selectColor(index))
}
}
// 选中颜色,根据来显示颜色,选中绿色,未选中灰色
selectColor(index: number){
return this.currentIndex === index ? $r('app.color.primary_color') : $r('app.color.gray')
}
build() {
Tabs({barPosition:BarPosition.End}){
TabContent(){
Text('饮食记录')
}
.tabBar(this.TabBarBuilder($r('app.string.tab_record'), $r('app.media.ic_calendar'), 0))
TabContent(){
Text('发现页面')
}
.tabBar(this.TabBarBuilder($r('app.string.tab_discover'), $r('app.media.discover'), 1))
TabContent(){
Text('我的主页')
}
.tabBar(this.TabBarBuilder($r('app.string.tab_user'), $r('app.media.ic_user_portrait'), 2))
}
.width('100%')
.height('100%')
.onChange(index => this.currentIndex = index)
.vertical(false)
}
}
饮食记录
顶部搜索栏
// src/main/ets/pages/Index.ets
import { CommonConstants } from '../common/constants/CommonConstants'
import RecordIndex from '../view/record/RecordIndex'
@Entry
@Component
struct Index {
@State currentIndex: number = 0
// 定义一个构建函数,自定义tabar
@Builder TabBarBuilder(title:ResourceStr, image: ResourceStr, index: number){
Column({ space: CommonConstants.SPACE_8 }) {
Image(image)
.width(22)
.fillColor(this.selectColor(index))
Text(title)
.fontSize(14)
.fontColor(this.selectColor(index))
}
}
// 选中颜色,根据来显示颜色,选中绿色,未选中灰色
selectColor(index: number){
return this.currentIndex === index ? $r('app.color.primary_color') : $r('app.color.gray')
}
build() {
Tabs({barPosition:BarPosition.End}){
TabContent(){
RecordIndex()
}
.tabBar(this.TabBarBuilder($r('app.string.tab_record'), $r('app.media.ic_calendar'), 0))
TabContent(){
Text('发现页面')
}
.tabBar(this.TabBarBuilder($r('app.string.tab_discover'), $r('app.media.discover'), 1))
TabContent(){
Text('我的主页')
}
.tabBar(this.TabBarBuilder($r('app.string.tab_user'), $r('app.media.ic_user_portrait'), 2))
}
.width('100%')
.height('100%')
.onChange(index => this.currentIndex = index)
.vertical(false)
}
}
组件
// src/main/ets/view/record/RecordIndex.ets
import SearchHeader from './SearchHeader'
@Component
export default struct RecordIndex {
build() {
Column(){
// 1.头部搜索栏
SearchHeader()
// 2.统计卡片
Text('统计卡片')
// 3.记录列表
Text('记录列表')
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.index_page_background'))
}
}
组件
// src/main/ets/view/record/SearchHeader.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct SearchHeader {
build() {
Row({space: CommonConstants.SPACE_6}){
Search({placeholder: '搜索饮食或运动信息'})
.textFont({size: 18})
.layoutWeight(1)
// Badge,在图标上显示小圆点,小圆点里显示未看消息数量
Badge({count: 1, position: BadgePosition.RightTop, style: {fontSize: 12}}){
Image($r('app.media.ic_public_email'))
.width(24)
}
}
.width(CommonConstants.THOUSANDTH_940)
}
}
统计卡片
上图需要用到日期选择器组件,自定义弹窗,还有AppStorage
// src/main/ets/view/record/StatsCard.ets
import DateUtil from '../../common/utils/DateUtil'
import DatePickDialog from './DatePickDialog'
import { CommonConstants } from '../../common/constants/CommonConstants'
import CalorieStats from './CalorieStats'
import NutrientStats from './NutrientStats'
@Component
export default struct StatsCard {
// 把selectedDate从AppStorage里出去来,这里需要用到 @StorageProp这个注解,同时我们要对取出来的日期处理,因为我们存的是日期毫秒数,这里自己写了个工具类
@StorageProp('selectedDate') selectedDate: number = DateUtil.beginTimeOfDay(new Date())
// 使用自定义弹窗
controller: CustomDialogController = new CustomDialogController({
builder: DatePickDialog({selectedDate: new Date(this.selectedDate)})
})
build(){
Column(){
// 1.日期信息
Row(){
Text(DateUtil.formatDate(this.selectedDate))
.fontColor($r('app.color.secondary_color'))
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor($r('app.color.secondary_color'))
}
.padding(CommonConstants.SPACE_8)
.onClick(() => this.controller.open()) // 打开弹窗
// 2.统计信息
Swiper(){
// 2.1.热量统计
CalorieStats()
// 2.2.营养素统计
NutrientStats()
}
}
}
}
组件
// src/main/ets/view/record/DatePickDialog.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
// 定义日期弹窗
@CustomDialog
export default struct DatePickDialog {
controller: CustomDialogController
selectedDate: Date = new Date()
build() {
Column({space: CommonConstants.SPACE_12}){
// 1.日期选择器,DatePicker官方组件
DatePicker({
start: new Date('2020-01-01'),
end: new Date(),
selected: this.selectedDate
})
.onChange((value: DatePickerResult) => {
// 注意这里,选择的结果是年月日,没有时分秒,默认是当日的0时,0分,0秒
this.selectedDate.setFullYear(value.year, value.month, value.day)
})
// 2.按钮
Row({space:CommonConstants.SPACE_12}){
Button('取消')
.width(120)
.backgroundColor($r('app.color.light_gray'))
.onClick(() => {
this.controller.close()
})
Button('确定')
.width(120)
.backgroundColor($r('app.color.primary_color'))
.onClick(()=> {
// 1.保存日期到全局存储
// AppStorage是一个全局的存储空间,getTime()获取的是日期毫秒数,以为存一个日期对象,会不好处理
AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime())
// 2.关闭窗口
this.controller.close()
})
}
}
.padding(CommonConstants.SPACE_12)
}
}
组件
// src/main/ets/view/record/CalorieStats.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct CalorieStats {
// 饮食摄入
intake: number = 192
// 消耗
expend: number = 150
//推荐的
recommend: number = CommonConstants.RECOMMEND_CALORIE
// 计算还可以吃的量
remainCalorie(){
return this.recommend - this.intake + this.expend
}
build() {
Row({space: CommonConstants.SPACE_6}){
// 1.饮食摄入
this.StatsBuilder('饮食摄入', this.intake)
// 2.还可以吃
Stack(){ // 堆叠容器
// 2.1.进度条
Progress({
value: this.intake,
total: this.recommend,
type: ProgressType.Ring
})
.width(120)
.style({strokeWidth: CommonConstants.DEFAULT_10}) // 进度条加粗
.color($r('app.color.primary_color'))
// 2.2.统计数据
this.StatsBuilder('还可以吃', this.remainCalorie(), `推荐${this.recommend}`)
}
// 3.运动消耗
this.StatsBuilder('运动消耗', this.expend)
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({top: 30, bottom: 35})
}
@Builder StatsBuilder(label: string, value: number, tips?: string){
Column({space: CommonConstants.SPACE_6}){
Text(label)
.fontColor($r('app.color.gray'))
.fontWeight(CommonConstants.FONT_WEIGHT_600)
Text(value.toFixed(0))
.fontSize(20)
.fontWeight(CommonConstants.FONT_WEIGHT_700)
if(tips){
Text(tips)
.fontSize(12)
.fontColor($r('app.color.light_gray'))
}
}
}
}
组件
// src/main/ets/view/record/NutrientStats.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NutrientStats {
// 碳水化合物
carbon: number = 10
// 蛋白质
protein: number = 20
// 脂肪
fat: number = 20
// 推荐摄入的碳水化合物,蛋白质,脂肪
recommendCarbon: number = CommonConstants.RECOMMEND_CARBON
recommendProtein: number = CommonConstants.RECOMMEND_PROTEIN
recommendFat: number = CommonConstants.RECOMMEND_FAT
build() {
Row({space: CommonConstants.SPACE_6}){
this.StatsBuilder(
'碳水化合物',
this.carbon,
this.recommendCarbon,
$r('app.color.carbon_color')
)
this.StatsBuilder(
'蛋白质',
this.protein,
this.recommendProtein,
$r('app.color.protein_color')
)
this.StatsBuilder(
'脂肪',
this.fat,
this.recommendFat,
$r('app.color.fat_color')
)
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({top: 30, bottom: 35})
}
@Builder StatsBuilder(label: string, value: number, recommend: number, color: ResourceStr){
Column({space: CommonConstants.SPACE_6}){
Stack(){
Progress({
value: value,
total: recommend,
type: ProgressType.Ring
})
.width(95)
.style({strokeWidth: CommonConstants.DEFAULT_6})
.color(color)
Column({space: CommonConstants.SPACE_6}){
Text('摄入推荐')
.fontSize(12)
.fontColor($r('app.color.gray'))
Text(`${value.toFixed(0)}/${recommend.toFixed(0)}`)
.fontSize(18)
.fontWeight(CommonConstants.FONT_WEIGHT_600)
}
}
Text(`${label}(克)`)
.fontSize(12)
.fontColor($r('app.color.light_gray'))
}
}
}
记录列表
// src/main/ets/view/record/RecordIndex.ets
import RecordList from './RecordList'
import SearchHeader from './SearchHeader'
import StatsCard from './StatsCard'
@Component
export default struct RecordIndex {
build() {
Column(){
// 1.头部搜索栏
SearchHeader()
// 2.统计卡片
StatsCard()
// 3.记录列表
RecordList()
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.index_page_background'))
}
}
组件
// src/main/ets/view/record/RecordList.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Extend(Text) function grayText() {
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
@Component
export default struct RecordList {
build() {
List({space: CommonConstants.SPACE_10}){
ForEach([1,2,3,4,5,6], (item) => {
ListItem() {
Column({ space: CommonConstants.SPACE_8 }) {
// 1.分组的标题
Row({space: CommonConstants.SPACE_4}){
Image($r('app.media.ic_breakfast')).width(24)
Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
Text('建议423-592千卡').grayText()
Blank()
Text('190').fontSize(14).fontColor($r('app.color.primary_color'))
Text('千卡').grayText()
Image($r('app.media.ic_public_add_norm_filled'))
.width(20)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
// 2.组内记录列表
List(){
ForEach([1,2], (item) => {
ListItem(){
Row({space: CommonConstants.SPACE_6}){
Image($r('app.media.toast')).width(50)
Column({space: CommonConstants.SPACE_4}){
Text('全麦土司').fontWeight(CommonConstants.FONT_WEIGHT_500)
Text(`1片`).grayText()
}
Blank()
Text('91千卡').grayText()
}
.width('100%')
.padding(CommonConstants.SPACE_6)
}.swipeAction({end: this.deleteButton.bind(this)})
})
}
.width('100%')
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(CommonConstants.DEFAULT_18)
.padding(CommonConstants.SPACE_12)
}
})
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
.margin({top: 10})
}
@Builder deleteButton() {
Image($r('app.media.ic_public_delete_filled'))
.width(20)
.height(20)
.fillColor(Color.Red)
.margin(5)
}
}
食物列表页
// src/main/ets/pages/ItemIndex.ets
import { CommonConstants } from '../common/constants/CommonConstants'
import ItemList from '../view/record/ItemList'
@Entry
@Component
struct ItemIndex {
@State message: string = 'Hello World'
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList()
}
}
@Builder Header() {
Row() {
Image($r('app.media.ic_public_back'))
.width(24)
Blank()
Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
}
.width(CommonConstants.THOUSANDTH_940)
.height(32)
}
}
组件
// src/main/ets/view/record/ItemList.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemList {
build() {
Tabs() {
TabContent() {
List({space: CommonConstants.SPACE_10}){
ForEach([1,2,3,4,5,6], (item) => {
ListItem(){
Row({space: CommonConstants.SPACE_6}){
Image($r('app.media.toast')).width(50)
Column({space: CommonConstants.SPACE_4}){
Text('全麦土司').fontWeight(CommonConstants.FONT_WEIGHT_500)
Text(`1片`).fontSize(14).fontColor($r('app.color.light_gray'))
}
Blank()
Image($r('app.media.ic_public_add_norm_filled'))
.width(18)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
.padding(CommonConstants.SPACE_6)
}
})
}
.width('100%')
.height('100%')
}
.tabBar('全部')
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
}
}```
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/2e6d69cefd8f48a5b02e04e9bb9c4368.png)
封装
```javascript
// src/main/ets/view/record/ItemList.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemList {
build() {
Tabs() {
TabContent() {
this.TabContentBuilder()
}
.tabBar('全部')
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
}
@Builder TabContentBuilder() {
List({space: CommonConstants.SPACE_10}){
ForEach([1,2,3,4,5,6], (item) => {
ListItem(){
Row({space: CommonConstants.SPACE_6}){
Image($r('app.media.toast')).width(50)
Column({space: CommonConstants.SPACE_4}){
Text('全麦土司').fontWeight(CommonConstants.FONT_WEIGHT_500)
Text(`1片`).fontSize(14).fontColor($r('app.color.light_gray'))
}
Blank()
Image($r('app.media.ic_public_add_norm_filled'))
.width(18)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
.padding(CommonConstants.SPACE_6)
}
})
}
.width('100%')
.height('100%')
}
}
食物列表底部Panel
源码在gitee
食物列表数字键盘
// src/main/ets/pages/ItemIndex.ets
import { CommonConstants } from '../common/constants/CommonConstants'
import ItemCard from '../view/item/ItemCard'
import ItemList from '../view/item/ItemList'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import NumberKeyboard from '../view/item/NumberKeyboard'
@Extend(Button) function panelButtonStyle(){
.width(120)
.type(ButtonType.Normal)
.borderRadius(6)
}
@Entry
@Component
struct ItemIndex {
@State showPanel: boolean = false
@State amount: number = 1
@State value: string = ''
onPanelShow(){
this.amount = 1
this.value = ''
this.showPanel = true
}
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList({showPanel: this.onPanelShow.bind(this)})
.layoutWeight(1) // 这里注意,ItemList作为Panel的容器,必须要有高度
// 3.底部面板
Panel(this.showPanel) {
// 3.1.顶部日期
ItemPanelHeader()
// 3.2.记录项卡片
ItemCard({amount: this.amount})
// 3.3.数字键盘
NumberKeyboard({amount: $amount, value: $value})
// 3.4.按钮
Row({space: CommonConstants.SPACE_6}){
Button('取消')
.panelButtonStyle()
.backgroundColor($r('app.color.light_gray'))
.onClick(() => this.showPanel = false)
Button('提交')
.panelButtonStyle()
.backgroundColor($r('app.color.primary_color'))
.onClick(() => this.showPanel = false)
}
.margin({top: 10})
}
.mode(PanelMode.Full)
.dragBar(false)
.backgroundMask($r('app.color.light_gray')) // 面板背景颜色
.backgroundColor(Color.White) // 面板自己的背景颜色
}
.width('100%')
.height('100%')
}
@Builder Header() {
Row() {
Image($r('app.media.ic_public_back'))
.width(24)
Blank()
Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
}
.width(CommonConstants.THOUSANDTH_940)
.height(32)
}
}
// src/main/ets/view/item/NumberKeyboard.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NumberKeyboard {
numbers: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.']
@Link amount: number
@Link value: string
@Styles keyBoxStyle(){
.backgroundColor(Color.White)
.borderRadius(8)
.height(60)
}
build() {
Grid(){
ForEach(this.numbers, num => {
GridItem(){
Text(num).fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickNumber(num))
})
GridItem(){
Text('删除').fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickDelete())
}
.width('100%')
.height(280)
.backgroundColor($r('app.color.index_page_background'))
.columnsTemplate('1fr 1fr 1fr') // 分成3列
.columnsGap(8) // 列间距
.rowsGap(8) // 行间距
.padding(8)
.margin(10)
}
clickNumber(num: string){
// 1.拼接用户输入的内容
let val = this.value + num
// 2.校验输入格式是否正确
let firstIndex = val.indexOf('.') // 获得最前面的小数点
let lastIndex = val.lastIndexOf('.') // 获得最前后的小数点
if(firstIndex !== lastIndex || (lastIndex != -1 && lastIndex < val.length - 2)){
// 非法输入
// firstIndex !== lastIndex 说明有两个及以上个小数点
// lastIndex != -1 && lastIndex < val.length - 2 有小数点,且小数长度不大于2,就是保留一位小数
return
}
// 3.将字符串转为数值
let amount = this.parseFloat(val)
// 4.保存
if(amount >= 999.9){
this.amount = 999.0
this.value = '999'
} else{
this.amount = amount
this.value = val
}
}
clickDelete(){
if(this.value.length <= 0){
this.value = ''
this.amount = 0
return
}
this.value = this.value.substring(0, this.value.length - 1)
this.amount = this.parseFloat(this.value)
}
parseFloat(str: string){
// 非空判断
if(!str){
return 0
}
// 如果最后有一个小数点,比如这样的 23. 就把最后的小数点去掉
if(str.endsWith('.')){
str = str.substring(0, str.length - 1)
}
return parseFloat(str)
}
}
多设备响应式布局
我们写的是手机的样式和布局
放大会变形
应该根据不同的设备动态的调整布局
媒体查询技术
源码在gitee:https://gitee.com/cheng_yong_xu/ho_healthy
数据模型
记录项
我们把饮食和运动,定义到一个模型里
思路我们从,最单元开始,建立数据模型,比如显示食物和运动,再去建立分类,时间等等的数据模型
源码在gitee
饮食记录
数据模型通用DB工具
通用DB工具都可以用,可以简化我们对关系数据库的增删改查
定义数据模型
// 记录的数据
// 注意和数据库字段名字不一样,所以还需要定义一个,数据库字段和数据记录数据相对应的类ColumnInfo
export default class RecordPO{
/**
* 记录id
*/
id?: number
/**
* 饮食记录类型
*/
typeId: number
/**
* 记录中的食物或运动信息
*/
itemId: number
/**
* 食物数量或运动时长,如果是运动信息则无
*/
amount: number
/**
* 记录的日期
*/
createTime: number
}
export class ColumnInfo{
name: string
columnName: string
type: ColumnType
}
export enum ColumnType{
LONG,
DOUBLE,
STRING,
BLOB
}
工具类
import common from '@ohos.app.ability.common'; // 系统工具
import relationalStore from '@ohos.data.relationalStore'; // 系统数据库
import { ColumnInfo, ColumnType } from '../bean/ColumnInfo';
import Logger from './Logger'; // 自定义打印类
const DB_FILENAME: string = 'HealthyApp.db'
class DbUtil {
rdbStore: relationalStore.RdbStore
initDB(context: common.UIAbilityContext): Promise<void> {
let config: relationalStore.StoreConfig = {
name: DB_FILENAME,
securityLevel: relationalStore.SecurityLevel.S1
}
return new Promise<void>((resolve, reject) => {
relationalStore.getRdbStore(context, config)
.then(rdbStore => {
this.rdbStore = rdbStore
Logger.debug('rdbStore 初始化完成!')
resolve()
})
.catch(reason => {
Logger.debug('rdbStore 初始化异常', JSON.stringify(reason))
reject(reason)
})
})
}
createTable(createSQL: string): Promise<void> {
return new Promise((resolve, reject) => {
this.rdbStore.executeSql(createSQL)
.then(() => {
Logger.debug('创建表成功', createSQL)
resolve()
})
.catch(err => {
Logger.error('创建表失败,' + err.message, JSON.stringify(err))
reject(err)
})
})
}
// obj是匿名对象{id: 12, name:zs,age: 18}
insert(tableName: string, obj: any, columns: ColumnInfo[]): Promise<number> {
return new Promise((resolve, reject) => {
// 1.构建新增数据
let value = this.buildValueBucket(obj, columns)
this.rdbStore.insert(tableName, value, (err, id) => {
if (err) {
Logger.error('新增失败!', JSON.stringify(err))
reject(err)
}else {
Logger.debug('新增成功!新增id:', id.toString())
resolve(id)
}
})
})
}
delete(predicates: relationalStore.RdbPredicates): Promise<number> {
return new Promise((resolve, reject) => {
this.rdbStore.delete(predicates, (err, rows) => {
if (err) {
Logger.error('删除失败!', JSON.stringify(err))
reject(err)
} else {
Logger.debug('删除成功!删除行数:', rows.toString())
resolve(rows)
}
})
})
}
// columns 把你要查询的字段告诉我
// 以后调用的时候知道类型
queryForList<T>(predicates: relationalStore.RdbPredicates, columns: ColumnInfo[]): Promise<T[]> {
return new Promise((resolve, reject) => {
this.rdbStore.query(predicates, columns.map(info => info.columnName), (err, result) => { // map将对象里的属性映射成数组
if (err) {
Logger.error('查询失败!', JSON.stringify(err))
reject(err)
} else {
Logger.debug('查询成功!查询行数:', result.rowCount.toString())
resolve(this.parseResultSet(result, columns)) // 由于查询结果是一个ResultSet,我们解析成数组,以后调用的时候方便
}
})
})
}
// 解析函数
parseResultSet<T>(result: relationalStore.ResultSet, columns: ColumnInfo[]){
// 1.声明最终返回的结果
let arr = []
if (result.rowCount <= 0) {
return arr
}
// 3.处理结果
while (!result.isAtLastRow) {
// 3.1.去下一行
result.goToNextRow()
// 3.2.解析这行数据,转为对象
let obj = {}
columns.forEach(info => {
let val = null
switch (info.type) { // 判断类型
case ColumnType.LONG: // 如果是LONG
val = result.getLong(result.getColumnIndex(info.columnName))
break
case ColumnType.DOUBLE: // 如果是DOUBLE
val = result.getDouble(result.getColumnIndex(info.columnName))
break
case ColumnType.STRING: // 如果是STRING
val = result.getString(result.getColumnIndex(info.columnName))
break
case ColumnType.BLOB: // 如果是BLOB
val = result.getBlob(result.getColumnIndex(info.columnName))
break
}
obj[info.name] = val
})
// 3.3.将对象填入结果数组
arr.push(obj)
Logger.debug('查询到数据:', JSON.stringify(obj))
}
return arr
}
buildValueBucket(obj: any, columns: ColumnInfo[]): relationalStore.ValuesBucket{
let value = {}
columns.forEach(info => {
let val = obj[info.name]
if (typeof val !== 'undefined') {
value[info.columnName] = val // 数据库的字段名和我们添加时候写的属性名称对应起来
}
})
return value
}
}
let dbUtil: DbUtil = new DbUtil();
export default dbUtil as DbUtil
数据查询
/**
* 数据库建表语句
*/
import relationalStore from '@ohos.data.relationalStore'
import { ColumnInfo, ColumnType } from '../common/bean/ColumnInfo'
import RecordPO from '../common/bean/RecordPO'
import DbUtil from '../common/utils/DbUtil'
const CREATE_TABLE_SQL: string = `
CREATE TABLE IF NOT EXISTS record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
amount DOUBLE NOT NULL,
create_time INTEGER NOT NULL
)
`
const COLUMNS: ColumnInfo[] = [
{name: 'id', columnName: 'id', type: ColumnType.LONG},
{name: 'typeId', columnName: 'type_id', type: ColumnType.LONG},
{name: 'itemId', columnName: 'item_id', type: ColumnType.LONG},
{name: 'amount', columnName: 'amount', type: ColumnType.DOUBLE},
{name: 'createTime', columnName: 'create_time', type: ColumnType.LONG}
]
// 表名
const TABLE_NAME = 'record'
const ID_COLUMN = 'id'
const DATE_COLUMN = 'create_time'
class RecordModel {
getCreateTableSql(): string {
return CREATE_TABLE_SQL
}
insert(record: RecordPO): Promise<number>{
// TABLE_NAM E表名
// record 插入的数据
// COLUMNS 数据库字段名
return DbUtil.insert(TABLE_NAME, record, COLUMNS)
}
deleteById(id: number): Promise<number>{
// 1.删除条件
let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
predicates.equalTo(ID_COLUMN, id)
// 2.删除
return DbUtil.delete(predicates)
}
listByDate(date: number): Promise<RecordPO[]>{
// 1.查询条件
let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
// predicates查询条件
predicates.equalTo(DATE_COLUMN, date)
// 2.查询
// predicates查询条件, 字段
return DbUtil.queryForList(predicates, COLUMNS)
}
}
let recordModel = new RecordModel()
export default recordModel as RecordModel
业务层开发
我们上面把数据从数据库中查出来了,但是我们页面需要的数据,还有些差别,我们需要处理下
从数据库里查询的内容,应该先做分组在渲染
gitee 源码:
https://gitee.com/cheng_yong_xu/ho_healthy