心愿池的储蓄管理:心愿清单与分类统计

如果你有心愿目标想攒钱实现,推荐去鸿蒙应用市场搜一下**「心愿池」**,下载体验体验。创建心愿清单、投币储蓄、分类统计,一套走下来对攒钱目标会有更清晰的把控。体验完再回来看这篇文章,你会更清楚心愿管理和分类统计背后是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。

很多人觉得"前端转鸿蒙"应该很容易——都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂

比如:

  • 数据存储:Web的localStorage到了ArkTS变成了@ohos.data.preferences
  • 列表渲染:React的map()变成了ForEach
  • 路由导航:React Router变成了router.pushUrl

接下来这篇文章,我会用"心愿池"的实际开发经历,带你看看心愿清单管理、分类统计分析。


这篇文章聊什么

心愿池的储蓄管理功能,核心要解决两个问题:

  1. 心愿清单:创建和管理多个心愿
  2. 分类统计:按分类分析储蓄数据

第一步:心愿清单页面

@Entry
@Component
struct WishListPage {
  @State wishes: Wish[] = []
  @State filterCategory: string = 'all'

  async aboutToAppear() {
    await this.loadWishes()
  }

  async loadWishes() {
    const store = await preferences.getPreferences(getContext(), 'xinyuanchi_data');
    const stored = await store.get('wishes', '[]') as string;
    this.wishes = JSON.parse(stored);
  }

  get filteredWishes(): Wish[] {
    if (this.filterCategory === 'all') return this.wishes;
    return this.wishes.filter(w => w.category === this.filterCategory);
  }

  get totalSaved(): number {
    return this.wishes.reduce((sum, w) => sum + w.savedAmount, 0);
  }

  get totalTarget(): number {
    return this.wishes.reduce((sum, w) => sum + w.targetAmount, 0);
  }

  build() {
    Column() {
      // 概览
      Row() {
        Column() {
          Text(`¥${this.totalSaved}`)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#A855F7')
          Text('已储蓄')
            .fontSize(12)
            .fontColor('#9CA3AF')
        }
        .layoutWeight(1)

        Column() {
          Text(`${this.wishes.length}`)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#A855F7')
          Text('心愿数')
            .fontSize(12)
            .fontColor('#9CA3AF')
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#1F2937')
      .borderRadius(12)
      .margin({ bottom: 16 })

      // 分类筛选
      Flex({ wrap: FlexWrap.Wrap }) {
        Text('全部')
          .fontSize(12)
          .padding(6)
          .margin(4)
          .borderRadius(6)
          .backgroundColor(this.filterCategory === 'all' ? '#A855F7' : '#374151')
          .onClick(() => { this.filterCategory = 'all' })
        ForEach(WISH_CATEGORIES, (cat) => {
          Text(`${cat.icon} ${cat.name}`)
            .fontSize(12)
            .padding(6)
            .margin(4)
            .borderRadius(6)
            .backgroundColor(this.filterCategory === cat.id ? '#A855F7' : '#374151')
            .onClick(() => { this.filterCategory = cat.id })
        })
      }
      .margin({ bottom: 12 })

      // 心愿列表
      List({ space: 12 }) {
        ForEach(this.filteredWishes, (wish: Wish) => {
          ListItem() {
            Column() {
              Row() {
                Text(this.getCategoryIcon(wish.category))
                  .fontSize(20)
                Text(wish.name)
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .layoutWeight(1)
                  .margin({ left: 8 })
                Text(this.getPriorityLabel(wish.priority))
                  .fontSize(11)
                  .padding(4)
                  .borderRadius(4)
                  .backgroundColor(this.getPriorityColor(wish.priority))
              }
              .width('100%')

              // 进度条
              Column() {
                Row() {
                  Text(`¥${wish.savedAmount}`)
                    .fontSize(14)
                    .fontColor('#A855F7')
                    .layoutWeight(1)
                  Text(`¥${wish.targetAmount}`)
                    .fontSize(14)
                    .fontColor('#9CA3AF')
                }
                .width('100%')

                Stack() {
                  Row()
                    .width('100%')
                    .height(8)
                    .borderRadius(4)
                    .backgroundColor('#374151')
                  Row()
                    .width(`${Math.round(wish.progress * 100)}%`)
                    .height(8)
                    .borderRadius(4)
                    .backgroundColor('#A855F7')
                }
                .width('100%')
                .margin({ top: 4 })
              }
              .margin({ top: 8 })
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#1F2937')
            .borderRadius(12)
            .onClick(() => {
              router.pushUrl({ url: `pages/WishDetail?id=${wish.id}` });
            })
          }
        })
      }
      .layoutWeight(1)

      Button('许个心愿')
        .onClick(() => {
          router.pushUrl({ url: 'pages/AddWish' });
        })
        .width('100%')
        .backgroundColor('#A855F7')
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#111827')
  }

  private getCategoryIcon(category: string): string {
    return WISH_CATEGORIES.find(c => c.id === category)?.icon || '💝';
  }

  private getPriorityLabel(priority: string): string {
    const labels: Record<string, string> = { 'high': '高', 'medium': '中', 'low': '低' };
    return labels[priority] || priority;
  }

  private getPriorityColor(priority: string): string {
    const colors: Record<string, string> = { 'high': '#EF4444', 'medium': '#F59E0B', 'low': '#6B7280' };
    return colors[priority] || '#374151';
  }
}

第二步:分类统计

@Entry
@Component
struct StatsPage {
  @State categoryStats: Record<string, number> = {}
  @State totalSaved: number = 0

  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  async aboutToAppear() {
    await this.loadStats()
  }

  async loadStats() {
    const store = await preferences.getPreferences(getContext(), 'xinyuanchi_data');
    const stored = await store.get('wishes', '[]') as string;
    const wishes: Wish[] = JSON.parse(stored);

    this.totalSaved = wishes.reduce((sum, w) => sum + w.savedAmount, 0);

    this.categoryStats = {};
    wishes.forEach(w => {
      this.categoryStats[w.category] = (this.categoryStats[w.category] || 0) + w.savedAmount;
    });
  }

  build() {
    Column() {
      Text('储蓄统计')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      Text(`累计储蓄 ¥${this.totalSaved}`)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#A855F7')
        .margin({ bottom: 24 })

      // 分类饼图
      Canvas(this.ctx)
        .width(200)
        .height(200)
        .onReady(() => {
          this.drawPieChart()
        })

      // 分类明细
      List({ space: 8 }) {
        ForEach(Object.entries(this.categoryStats), ([cat, amount]) => {
          ListItem() {
            Row() {
              Text(this.getCategoryIcon(cat))
                .fontSize(16)
              Text(this.getCategoryName(cat))
                .fontSize(14)
                .layoutWeight(1)
                .margin({ left: 8 })
              Text(`¥${amount}`)
                .fontSize(14)
                .fontColor('#A855F7')
            }
            .width('100%')
            .padding(8)
            .backgroundColor('#1F2937')
            .borderRadius(8)
          }
        })
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#111827')
  }

  private drawPieChart() {
    const ctx = this.ctx;
    const centerX = 100;
    const centerY = 100;
    const radius = 70;

    const entries = Object.entries(this.categoryStats);
    const total = entries.reduce((sum, [, amount]) => sum + amount, 0);
    if (total === 0) return;

    const colors = ['#A855F7', '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#EC4899', '#14B8A6', '#6B7280'];
    let startAngle = 0;

    entries.forEach(([cat, amount], index) => {
      const sliceAngle = (amount / total) * Math.PI * 2;

      ctx.beginPath();
      ctx.moveTo(centerX, centerY);
      ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
      ctx.closePath();
      ctx.fillStyle = colors[index % colors.length];
      ctx.fill();

      startAngle += sliceAngle;
    });
  }

  private getCategoryIcon(cat: string): string {
    return WISH_CATEGORIES.find(c => c.id === cat)?.icon || '💝';
  }

  private getCategoryName(cat: string): string {
    return WISH_CATEGORIES.find(c => c.id === cat)?.name || cat;
  }
}

总结

这篇文章围绕"心愿池"的储蓄管理功能,讲解了两个核心主题:

  1. 心愿清单:多心愿管理、分类筛选、进度条展示
  2. 分类统计:按分类统计储蓄金额,用饼图可视化

心愿清单的核心是数据过滤和进度计算。分类统计用Canvas饼图展示各分类的储蓄占比。


如果你有心愿目标想攒钱实现,希望这篇文章能帮你理解心愿池背后的管理逻辑。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐