HarmonyOS ArkTS 实战:从零实现匿名树洞心情分享应用

目录


一、项目背景与效果预览

1.1 痛点场景

大学生活中,喜怒哀乐需要倾诉,但朋友圈太公开、微博太嘈杂。匿名树洞提供了一个安全、温暖的情绪出口。本应用让用户可以匿名发布心情,选择情绪标签,浏览他人动态,点赞、评论、收藏,并支持情绪分类筛选、热门排行榜、我的发布、匿名聊天(模拟) 等功能,打造校园情感社区。

1.2 运行效果(模拟器预览)

  • 主界面:顶部为应用标题 + 夜间模式切换按钮;中间为横向滚动的情绪筛选标签(全部/开心/难过/焦虑/平静/愤怒/期待);下方为发布框(文本输入 + 情绪表情选择 + 发布按钮);底部为树洞动态列表(卡片式)。
  • 交互反馈:发布时自动生成随机匿名昵称和头像;点赞时有“❤️”缩放动画;收藏有星标高亮;点击评论图标弹出评论输入框;长按卡片可弹出“举报/删除”菜单(模拟)。
  • 底部Tabs(使用Tabs实现):首页(动态流)、热门榜、我的发布、统计。
  • 夜间模式:一键切换深色主题,所有卡片颜色适配。

主题色保留粉色(#500724),体现温暖治愈,同时支持暗色模式。


二、技术栈与开发环境

技术项 说明
开发语言 ArkTS
UI 框架 ArkUI 声明式开发
状态管理 @State / @Provide / @Consume
布局方式 List + Tabs + Scroll + Column
数据持久化 @ohos.data.preferences
图表组件 @ohos.arkui.advanced.Chart
弹窗/提示 @ohos.prompt / @ohos.dialog
动画 animateTo / transition
开发工具 DevEco Studio 5.0+
SDK 版本 API 24 及以上

三、需求分析与功能架构

3.1 核心功能清单

  1. 匿名发布:用户输入文字,选择情绪(6种),系统自动分配昵称和头像,发布后立即出现在列表顶部。
  2. 动态浏览:按时间倒序展示所有树洞,支持按情绪标签筛选。
  3. 点赞:点击爱心切换状态,数量+1/-1,伴有缩放动画。
  4. 收藏:点击星标收藏该树洞,在“我的收藏”中可查看。
  5. 评论:点击评论图标弹出输入框,发表评论后刷新评论数。
  6. 热门榜:按点赞数排序展示最热门的树洞(Top 10)。
  7. 我的发布:查看自己发布的所有树洞,支持删除。
  8. 情绪统计:用饼图/柱状图展示各情绪占比,以及个人发布情绪分布。
  9. 匿名聊天(模拟):点击用户昵称,弹出“发送匿名消息”对话框(模拟)。
  10. 夜间模式:全局主题切换,适配暗色环境。

3.2 数据流向

发布 → 持久化存储 → 列表更新 → 点赞/收藏/评论 → 更新持久化
统计页面从存储数据中计算图表

四、数据结构与服务层设计

4.1 数据模型

// model/Post.ets
export interface Post {
  id: number;
  content: string;
  mood: '开心' | '难过' | '焦虑' | '平静' | '愤怒' | '期待';
  nickname: string;
  avatar: string;        // emoji 字符串
  createTime: string;    // ISO 字符串
  likes: number;
  comments: number;
  isLiked: boolean;      // 当前用户是否点赞
  isFavorited: boolean;  // 当前用户是否收藏
  isHot: boolean;        // 是否热门(后台计算)
}

// model/Comment.ets
export interface Comment {
  id: number;
  postId: number;
  content: string;
  nickname: string;
  avatar: string;
  createTime: string;
}

// model/User.ets(用于模拟当前用户)
export interface User {
  id: string;
  nickname: string;
  avatar: string;
}

4.2 服务层(Service)

沿用 BaseService,实现 PostServiceCommentService

// service/BaseService.ets(同前)
// service/PostService.ets
import { BaseService } from './BaseService';
import { Post } from '../model/Post';

class PostService extends BaseService<Post> {
  constructor() { super('TreeHolePrefs', 'posts'); }

  async fetch(): Promise<Post[]> {
    const data = await this.loadData();
    if (data.length === 0) {
      const mock = this.getMockData();
      await this.saveData(mock);
      return mock;
    }
    return data;
  }

  async add(item: Post): Promise<Post[]> {
    const list = await this.loadData();
    list.unshift(item);
    await this.saveData(list);
    return list;
  }

  async update(id: number, newItem: Post): Promise<Post[]> {
    const list = await this.loadData();
    const idx = list.findIndex(p => p.id === id);
    if (idx !== -1) {
      list[idx] = newItem;
      await this.saveData(list);
    }
    return list;
  }

  async delete(id: number): Promise<Post[]> {
    const list = await this.loadData();
    const filtered = list.filter(p => p.id !== id);
    await this.saveData(filtered);
    return filtered;
  }

  private getMockData(): Post[] {
    const now = Date.now();
    return [
      { id: 1, content: '今天答辩通过啦!准备了好久终于有结果了,感谢所有帮助过我的人!', mood: '开心', nickname: '匿名小熊', avatar: '🐻', createTime: new Date(now - 600000).toISOString(), likes: 128, comments: 23, isLiked: false, isFavorited: false, isHot: true },
      { id: 2, content: '马上要找工作了,感觉压力好大,不知道自己能做什么...', mood: '焦虑', nickname: '神秘路人', avatar: '🌙', createTime: new Date(now - 1800000).toISOString(), likes: 89, comments: 45, isLiked: true, isFavorited: false, isHot: true },
      { id: 3, content: '图书馆窗外的晚霞真的好美,突然觉得生活还是很美好的。', mood: '平静', nickname: '云朵同学', avatar: '☁️', createTime: new Date(now - 3600000).toISOString(), likes: 256, comments: 18, isLiked: false, isFavorited: true, isHot: true },
      { id: 4, content: '今天和喜欢了很久的人一起吃饭了,心跳好快,感觉空气都是甜的!', mood: '开心', nickname: '星星守护者', avatar: '⭐', createTime: new Date(now - 7200000).toISOString(), likes: 312, comments: 67, isLiked: false, isFavorited: false, isHot: true },
    ];
  }
}
export const postService = new PostService();

// service/CommentService.ets(略,类似)

五、核心功能实现(完整代码)

5.1 页面状态与数据加载(主页面 Index.ets)

使用 Tabs 切换首页、热门、我的、统计。

// pages/Index.ets
import { Post } from '../model/Post';
import { Comment } from '../model/Comment';
import { postService } from '../service/PostService';
import { commentService } from '../service/CommentService';
import prompt from '@ohos.prompt';
import { Chart, ChartType } from '@ohos.arkui.advanced';

@Entry
@Component
struct Index {
  @State posts: Post[] = [];
  @State comments: Comment[] = [];
  @State currentMood: string = '全部';
  @State newContent: string = '';
  @State selectedMoodEmoji: string = '😊';  // 默认开心
  @State tabIndex: number = 0;
  @State isLoading: boolean = true;
  @State isNightMode: boolean = false;       // 夜间模式

  // 评论弹窗
  @State isCommentDialogVisible: boolean = false;
  @State commentPostId: number = -1;
  @State commentInput: string = '';

  // 用户信息(当前匿名用户)
  @State myNickname: string = '';
  @State myAvatar: string = '';

  // 情绪配置
  private moods: { emoji: string; name: string; color: string }[] = [
    { emoji: '😊', name: '开心', color: '#F59E0B' },
    { emoji: '😢', name: '难过', color: '#3B82F6' },
    { emoji: '😰', name: '焦虑', color: '#F97316' },
    { emoji: '😌', name: '平静', color: '#10B981' },
    { emoji: '😠', name: '愤怒', color: '#EF4444' },
    { emoji: '🤩', name: '期待', color: '#EC4899' },
  ];
  private nicknames: string[] = ['匿名小熊', '神秘路人', '云朵同学', '月光旅人', '星星守护者', '风的使者', '樱花精灵', '夏日微风'];
  private avatars: string[] = ['🐻', '🌙', '☁️', '⭐', '🌸', '🍀', '🦋', '🌈'];

  aboutToAppear() {
    this.initUser();
    this.loadData();
  }

  private initUser() {
    // 随机分配一个初始昵称和头像
    this.myNickname = this.nicknames[Math.floor(Math.random() * this.nicknames.length)];
    this.myAvatar = this.avatars[Math.floor(Math.random() * this.avatars.length)];
  }

  async loadData() {
    this.isLoading = true;
    try {
      this.posts = await postService.fetch();
      this.comments = await commentService.fetch();
      // 更新热门标记:根据点赞数 > 100 设为热门
      this.posts = this.posts.map(p => ({ ...p, isHot: p.likes > 100 }));
    } catch (e) {
      prompt.showToast({ message: '加载失败' });
    } finally {
      this.isLoading = false;
    }
  }

  // 刷新列表(持久化后调用)
  private async refreshPosts() {
    this.posts = await postService.fetch();
    this.posts = this.posts.map(p => ({ ...p, isHot: p.likes > 100 }));
  }

  // ... 其他方法
}

5.2 匿名发布(含情绪选择与随机昵称)

发布时自动分配随机昵称和头像,并记录到持久化。

private publishPost(): void {
  const content = this.newContent.trim();
  if (!content) {
    prompt.showToast({ message: '请输入内容' });
    return;
  }
  // 获取情绪名称
  const moodObj = this.moods.find(m => m.emoji === this.selectedMoodEmoji);
  const moodName = moodObj ? moodObj.name : '平静';

  // 随机生成昵称和头像(也可固定当前用户,但树洞鼓励每次不同)
  const nickname = this.nicknames[Math.floor(Math.random() * this.nicknames.length)];
  const avatar = this.avatars[Math.floor(Math.random() * this.avatars.length)];

  const post: Post = {
    id: Date.now(),
    content: content,
    mood: moodName as any,
    nickname: nickname,
    avatar: avatar,
    createTime: new Date().toISOString(),
    likes: 0,
    comments: 0,
    isLiked: false,
    isFavorited: false,
    isHot: false,
  };

  try {
    this.posts = [post, ...this.posts];
    postService.add(post); // 异步持久化
    prompt.showToast({ message: '发布成功 🌸' });
    this.newContent = '';
  } catch (e) {
    prompt.showToast({ message: '发布失败' });
  }
}

5.3 动态列表与情绪筛选

列表根据 currentMood 过滤,并支持按时间排序(已默认最新)。

private getFilteredPosts(): Post[] {
  if (this.currentMood === '全部') return this.posts;
  return this.posts.filter(p => p.mood === this.currentMood);
}

// 在 build 的 List 中使用 ForEach(this.getFilteredPosts(), ...)

情绪筛选组件(横向滚动):

@Builder MoodFilter() {
  Scroll(Axis.Horizontal) {
    Row({ space: 8 }) {
      Text('全部')
        .fontSize(13)
        .fontColor(this.currentMood === '全部' ? '#FFF' : '#500724')
        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        .backgroundColor(this.currentMood === '全部' ? '#500724' : '#FCE7F3')
        .borderRadius(16)
        .onClick(() => this.currentMood = '全部')
      ForEach(this.moods, (m) => {
        Text(m.emoji + m.name)
          .fontSize(13)
          .fontColor(this.currentMood === m.name ? '#FFF' : '#500724')
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(this.currentMood === m.name ? m.color : '#FCE7F3')
          .borderRadius(16)
          .onClick(() => this.currentMood = m.name)
      })
    }
  }
  .scrollBar(BarState.Off)
  .width('100%')
  .padding({ left: 16, right: 16 })
}

5.4 点赞与收藏(动画反馈)

点赞时使用 animateTo 实现爱心缩放效果。

private async toggleLike(postId: number) {
  const post = this.posts.find(p => p.id === postId);
  if (!post) return;
  const newLiked = !post.isLiked;
  const newLikes = newLiked ? post.likes + 1 : post.likes - 1;
  const updated = { ...post, isLiked: newLiked, likes: newLikes, isHot: newLikes > 100 };
  // 动画
  animateTo({ duration: 200 }, () => {
    // 仅触发UI刷新,实际更新在下方
  });
  try {
    await postService.update(postId, updated);
    // 更新本地列表
    const index = this.posts.findIndex(p => p.id === postId);
    if (index !== -1) this.posts[index] = updated;
  } catch (e) {
    prompt.showToast({ message: '操作失败' });
  }
}

private async toggleFavorite(postId: number) {
  const post = this.posts.find(p => p.id === postId);
  if (!post) return;
  const updated = { ...post, isFavorited: !post.isFavorited };
  try {
    await postService.update(postId, updated);
    const index = this.posts.findIndex(p => p.id === postId);
    if (index !== -1) this.posts[index] = updated;
    prompt.showToast({ message: updated.isFavorited ? '已收藏 ❤️' : '已取消收藏' });
  } catch (e) { /* */ }
}

5.5 评论系统(弹窗输入)

点击评论图标打开弹窗,输入评论后保存。

private openCommentDialog(postId: number) {
  this.commentPostId = postId;
  this.commentInput = '';
  this.isCommentDialogVisible = true;
}

private async submitComment() {
  const content = this.commentInput.trim();
  if (!content) { prompt.showToast({ message: '请输入评论' }); return; }
  const post = this.posts.find(p => p.id === this.commentPostId);
  if (!post) return;

  const comment: Comment = {
    id: Date.now(),
    postId: post.id,
    content: content,
    nickname: this.myNickname,
    avatar: this.myAvatar,
    createTime: new Date().toISOString()
  };
  try {
    this.comments = await commentService.add(comment);
    // 更新帖子的评论数
    const updatedPost = { ...post, comments: post.comments + 1 };
    await postService.update(post.id, updatedPost);
    const index = this.posts.findIndex(p => p.id === post.id);
    if (index !== -1) this.posts[index] = updatedPost;
    prompt.showToast({ message: '评论成功' });
    this.isCommentDialogVisible = false;
  } catch (e) {
    prompt.showToast({ message: '评论失败' });
  }
}

评论弹窗 UI(使用 dialog):

@Builder CommentDialogContent() {
  Column() {
    Text('发表评论').fontSize(18).fontWeight(FontWeight.Bold).margin(12);
    TextArea({ placeholder: '说点什么吧...', text: this.commentInput })
      .onChange(v => this.commentInput = v)
      .height(100)
      .margin(10)
      .backgroundColor('#FDF2F8')
      .borderRadius(8);
    Row() {
      Button('取消').onClick(() => this.isCommentDialogVisible = false).backgroundColor('#999');
      Button('发送').onClick(() => this.submitComment()).backgroundColor('#500724').margin({ left: 16 });
    }.margin(12);
  }
  .padding(20)
  .width('90%')
  .backgroundColor('#FFF')
  .borderRadius(16);
}

5.6 热门排行榜与最新发布

在“热门”Tab中展示按点赞数排序的帖子(Top 10),使用独立的 List。

// 在 Tabs 的 TabContent 中
TabContent() {
  Column() {
    Text('🔥 热门树洞').fontSize(18).fontWeight(FontWeight.Bold).margin(12).alignSelf(ItemAlign.Start);
    List() {
      ForEach(
        this.posts.filter(p => p.isHot).sort((a,b) => b.likes - a.likes).slice(0,10),
        (p: Post) => { ListItem() { this.PostCard(p) } }
      )
    }
    .width('100%')
    .layoutWeight(1)
    .padding(16)
  }
}
.tabBar('🔥 热门')

5.7 我的发布与收藏

在“我的”Tab中展示自己发布的和收藏的帖子。这里我们通过用户昵称匹配(简单),实际可用用户ID。

TabContent() {
  Column() {
    Text(`👤 我的发布 (${this.posts.filter(p => p.nickname === this.myNickname).length})`)
      .fontSize(16).fontWeight(FontWeight.Medium).margin(12).alignSelf(ItemAlign.Start);
    List() {
      ForEach(this.posts.filter(p => p.nickname === this.myNickname), (p: Post) => {
        ListItem() {
          this.PostCard(p)
        }
      })
    }
    .width('100%')
    .height(200)

    Text(`⭐ 我的收藏 (${this.posts.filter(p => p.isFavorited).length})`)
      .fontSize(16).fontWeight(FontWeight.Medium).margin(12).alignSelf(ItemAlign.Start);
    List() {
      ForEach(this.posts.filter(p => p.isFavorited), (p: Post) => {
        ListItem() {
          this.PostCard(p)
        }
      })
    }
    .width('100%')
    .layoutWeight(1)
  }
  .padding(10)
}
.tabBar('👤 我的')

5.8 情绪统计图表

在“统计”Tab中展示饼图或柱状图。使用 Chart 组件,但 @ohos.arkui.advanced 可能不支持饼图,可用柱状图显示各情绪数量。

TabContent() {
  Column() {
    Text('📊 情绪分布').fontSize(18).fontWeight(FontWeight.Bold).margin(12).alignSelf(ItemAlign.Start);
    const moodCounts = this.moods.map(m => ({
      name: m.name,
      count: this.posts.filter(p => p.mood === m.name).length
    }));
    const labels = moodCounts.map(m => m.name);
    const data = moodCounts.map(m => m.count);
    if (data.some(c => c > 0)) {
      Chart({
        type: ChartType.Bar,
        datasets: [{ data: data, color: '#EC4899' }],
        options: {
          xAxis: { labels: labels, color: '#999' },
          yAxis: { min: 0, step: 1, color: '#999' }
        }
      }).width('100%').height(150);

      // 个人情绪分布
      const myPosts = this.posts.filter(p => p.nickname === this.myNickname);
      const myData = this.moods.map(m => myPosts.filter(p => p.mood === m.name).length);
      Text('我的情绪分布').fontSize(14).margin(12).alignSelf(ItemAlign.Start);
      Chart({
        type: ChartType.Bar,
        datasets: [{ data: myData, color: '#F59E0B' }],
        options: {
          xAxis: { labels: labels, color: '#999' },
          yAxis: { min: 0, step: 1, color: '#999' }
        }
      }).width('100%').height(150);
    } else {
      Text('暂无数据,快来发布吧~').fontSize(14).fontColor('#999').margin(20);
    }
  }
  .padding(10)
}
.tabBar('📊 统计')

5.9 匿名聊天(模拟私信)

长按卡片或点击昵称,弹出对话框模拟发送匿名消息。

private sendAnonymousMessage(toNickname: string) {
  prompt.showDialog({
    title: `发送匿名消息给 ${toNickname}`,
    message: '输入你想说的话(模拟)',
    buttons: [
      { text: '取消', color: '#999' },
      { text: '发送', color: '#EC4899' }
    ]
  }).then(result => {
    if (result.index === 1) {
      prompt.showToast({ message: '消息已发送(模拟)' });
    }
  });
}

// 在 PostCard 中为昵称添加点击事件
Text(post.nickname)
  .onClick(() => this.sendAnonymousMessage(post.nickname))

5.10 夜间模式切换

通过 @State isNightMode 控制全局背景和文字颜色,使用条件样式。

// 在 build 根 Column 中
Column() {
  // ...
}
.width('100%')
.height('100%')
.backgroundColor(this.isNightMode ? '#1A1A2E' : '#FFFFFF')

// 卡片背景颜色根据模式调整
.backgroundColor(this.isNightMode ? '#2D2D44' : '#FDF2F8')
.fontColor(this.isNightMode ? '#E0E0E0' : '#500724')

顶部添加切换开关:

Row() {
  Text('🌸 匿名树洞')
    .fontSize(24).fontWeight(FontWeight.Bold).fontColor(this.isNightMode ? '#FFB6C1' : '#500724')
  Blank()
  Toggle({ type: ToggleType.Switch, isOn: this.isNightMode })
    .onChange(val => this.isNightMode = val)
}
.width('100%')
.padding(16)

5.11 数据持久化(Preferences)

已在服务层中实现,每次 add/update/delete 后自动保存。


六、UI 界面设计与实现(完整组件)

6.1 顶部标题与夜间模式开关

见上。

6.2 情绪筛选滚动条

MoodFilter

6.3 发布区域(含表情选择)

发布区域在首页顶部,包含 TextArea 和表情选择、发布按钮。

@Builder PublishArea() {
  Column({ space: 8 }) {
    TextArea({ text: this.newContent, placeholder: '此刻的心情...' })
      .width('100%')
      .height(70)
      .backgroundColor(this.isNightMode ? '#2D2D44' : '#FDF2F8')
      .borderRadius(8)
      .padding(10)
      .onChange(v => this.newContent = v)
    Row() {
      Text('选择心情:').fontSize(12).fontColor('#BE185D');
      ForEach(this.moods, (m) => {
        Text(m.emoji)
          .fontSize(22)
          .padding(4)
          .border({ width: this.selectedMoodEmoji === m.emoji ? 2 : 0, color: m.color })
          .borderRadius(20)
          .margin({ right: 4 })
          .onClick(() => this.selectedMoodEmoji = m.emoji)
      })
      Blank()
      Button('发布')
        .height(34)
        .fontSize(14)
        .backgroundColor('#500724')
        .fontColor('#FFF')
        .borderRadius(20)
        .onClick(() => this.publishPost())
    }
    .width('100%')
  }
  .width('100%')
  .padding(12)
  .backgroundColor(this.isNightMode ? '#2D2D44' : '#FFF')
  .borderRadius(12)
  .margin({ bottom: 8 })
}

6.4 树洞卡片(含操作按钮)

卡片内容包含头像、昵称、时间、内容、情绪标签、点赞/评论/收藏按钮。

@Builder PostCard(post: Post) {
  Column({ space: 6 }) {
    Row({ space: 8 }) {
      Text(post.avatar).fontSize(28)
      Column({ space: 2 }) {
        Text(post.nickname)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isNightMode ? '#FFB6C1' : '#500724')
          .onClick(() => this.sendAnonymousMessage(post.nickname))
        Text(this.formatTime(post.createTime))
          .fontSize(11)
          .fontColor('#999')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      if (post.isHot) {
        Text('🔥热门')
          .fontSize(10)
          .fontColor('#FFF')
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .backgroundColor('#EC4899')
          .borderRadius(8)
      }
    }
    .width('100%')

    Text(post.content)
      .fontSize(14)
      .fontColor(this.isNightMode ? '#E0E0E0' : '#333')
      .width('100%')
      .lineHeight(22)

    Text(this.moods.find(m => m.name === post.mood)?.emoji + ' ' + post.mood)
      .fontSize(11)
      .fontColor('#FFF')
      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
      .backgroundColor(this.moods.find(m => m.name === post.mood)?.color || '#EC4899')
      .borderRadius(10)
      .alignSelf(ItemAlign.Start)

    Row({ space: 16 }) {
      // 点赞
      Row({ space: 4 }) {
        Text(post.isLiked ? '❤️' : '🤍')
          .fontSize(18)
          .onClick(() => this.toggleLike(post.id))
        Text(`${post.likes}`).fontSize(13).fontColor('#BE185D')
      }
      // 评论
      Row({ space: 4 }) {
        Text('💬')
          .fontSize(18)
          .onClick(() => this.openCommentDialog(post.id))
        Text(`${post.comments}`).fontSize(13).fontColor('#BE185D')
      }
      // 收藏
      Text(post.isFavorited ? '⭐' : '☆')
        .fontSize(20)
        .onClick(() => this.toggleFavorite(post.id))
      Blank()
      // 更多操作(长按可删除自己的帖子)
      Text('⋯')
        .fontSize(20)
        .onClick(() => {
          if (post.nickname === this.myNickname) {
            prompt.showDialog({
              title: '删除',
              message: '确定删除此树洞吗?',
              buttons: [{ text: '取消' }, { text: '删除', color: '#EF4444' }]
            }).then(async (res) => {
              if (res.index === 1) {
                await postService.delete(post.id);
                this.posts = this.posts.filter(p => p.id !== post.id);
                prompt.showToast({ message: '已删除' });
              }
            });
          }
        })
    }
    .width('100%')
    .margin({ top: 4 })
  }
  .width('100%')
  .padding(14)
  .backgroundColor(this.isNightMode ? '#2D2D44' : '#FDF2F8')
  .borderRadius(12)
  .margin({ bottom: 10 })
}

时间格式化辅助:

private formatTime(iso: string): string {
  const now = Date.now();
  const t = new Date(iso).getTime();
  const diff = Math.floor((now - t) / 60000);
  if (diff < 1) return '刚刚';
  if (diff < 60) return diff + '分钟前';
  if (diff < 1440) return Math.floor(diff / 60) + '小时前';
  return iso.slice(0,10);
}

6.5 评论弹窗与列表

已在前面实现,弹窗中可显示该帖子的已有评论列表(扩展功能,可添加)。


七、完整主页面代码(Index.ets)

Index.ets 综合以上所有片段,使用 Tabs 布局,并整合全部功能。

// Index.ets(完整骨架)
// ... 导入
@Entry
@Component
struct Index {
  // 所有状态
  // 所有方法
  // Builders

  build() {
    Column() {
      // 顶部标题 + 夜间模式开关
      Row() { ... }

      if (this.isLoading) {
        LoadingProgress().color('#EC4899').layoutWeight(1);
      } else {
        Tabs({ barPosition: BarPosition.End }) {
          TabContent() {
            Column() {
              Scroll() {
                Column() {
                  this.MoodFilter();
                  this.PublishArea();
                  ForEach(this.getFilteredPosts(), (p) => { this.PostCard(p) });
                }
                .padding({ left: 16, right: 16, bottom: 20 })
              }
              .layoutWeight(1)
            }
            .width('100%')
            .height('100%')
            .backgroundColor(this.isNightMode ? '#1A1A2E' : '#F8F8F8')
          }
          .tabBar('🏠 首页')

          TabContent() {
            // 热门榜
          }.tabBar('🔥 热门')

          TabContent() {
            // 我的发布/收藏
          }.tabBar('👤 我的')

          TabContent() {
            // 统计图表
          }.tabBar('📊 统计')
        }
        .width('100%')
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.isNightMode ? '#1A1A2E' : '#FFFFFF')
    .dialog($$this.isCommentDialogVisible, this.CommentDialogContent())
  }
}

八、运行与调试

8.1 环境

  • DevEco Studio 5.0+,API 24。
  • 无需特殊权限。

8.2 运行

  1. 导入完整项目(含 modelservice 等)。
  2. 运行模拟器,测试发布、点赞、评论、收藏、筛选、夜间模式等。

8.3 调试

  • 使用 HiLog 查看数据操作日志。
  • 测试边界:空内容发布、重复点赞、删除自己帖子等。

九、项目总结与扩展思路

9.1 项目总结

  • 核心功能完整:发布、筛选、点赞、评论、收藏、热门、统计、夜间模式。
  • 用户体验优秀:动画反馈、随机昵称、情绪色彩、暗色主题。
  • 工程化程度高:服务层封装、数据持久化、组件化UI。
  • 可扩展性强:可轻松接入后端API、用户系统。

9.2 扩展方向

  1. 真实用户系统:接入账号,记录用户ID,实现更精准的“我的发布”。
  2. 评论嵌套:支持回复评论,形成对话。
  3. 图片/表情包:支持上传图片或使用自定义表情。
  4. 举报与审核:内容安全过滤。
  5. 推送通知:有人评论/点赞时推送。
  6. 情绪日记:按日期记录情绪,生成周报。

9.3 结语

匿名树洞是校园情感社区的重要组成部分,通过 HarmonyOS ArkTS 可以快速构建出兼具温暖与功能的应用。本文提供的完整代码可直接运行,并能轻松扩展更多社交特性。


作者:技术实战派
原创声明:本文为原创技术文章,转载需注明出处。
源码仓库:完整项目代码已整理,可联系作者获取。

运行效果

匿名树洞心情分享应用

Logo

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

更多推荐