HarmonyOS | 项目开发练习 「钢笔单词」 #3 首页、书架页、用户选项页 | 涉及弹性布局(FLEX)、用户首选项、关系型数据库
项目结构:
首页:
首页的重点是统计单词量的卡片,但是只实现了UI,实际上不能进行单词量统计。卡片上有一个日期选择器,卡片UI本身的颜色会根据单词量情况而改变,小于等于300为蓝色,否则为红色。卡片可以翻页,第二页设计用于记录背诵的单词总数和单日记录。
首页使用 Tabs 与 TabContent 实现不同子页面卡片的切换。
Index.ets 构成整个应用程序核心页面的骨架
``
import BookIndex from '../view/book_page/BookIndex'
import HomeIndex from '../view/home_page/HomeIndex'
import UserIndex from '../view/user_page/UserIndex'
@Entry
@Component
struct Index {
@State currentIndex: number = 0
@Builder TabBarBuilder(title: ResourceStr, image: ResourceStr, index: number){
Column(){
Image(image).width(33)
.fillColor(this.currentIndex === index ? Color.White : $r('app.color.gray'))
}
.backgroundColor(this.currentIndex === index ? $r('app.color.lightest_primary_color') : Color.White)
.width('80%').height('100%').justifyContent(FlexAlign.Center).margin({left: 20})
}
build() {
Stack(){
// 直接给父组件设置背景图片,在设备反转时,背景图片不转动,留下空白,不知道为什么没有自动旋转
Image($r('app.media.1719303607445')).width('100%').height('100%')
Tabs(){
TabContent(){
HomeIndex()
}.tabBar(this.TabBarBuilder($r('app.string.tab_home_page'), $r('app.media.home'), 0))
TabContent(){
BookIndex();
}.tabBar(this.TabBarBuilder($r('app.string.tab_music_hall'), $r('app.media.bookshelf'), 1))
TabContent(){
UserIndex()
}.tabBar(this.TabBarBuilder($r('app.string.tab_user'), $r('app.media.user10'), 2))
}.vertical(true).onChange(index => this.currentIndex = index)
}
}
}
接下来是被 Index.ets 调用的各个页面卡片,这些页面卡片里面又各自包含了一些模块。
HomeIndex.ets 构成首页
``
import Header from '../../common/Components/Header'
import StatisticsCard from './StatisticsCard'
@Component
export default struct HomeIndex {
build() {
Flex({direction: FlexDirection.Column}){
Header({text: '首页'})
Divider().margin({top: 10, bottom: 30})
Flex({justifyContent: FlexAlign.SpaceEvenly}){
StatisticsCard({color: '#ffffffff' })
StatisticsCard({color: '#ffffffff'})
}
}.width('96%').height('90%')
.backgroundColor(Color.White)
// .opacity(0.7).backdropBlur(30) // 毛玻璃
.shadow({radius: 5, color: $r('app.color.light_primary_color')}).borderRadius(10)
}
}
Header.ets 通用顶部栏
``
@Component
export default struct SearchHeader {
text: string
build() {
Flex({direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween}){
Text(this.text).fontSize(40).margin({left: 35}).fontWeight(FontWeight.Bold)
// @ts-ignore
Search({placeholder: $r('app.string.search_text')})
.textFont({size: 20}).backgroundColor('#ffffffff').placeholderColor('#ff2f2f2f')
.searchButton('搜索').width('50%').borderWidth(1)
Badge({value: '!', position: BadgePosition.Left, style: {fontSize: 16}}){
Image($r('app.media.mail')).fillColor($r('app.color.light_primary_color')).width(40).margin({left: 10, right: 10})
}
}
}
}
StatisticsCard.ets 构成统计卡片的骨架,这个卡片可以进行翻页
``
import DateUtil from '../../common/utils/DateUtil'
import DatePickDialog from './DatePickDialog'
import SummaryStat from './SummaryStat'
import TodayStatistics from './TodayStatistics'
@Component
export default struct StatisticsCard {
color: ResourceStr
@StorageProp('selectedDate') selectedDate: number = DateUtil.beginTimeOfDay(new Date()) // 获取开始日期
controller: CustomDialogController = new CustomDialogController({
builder: DatePickDialog({selectedDate: new Date(this.selectedDate)})
})
build() {
Flex({direction: FlexDirection.Column}){
Row(){
Image($r('app.media.more')).width(30).fillColor(Color.Black)
Text(DateUtil.formatDate(this.selectedDate)).fontColor(Color.Black).fontSize(30)
}.width('100%').backgroundColor(Color.White).padding(10)
.onClick(() => this.controller.open())
Swiper(){
TodayStatistics()
SummaryStat()
}.width('100%').height('80%').borderRadius(10).indicatorStyle({selectedColor: Color.White}).loop(true)
}.backgroundColor(this.color).width('45%').height('35%').borderRadius(10)
.shadow({radius: 5, color: $r('app.color.light_primary_color')})
}
}
TodayStatistics.ets 卡片的第一个页面
``
@Component
export default struct TodayStatistics {
@State count: number = 80 // 今日背词总数
readonly healthNum: number = 300 // 安全次数上限
build() {
Flex({justifyContent: FlexAlign.SpaceAround, alignItems: ItemAlign.Center}){
Flex({direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){
Text('今日词汇量').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
Text((this.count).toString()).fontSize(30).fontColor(Color.White)
Text(this.count <= this.healthNum ? '适宜' : '过量').fontColor(Color.White)
}
Progress({
value: this.count,
total: this.healthNum,
type: ProgressType.Eclipse
}).width('20%').color(Color.White)
Text('记录').fontSize(40).fontColor(Color.White).fontWeight(FontWeight.Bold)
}.padding(30).backgroundColor(this.count <= this.healthNum ? $r('app.color.primary_color') : '#ffff2e4f')
}
}
SummaryStat.ets 卡片的第二个页面
``
@Component
export default struct SummaryStat {
@State historyCount: number = 23540 // 听歌总数
@State dayCount: number = 30 // 单日最高记录
build() {
Flex({ justifyContent: FlexAlign.SpaceAround, alignItems: ItemAlign.Center }) {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Text('单日最高记录').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
Text((this.dayCount).toString()).fontSize(30).fontColor(Color.White)
}
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Text('历史次数').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
Text((this.historyCount).toString()).fontSize(30).fontColor(Color.White)
}
}.padding(30).backgroundColor('#ff2d4167')
}
}
DatePickDialog.ets 日期弹窗
``
import CommonConstants from '../../common/constants/CommonConstants'
@CustomDialog
export default struct DatePickDialog {
controller: CustomDialogController
selectedDate: Date = new Date()
build() {
Column({space: CommonConstants.SPACE_10}){
DatePicker({ // 日期选择器
start: new Date('2012-01-01'),
end: new Date(),
selected: this.selectedDate
})
.onChange((value: DatePickerResult) => {
this.selectedDate.setFullYear(value.year, value.month, value.day)
console.info('select current date is: ' + JSON.stringify(value))
})
Row({space: CommonConstants.SPACE_10}){
Button($r('app.string.cancel_label')).backgroundColor($r('app.color.lightest_primary_color')).width('40%')
.onClick(() => {this.controller.close()})
Button($r('app.string.confirm_label')).backgroundColor($r('app.color.primary_color')).width('40%')
.onClick(() => {
AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime()) // 将日期保存到全局存储
this.controller.close()
})
}
}.padding(CommonConstants.SPACE_10)
}
}
书架页面:
这个页面是进入背单词页面的入口,仅实现了路由跳转功能。
BookIndex.ets 构成书架页面
``
import Header from '../../common/Components/Header'
import Book from '../../viewModel/Book'
import BookCard from './BookCard'
import router from '@ohos.router'
@Component
export default struct BookIndex {
cardMargin_left: number = 20 // 词书卡片的margin
build() {
Flex({direction: FlexDirection.Column}){
Header({text: '书架'})
Divider().margin({top: 10, bottom: 30})
List({space: 25}){
ListItem(){
Flex(){
BookCard({book: new Book('生词本', null, 'CET-4', '无', '用户'), color: '#ff67398c', lightColor: '#ff8864be'}).margin({left: this.cardMargin_left})
.onClick(() => {
router.pushUrl({
url: 'pages/LearningIndex',
params: {}
},
router.RouterMode.Single,
e => {
if(e){
console.log('路由失败>>>' + e.code + '>>>' + e.message)
}
}
)
})
}
}.margin({top: 5})
ListItem(){
Text('四级专区').fontColor('#ff606060')
}.width('100%').backgroundColor('#ffefefef')
ListItem(){
Flex(){
BookCard({book: new Book('四级大纲词汇', null, 'CET-4', '无', '官方'), color: '#ff943341', lightColor: '#ffb85866'}).margin({left: this.cardMargin_left})
BookCard({book: new Book('四级核心词汇', null, 'CET-4', '无', '官方'), color: '#ff2e8120', lightColor: '#ff589f4c'}).margin({left: this.cardMargin_left})
}
}
ListItem(){
Text('六级专区').fontColor('#ff606060')
}.width('100%').backgroundColor('#ffefefef')
ListItem(){
Flex(){
BookCard({book: new Book('六级大纲词汇', null, 'CET-6', '无', '官方'), color: '#ff293f91', lightColor: '#ff4f63ac'}).margin({left: this.cardMargin_left})
BookCard({book: new Book('六级核心词汇', null, 'CET-6', '无', '官方'), color: '#ff196177', lightColor: '#ff448599'}).margin({left: this.cardMargin_left})
BookCard({book: new Book('六级真题词汇', null, 'CET-6', '无', '官方'), color: '#ff286e3c', lightColor: '#ff529164'}).margin({left: this.cardMargin_left})
}
}
ListItem(){
Text('导入').fontColor('#ff606060')
}.width('100%').backgroundColor('#ffefefef')
ListItem(){
Flex(){
BookCard({book: new Book('骑士专用词汇', null, 'OTHER', '无', '用户'), color: '#ff323232', lightColor: '#ff5a5a5a'}).margin({left: this.cardMargin_left})
}
}
ListItem(){
Text('.')
}
}.width('100%')
.layoutWeight(1)
.alignListItem(ListItemAlign.Center)
}.width('96%').height('90%')
.backgroundColor(Color.White)
// .opacity(0.7).backdropBlur(30) // 毛玻璃
.shadow({radius: 5, color: $r('app.color.light_primary_color')}).borderRadius(10)
}
}
BookCard.ets 书架里的词书卡片
``
import Book from '../../viewModel/Book'
@Component
export default struct BookCard {
private book: Book
private color: string
private lightColor: string
build() {
Flex({direction: FlexDirection.Column}){
Text(this.book.type).fontSize(25)
Flex({direction: FlexDirection.Column}){
Text(this.book.bookName).fontSize(40).fontColor(Color.White).margin({top: 10})
Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){
Text()
}.backgroundColor(this.lightColor).height('68%')
}.backgroundColor(this.color).width('100%').borderRadius(10)
}.backgroundImage(this.book.bookBackground).width('30%').height('60%').borderRadius(10)
.shadow({radius: 5, color: $r('app.color.light_primary_color')})
}
}
选项页面
并没有实现任何功能的页面。
UserIndex.ets 构成选项页面
``
import Header from '../../common/Components/Header'
import CommonUserCard from './CommonUserCard'
import WordBoxCard from './WordBoxCard'
@Component
export default struct UserIndex {
build() {
Flex({direction: FlexDirection.Column}){
Header({text: '选项'})
Divider().margin({top: 10, bottom: 30})
Flex({justifyContent: FlexAlign.SpaceEvenly, }){
CommonUserCard({text: '迪莫骑士', icon: $r('app.media.userIcon')})
WordBoxCard({text: '词书工具箱', icon: $r('app.media.wordb')})
}
}.width('96%').height('90%').backgroundColor(Color.White)
// .opacity(0.7).backdropBlur(30) // 毛玻璃
.shadow({radius: 5, color: $r('app.color.light_primary_color')}).borderRadius(10)
}
}
CommonUserCard.ets 左卡片
``
@Component
export default struct CommonUserCard {
private text: ResourceStr
private icon: ResourceStr
build() {
Flex({direction: FlexDirection.Column}){
Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){
Image(this.icon).fillColor($r('app.color.primary_color')).height('95%')
.interpolation(ImageInterpolation.High)
Text(this.text).fontSize(30)
}.height('20%')
Flex({direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceEvenly}){
Row(){
// 资料卡按钮
Column(){
Image($r('app.media.acst')).fillColor(Color.White).width('80%')
Blank()
Text('信息设置').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ffb36068').borderRadius(10)
// 资料卡按钮
Column(){
Image($r('app.media.acst')).fillColor(Color.White).width('80%')
Blank()
Text('目标设置').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ff6667ab').borderRadius(10)
}.width('100%').justifyContent(FlexAlign.SpaceEvenly)
Row(){
// 资料卡按钮
Column(){
Image($r('app.media.acst')).fillColor(Color.White).width('80%')
Blank()
Text('记录查看').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ff3f7070').borderRadius(10)
// 资料卡按钮
Column(){
Image($r('app.media.acst')).fillColor(Color.White).width('80%')
Blank()
Text('清空记录').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ff6a4b80').borderRadius(10)
}.width('100%').justifyContent(FlexAlign.SpaceEvenly)
}.backgroundColor($r('app.color.primary_color')).width('100%').borderRadius(10)
}.width('40%').height('80%')
.shadow({radius: 5, color: $r('app.color.light_primary_color')}).borderRadius(10).margin({top: 15})
}
}
WordBoxCard.ets 右卡片
``
@Component
export default struct WordBoxCard {
private text: ResourceStr
private icon: ResourceStr
build() {
Flex({direction: FlexDirection.Column}){
Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}){
Image(this.icon).fillColor($r('app.color.primary_color')).height('95%')
.interpolation(ImageInterpolation.High)
Text(this.text).fontSize(30)
}.height('20%')
Flex({direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceEvenly}){
Row(){
// 资料卡按钮
Column(){
Image($r('app.media.addword')).fillColor(Color.White).width('78%')
Blank()
Text('增加生词').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ffb36068').borderRadius(10)
.onClick(() => {
})
// 资料卡按钮
Column(){
Image($r('app.media.addword')).fillColor(Color.White).width('78%')
Blank()
Text('删除生词').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ff6667ab').borderRadius(10)
}.width('100%').justifyContent(FlexAlign.SpaceEvenly)
Row(){
// 资料卡按钮
Column(){
Image($r('app.media.addword')).fillColor(Color.White).width('78%')
Blank()
Text('导入词书').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ff3f7070').borderRadius(10)
// 资料卡按钮
Column(){
Image($r('app.media.addword')).fillColor(Color.White).width('78%')
Blank()
Text('删除词书').fontColor(Color.White).fontSize(30)
}.width('45%').height('45%').backgroundColor('#ff6a4b80').borderRadius(10)
}.width('100%').justifyContent(FlexAlign.SpaceEvenly)
}.backgroundColor($r('app.color.primary_color')).width('100%').borderRadius(10)
}.width('40%').height('80%')
.shadow({radius: 5, color: $r('app.color.light_primary_color')}).borderRadius(10).margin({top: 15})
}
}