HarmonyOS NEXT 自己设计并开发一款诗词应用

165 阅读4分钟

项目介绍

设计一款学习诗词的app。用作学习鸿蒙开发的练手demo。服务端接口使用公开的API。

项目配置

入口模块配置

项目名称+项目ico+开屏图片

颜色资源和国际化资源配置

  1. 颜色资源:背景色、主要字体颜色或者对比色等
  2. 国际化资源配置

媒体资源

  1. 图标资源
  2. 页面图片资源
  3. ...

通用common配置

常量配置

项目中使用到的常量数据、避免硬编码。

例如:app名称、图片资源路径等

// src/main/ets/common/DxinConstants.ets
/*
 * 整个文件的 常量配置文件
 * */
export default class DxinConstants{
  // 项目名称 百诗斩
  static readonly appName :string = "百诗斩"
  // 引导页 背景图数据
  static readonly bgImageSrc:string [] = [
    "/images/guideBgImage1.jpg",
    "/images/guideBgImage2.jpg",
    "/images/guideBgImage3.jpg",
    // ...
  ]
​
    // 首页引导页停留时长 毫秒
  static readonly guideDuration: number = 1500// 随机获取 一首诗词的 api
  static readonly  getOnePoemApi :string = "https://v1.jinrishici.com/all"// 随机获取一句诗词的 api
  static readonly  getOneContentApi :string = "https://v1.jinrishici.com/all.txt"// ...
}
​

功能函数配置

项目开发中会涉及到多种共用的业务功能。抽取到工具类中进行服用。

// src/main/ets/common/DxinUtils.ets
class DxinUtils {
  // 取随机整数 [min,max)
  getRandomNum(min: number, max: number) {
    return Math.floor(Math.random() * (max-min)) + min;
  }
  
  // ...
}
​
export default new DxinUtils()

模型配置model

src/main/ets/model目录,存放数据模型文件。

诗词类型

// src/main/ets/model/Poem.ets
export default class Poem{
  content:string =  "一句诗词"
  origin:string =  "来源:名称"
  author:string =  "作者:杜甫"
  category:string =  "分类:古诗文-节日-端午节"constructor(content: string, origin: string, author: string, category: string) {
    this.content = content
    this.origin = origin
    this.author = author
    this.category = category
  }
​
}

一级诗词分类

// src/main/ets/model/PoemCategory.ets
import PoemLevel2Category from "./PoemLevel2Category"export default class PoemCategory {
  type: string = "全部"
  typeApi: string = "https://v1.jinrishici.com/all"
  Level2Type: PoemLevel2Category[] = []
​
  constructor(type: string, typeApi: string, Level2Type: PoemLevel2Category[]) {
    this.type = type
    this.typeApi = typeApi
    this.Level2Type = Level2Type
  }
}

二级诗词分类

// src/main/ets/model/PoemLevel2Category.ets
export default class PoemLevel2Category {
  type: string = "二级类型"
  typeApi: string = "https://v1.jinrishici.com/shuqing/sinian"constructor(type: string, typeApi: string) {
    this.type = type
    this.typeApi = typeApi
  }
}

诗词对象数据

// src/main/ets/common/DxinConstants.ets
/*
   * 所有诗词分类 及分类API
   * get
   * */
  static readonly poemCategory: PoemCategory[] = [
    new PoemCategory("抒情", "https://v1.jinrishici.com/shuqing", [
      new PoemLevel2Category("友情", "https://v1.jinrishici.com/shuqing/youqing"),
      new PoemLevel2Category("离别", "https://v1.jinrishici.com/shuqing/libie"),
      new PoemLevel2Category("思念", "https://v1.jinrishici.com/shuqing/sinian"),
      new PoemLevel2Category("思乡", "https://v1.jinrishici.com/shuqing/sixiang"),
      new PoemLevel2Category("伤感", "https://v1.jinrishici.com/shuqing/shanggan"),
      new PoemLevel2Category("孤独", "https://v1.jinrishici.com/shuqing/gudu"),
      new PoemLevel2Category("闺怨", "https://v1.jinrishici.com/shuqing/guiyuan"),
      new PoemLevel2Category("悼亡", "https://v1.jinrishici.com/shuqing/daowang"),
      new PoemLevel2Category("怀古", "https://v1.jinrishici.com/shuqing/huaigu"),
      new PoemLevel2Category("爱国", "https://v1.jinrishici.com/shuqing/aiguo"),
      new PoemLevel2Category("感恩", "https://v1.jinrishici.com/shuqing/ganen")
    ]),
    new PoemCategory("四季", "https://v1.jinrishici.com/siji", [
      new PoemLevel2Category("春天", "https://v1.jinrishici.com/siji/chuntian"),
      new PoemLevel2Category("夏天", "https://v1.jinrishici.com/siji/xiatian"),
      new PoemLevel2Category("秋天", "https://v1.jinrishici.com/siji/qiutian"),
      new PoemLevel2Category("冬天", "https://v1.jinrishici.com/siji/dongtian")
    ]),
    new PoemCategory("山水", "https://v1.jinrishici.com/shanshui", [
      new PoemLevel2Category("庐山", "https://v1.jinrishici.com/shanshui/lushan"),
      new PoemLevel2Category("泰山", "https://v1.jinrishici.com/shanshui/taishan"),
      new PoemLevel2Category("江河", "https://v1.jinrishici.com/shanshui/jianghe"),
      new PoemLevel2Category("长江", "https://v1.jinrishici.com/shanshui/changjiang"),
      new PoemLevel2Category("黄河", "https://v1.jinrishici.com/shanshui/huanghe"),
      new PoemLevel2Category("西湖", "https://v1.jinrishici.com/shanshui/xihu"),
      new PoemLevel2Category("瀑布", "https://v1.jinrishici.com/shanshui/pubu")
    ]),
    new PoemCategory("天气", "https://v1.jinrishici.com/tianqi", [
      new PoemLevel2Category("写风", "https://v1.jinrishici.com/tianqi/xiefeng"),
      new PoemLevel2Category("写云", "https://v1.jinrishici.com/tianqi/xieyun"),
      new PoemLevel2Category("写雨", "https://v1.jinrishici.com/tianqi/xieyu"),
      new PoemLevel2Category("写雪", "https://v1.jinrishici.com/tianqi/xiexue"),
      new PoemLevel2Category("彩虹", "https://v1.jinrishici.com/tianqi/caihong"),
      new PoemLevel2Category("太阳", "https://v1.jinrishici.com/tianqi/taiyang"),
      new PoemLevel2Category("月亮", "https://v1.jinrishici.com/tianqi/yueliang"),
      new PoemLevel2Category("星星", "https://v1.jinrishici.com/tianqi/xingxing")
    ]),
    new PoemCategory("人物", "https://v1.jinrishici.com/renwu", [
      new PoemLevel2Category("女子", "https://v1.jinrishici.com/renwu/nvzi"),
      new PoemLevel2Category("父亲", "https://v1.jinrishici.com/renwu/fuqin"),
      new PoemLevel2Category("母亲", "https://v1.jinrishici.com/renwu/muqin"),
      new PoemLevel2Category("老师", "https://v1.jinrishici.com/renwu/laoshi"),
      new PoemLevel2Category("儿童", "https://v1.jinrishici.com/renwu/ertong")
    ]),
    new PoemCategory("人生", "https://v1.jinrishici.com/rensheng", [
      new PoemLevel2Category("励志", "https://v1.jinrishici.com/rensheng/lizhi"),
      new PoemLevel2Category("哲理", "https://v1.jinrishici.com/rensheng/zheli"),
      new PoemLevel2Category("青春", "https://v1.jinrishici.com/rensheng/qingchun"),
      new PoemLevel2Category("时光", "https://v1.jinrishici.com/rensheng/shiguang"),
      new PoemLevel2Category("梦想", "https://v1.jinrishici.com/rensheng/mengxiang"),
      new PoemLevel2Category("读书", "https://v1.jinrishici.com/rensheng/dushu"),
      new PoemLevel2Category("战争", "https://v1.jinrishici.com/rensheng/zhanzheng")
    ]),
    new PoemCategory("生活", "https://v1.jinrishici.com/shenghuo", [
      new PoemLevel2Category("乡村", "https://v1.jinrishici.com/shenghuo/xiangcun"),
      new PoemLevel2Category("田园", "https://v1.jinrishici.com/shenghuo/tianyuan"),
      new PoemLevel2Category("边塞", "https://v1.jinrishici.com/shenghuo/biansai"),
      new PoemLevel2Category("写桥", "https://v1.jinrishici.com/shenghuo/xieqiao")
    ]),
    new PoemCategory("节日", "https://v1.jinrishici.com/jieri", [
      new PoemLevel2Category("春节", "https://v1.jinrishici.com/jieri/chunjie"),
      new PoemLevel2Category("元宵节", "https://v1.jinrishici.com/jieri/yuanxiaojie"),
      new PoemLevel2Category("寒食节", "https://v1.jinrishici.com/jieri/hanshijie"),
      new PoemLevel2Category("清明节", "https://v1.jinrishici.com/jieri/qingmingjie"),
      new PoemLevel2Category("端午节", "https://v1.jinrishici.com/jieri/duanwujie"),
      new PoemLevel2Category("七夕节", "https://v1.jinrishici.com/jieri/qixijie"),
      new PoemLevel2Category("中秋节", "https://v1.jinrishici.com/jieri/zhongqiujie"),
      new PoemLevel2Category("重阳节", "https://v1.jinrishici.com/jieri/chongyangjie")
    ]),
    new PoemCategory("动物", "https://v1.jinrishici.com/dongwu", [
      new PoemLevel2Category("写鸟", "https://v1.jinrishici.com/dongwu/xieniao"),
      new PoemLevel2Category("写马", "https://v1.jinrishici.com/dongwu/xiema"),
      new PoemLevel2Category("写猫", "https://v1.jinrishici.com/dongwu/xiemao"),
    ]),
    new PoemCategory("植物", "https://v1.jinrishici.com/zhiwu", [
      new PoemLevel2Category("梅花", "https://v1.jinrishici.com/zhiwu/meihua"),
      new PoemLevel2Category("梨花", "https://v1.jinrishici.com/zhiwu/lihua"),
      new PoemLevel2Category("桃花", "https://v1.jinrishici.com/zhiwu/taohua"),
      new PoemLevel2Category("荷花", "https://v1.jinrishici.com/zhiwu/hehua"),
      new PoemLevel2Category("菊花", "https://v1.jinrishici.com/zhiwu/juhua"),
      new PoemLevel2Category("柳树", "https://v1.jinrishici.com/zhiwu/liushu"),
      new PoemLevel2Category("叶子", "https://v1.jinrishici.com/zhiwu/yezi"),
      new PoemLevel2Category("竹子", "https://v1.jinrishici.com/zhiwu/zhuzi"),
    ]),
    new PoemCategory("食物", "https://v1.jinrishici.com/shiwu", [
      new PoemLevel2Category("写酒", "https://v1.jinrishici.com/shiwu/xiejiu"),
      new PoemLevel2Category("写茶", "https://v1.jinrishici.com/shiwu/xiecha"),
      new PoemLevel2Category("荔枝", "https://v1.jinrishici.com/shiwu/lizhi")
    ])
  ]

自定义组件view

src/main/ets/view目录,存放自定义组件文件。

沉浸式设置


页面UI

axure设计稿

动态效果

引导页静态图引导页运行效果图

静态UI

今日诗词:未收藏今日诗词:已收藏个人中心:未收藏个人中心:已收藏诗词大全

引导页

引导页

  1. 网络请求动态获取一句诗词渲染到页面底部
  2. 图片动画:尺寸、圆角、旋转角度
  3. 单次定时器跳转Index页面
// src/main/ets/pages/Guide.ets
// 引导页 X秒后跳转到index页面
import DxinConstants from '../common/DxinConstants'
import DxinUtils from '../common/DxinUtils'
import { http } from '@kit.NetworkKit'
import { router } from '@kit.ArkUI'
​
@Entry
@Component
struct Guide {
​
  // 图片参数
  @State imgAngle: number = 0
  @State imgBorderRadius: number = 30
  @State imgWidth: string = "30%"// 底部诗句
  @State content: string = `劝君莫忘少年志,曾许人间第一流`// 随机数
  index: number = DxinUtils.getRandomNum(0, DxinConstants.bgImageSrc.length)
​
  async aboutToAppear(): Promise<void> {
    // 发送网络请求获取结果。
    let res = await http.createHttp().request(DxinConstants.getOneContentApi)
    this.content = res.result as string
​
    // 动画
    animateTo({duration:DxinConstants.guideDuration}, () => {
      this.imgAngle = 360
      this.imgBorderRadius = 50
      this.imgWidth = "50%"
    })
​
    //  单次定时器  X秒钟后页面跳转
    setTimeout(() => {
      router.replaceUrl({ url: "pages/Index" })
    }, DxinConstants.guideDuration)
  }
​
  build() {
    Column({ space: 30 }) {
      Blank()
      Image($r('app.media.dixin'))
        .width(this.imgWidth)
        .rotate({ angle: this.imgAngle })
        .borderRadius(this.imgBorderRadius)
      Blank()
      Text(this.content)
        .poemTextStyle()
    }
    .width('100%')
    .height('100%')
    .backgroundImage(DxinConstants.bgImageSrc[this.index])
    .backgroundImageSize({ width: "100%", height: "100%" })
    .justifyContent(FlexAlign.End)
  }
}
​
@Extend(Text)
function poemTextStyle() {
  .fontSize(30)
  .fontColor("#ffea0000")
  .width("90%")
  .maxLines(1)
  .textOverflow({ overflow: TextOverflow.MARQUEE })
  .margin({ bottom: 150 })
}
​

Index页面

运行效果图

tabs布局,设计三份TabContent。分别为:今日诗词诗词大全个人中心。定义为自定义子组件,提高代码可读性。

PersistentStorage:持久化存储UI状态。只能在UI中初始化使用。在Index页面预先保存持久化数组。

// src/main/ets/pages/Index.ets
import DxinConstants from '../common/DxinConstants';
import Poem from '../model/Poem';
import AllPoem from '../view/AllPoem';
import DayPoem from '../view/DayPoem';
import PersonalCenter from '../view/PersonalCenter';
​
// 持久化
let arr:Array<Poem> = []
PersistentStorage.persistProp(DxinConstants.poemArrKey,arr)
​
​
@Entry
@Component
struct Index {
  @StorageProp('bottomRectHeight') bottomRectHeight: number = 0;
  @StorageProp('topRectHeight') topRectHeight: number = 0;
  title: string = DxinConstants.appName;
​
  build() {
    Column({ space: 30 }) {
      Text(this.title).fontSize(30)
​
      Tabs({ barPosition: BarPosition.End, index: 0 }) {
        TabContent() {
          DayPoem()
        }
        .tabBar("今日诗词")
​
        TabContent() {
          AllPoem()
        }
        .tabBar("诗词大全")
​
        TabContent() {
          PersonalCenter()
        }
        .tabBar("个人中心")
      }
      .layoutWeight(1)
      .backgroundColor($r('app.color.light_green'))
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.theme_color'))
    // top数值与状态栏区域高度保持一致;bottom数值与导航条区域高度保持一致
    .padding({ top: px2vp(this.topRectHeight), bottom: px2vp(this.bottomRectHeight) })
  }
}

今日诗词

今日诗词:未收藏

  1. 点击诗词区域,访问API更新诗词(添加动画效果)
  2. 收藏/取消收藏功能
  3. 收藏的诗词应全局持久化存储,方便个人中心数据同步。
// src/main/ets/view/DayPoem.ets
/*
 * 首页中 今日诗词
 * */
import DxinConstants from '../common/DxinConstants'
import Poem from '../model/Poem'
import { http } from '@kit.NetworkKit'
import TabContentTopTitle from './TabContentTopTitle'@Component
export default struct DayPoem {
​
  @State poem: Poem = new Poem("玲珑骰子安红豆,入骨相思知不知", "这不是普通的红豆", "帝心", "帝-相思-心")
​
  @State angleVal: number = 0
  @State flag: boolean = false
  @State title:string  = "今日诗词"// 双向绑定全局存储诗词的对象
  @StorageLink(DxinConstants.poemArrKey) @Watch('detectionFlag') collectionPoemArr:Poem[] = []
​
  // 检测到了收藏数组变化  确认当前flag状态是否要更新
  detectionFlag(){
    let findRes =  this.collectionPoemArr.find(item=> item.content === this.poem.content)
    if (findRes) {
      // 没找到 取消收藏
      this.flag = true
    }else {
      this.flag = false
    }
  }
​
  aboutToAppear(): void {
    this.detectionFlag()
  }
​
  build() {
    Column() {
      TabContentTopTitle({title:"今日一斩"})
      Column() {
        Text(this.poem.content)
          .fontColor("#ffbe1111")
          .fontSize(20)
        Text(this.poem.origin)
          .fontColor("#dc0c735b")
          .fontSize(20)
​
        Text(this.poem.author)
          .fontColor("#ffbe1111")
          .fontSize(20)
        Text(this.poem.category)
          .fontColor("#dc0c735b")
          .fontSize(20)
      }
      .corePoemStyle()
      .rotate({ angle: this.angleVal })
      .onClick(() => {
        // 网络请求:访问接口。给我一个新的数据
        http.createHttp().request(DxinConstants.getOnePoemApi, (err, data) => {
          if (err) {
            this.title ="获取一首诗出错了" + err.message
          }else {
            this.poem = JSON.parse(`${data.result}`) as Poem
          }
        })
        animateTo({}, () => {
          this.angleVal = this.angleVal == 0 ? 360 : 0
        })
        //恢复 一下收藏标记
        this.flag = false
      })
​
      Row(){
        Image( $r("app.media.collect") )
          .width(50)
          .fillColor(this.flag ? "#a4ef2c71" : "#d858a6d9")
        Button(this.flag ? "已收藏" : "收藏")
          .backgroundColor(this.flag ? "#a4ef2c71" : "#d858a6d9")
          .width(100)
          .type(ButtonType.Normal)
          .borderRadius(16)
      }
      .width("40%")
      .height(100)
      .justifyContent(FlexAlign.SpaceAround)
      .onClick(() => {
        this.flag = !this.flag
        // 根据收藏状态 存进去或者删掉曾存的数据
        if (this.flag) {
          this.collectionPoemArr.unshift(this.poem)
        } else {
          this.collectionPoemArr =  this.collectionPoemArr.filter((item: Poem) => item.content != this.poem.content)
        }
      })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.SpaceAround)
  }
}
​
@Extend(Column)
function corePoemStyle() {
  .width("90%")
  .height("40%")
  .borderRadius(20)
  .backgroundColor("#bcd4c5c5")
  .justifyContent(FlexAlign.SpaceEvenly)
}
​

个人中心

个人中心:未收藏个人中心:已收藏

现实个人信息:例如收藏过的诗句或者其他个人数据。

// src/main/ets/view/PersonalCenter.ets
import DxinConstants from "../common/DxinConstants";
import Poem from "../model/Poem";
import TabContentTopTitle from "./TabContentTopTitle";
​
@Component
export default struct PersonalCenter {
  // 双向绑定全局存储诗词的对象
  @StorageLink(DxinConstants.poemArrKey) collectionPoemArr: Poem[] = []
​
  @Builder
  del(item1: Poem) {
    Row() {
      Image($r('app.media.del')).width(50)
    }
    .width(70)
    .height(70)
    .borderRadius(35)
    .backgroundColor($r('app.color.light_green'))
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      // 从数组中删除元素 再存回数组中
      this.collectionPoemArr = this.collectionPoemArr.filter((item: Poem) => item.content != item1.content)
    })
  }
​
  build() {
    Column() {
​
      if (this.collectionPoemArr.length === 0) {
        Text("哥哥你还没收藏呢")
          .fontSize(30)
          .fontColor("#ff9d0000")
          .width(30)
      }else{
        Column({space:10}){
          TabContentTopTitle({title:"我的喜欢"})
          List({ space: 10 }) {
            ForEach(this.collectionPoemArr, (item: Poem) => {
              ListItem() {
                CollectionItem({ item })
              }
              .swipeAction({
                start: this.del(item),
                end: this.del(item)
              })
            })
          }
          .scrollBar(BarState.Off)
          .width("95%")
          .layoutWeight(1)
          .divider({ strokeWidth: 1, color: "#ff7e043c" })
        }
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.theme_color'))
  }
}
​
@Component
struct CollectionItem {
  item: Poem = new Poem("", "", "", "")
​
  build() {
    Column({ space: 20 }) {
      Text(this.item.content).fontSize(22).fontColor("#fd048d62")
      Row() {
        Text(this.item.origin).fontSize(20)
          .width(100)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.MARQUEE })
        Text(this.item.author).fontSize(20)
        Text(this.item.category?.split('-')[1]).fontSize(20)
      }
      .width("80%")
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width("100%")
    .height(150)
    .backgroundColor("#2dd4b6b6")
    .justifyContent(FlexAlign.Center)
    .borderRadius(30)
  }
}