HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(三十):【社区分享】本地社区功能——让菜谱从“独享”走向“共享”

HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(三十):【社区分享】本地社区功能------让菜谱从"独享"走向"共享"

摘要:一个人做饭是生活,一群人分享是社区。前面 29 篇中,《灵犀厨房》的菜谱从推荐、收藏到烹饪,都围绕着"我一个人"。但烹饪的乐趣有一半在于分享------把自己拿手的番茄牛腩煲分享出去,看看别人做了什么。本篇为《灵犀厨房》新增"社区广场"------用户可在菜谱详情页一键分享菜谱到社区,其他用户可浏览所有分享,无需后端服务器,纯本地数据库实现。


一、引言:从"一个人"到"一群人"

打开《灵犀厨房》第 29 篇的版本,浏览菜谱、收藏、烹饪------一切围绕"我"。但你有没有想过:隔壁老王做的宫保鸡丁可能比菜谱库里的还正宗?同事小张的低脂健身餐才是减肥必备?

目前的设计无法满足一个基本需求:用户生成内容(UGC)。所有菜谱都来自我们预设的 10 道模拟数据,用户只能消费,不能生产。这既限制了内容生态的扩展,也缺少了社区互动带来的活跃度。

功能现状 用户需求 差距
菜谱来自预设 MockData 用户想分享自己的拿手菜 缺少 UGC 入口
菜谱详情只读 用户想浏览他人的分享 缺少社区展示页
数据在本地 分享需要持久化存储 缺少存储表

🎯 本篇目标 :用 shared_recipes 持久化表(已在第 28 篇预建)和约 130 行新代码,让用户可以分享菜谱到社区广场、浏览所有分享的菜谱。不需要后端服务器,纯本地数据库实现,为后续的云端社区打下基础。


二、功能设计:两个页面,一套数据

复制代码
菜谱详情页                     社区广场
┌──────────────────┐         ┌──────────────────┐
│ 🍳 番茄牛腩煲      │         │ 👤 灵犀大厨         │
│ 📋 食材清单        │         │ 🍳 番茄牛腩煲       │
│ 第1步 焯水        │         │ 食材:牛腩、番茄...  │
│ 第2步 炒香        │ 点击分享 │ 步骤:焯水; 炒香...  │
│ ...               │────────→│ 5/28 14:30         │
│                   │         │                    │
│ [🔄分享] [❤收藏]  │         │ 👤 小明             │
└──────────────────┘         │ 🍳 宫保鸡丁         │
                              │ ...                │
                              └──────────────────┘

交互模型

  1. 用户在菜谱详情页点击分享按钮 → 菜谱信息写入 shared_recipes
  2. 用户在"我的"Tab 进入社区广场 → 从数据库按时间倒序加载所有分享
  3. 点击社区广场中的某条分享 → 进入该菜谱的详情页(复用现有 RecipeDetailPage)

三、数据层:第 28 篇已预建的表

在第 28 篇中,我们已经预建了 shared_recipes 表:

sql 复制代码
CREATE TABLE IF NOT EXISTS shared_recipes (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  user_name   TEXT    NOT NULL,
  recipe_name TEXT    NOT NULL,
  ingredients TEXT,
  steps       TEXT,
  shared_at   INTEGER DEFAULT (strftime('%s','now'))
);

本篇新增的用途:在第 28 篇中,这张表只是"预留字段",没有写入逻辑。本篇将实现完整的 INSERT(分享)和 SELECT(广场加载)操作。

字段 类型 存储格式 用途
user_name TEXT 固定值"灵犀大厨"(后续对接登录系统) 分享者标识
ingredients TEXT 分隔(如"牛腩、番茄、洋葱") 食材展示
steps TEXT ; 分隔(如"焯水:...; 炒香:...") 步骤展示
shared_at INTEGER Unix 时间戳 按时间排序

设计考量 :为什么 ingredientssteps 用分隔符字符串而非 JSON?因为这两个字段在本篇中只用于展示,不需要结构化查询。分隔符字符串可以直接展示(牛腩、番茄、洋葱),不需要 JSON.parsejoin。如果后续需要查询"包含牛腩的所有分享",再迁移到 JSON 不迟。


四、分享按钮:从详情页到数据库的"一键传送"

4.1 入口位置

RecipeDetailPage 底部操作栏中,收藏按钮之前新增橙色分享按钮:

typescript 复制代码
// ★ 分享到社区
Button({ type: ButtonType.Circle }) {
  SymbolGlyph($r('sys.symbol.square_and_arrow_up')).fontSize(16).fontColor([Color.White])
}
.width(38).height(38).backgroundColor('#FF8C5A')
.onClick(() => this.shareToCommunity())

4.2 shareToCommunity 方法

typescript 复制代码
private async shareToCommunity(): Promise<void> {
  try {
    await storeHelper.insert('shared_recipes', {
      user_name: '灵犀大厨',
      recipe_name: this.recipe.name,
      ingredients: this.recipe.ingredients.join('、'),
      steps: this.recipe.steps.join('; '),
      shared_at: Date.now()
    });
    ToastUtil.showToast(this.getUIContext(), '✅ 已分享到社区');
  } catch (err) {
    ToastUtil.showToast(this.getUIContext(), '分享失败,请重试');
  }
}

SQLite RelationalStoreHelper RecipeDetailPage 👤 用户 SQLite RelationalStoreHelper RecipeDetailPage 👤 用户 #mermaid-svg-1o2TAdwm5QvrQolA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1o2TAdwm5QvrQolA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1o2TAdwm5QvrQolA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1o2TAdwm5QvrQolA .error-icon{fill:#552222;}#mermaid-svg-1o2TAdwm5QvrQolA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1o2TAdwm5QvrQolA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1o2TAdwm5QvrQolA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1o2TAdwm5QvrQolA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1o2TAdwm5QvrQolA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1o2TAdwm5QvrQolA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1o2TAdwm5QvrQolA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1o2TAdwm5QvrQolA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1o2TAdwm5QvrQolA .marker.cross{stroke:#333333;}#mermaid-svg-1o2TAdwm5QvrQolA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1o2TAdwm5QvrQolA p{margin:0;}#mermaid-svg-1o2TAdwm5QvrQolA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1o2TAdwm5QvrQolA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1o2TAdwm5QvrQolA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1o2TAdwm5QvrQolA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1o2TAdwm5QvrQolA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1o2TAdwm5QvrQolA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1o2TAdwm5QvrQolA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1o2TAdwm5QvrQolA .sequenceNumber{fill:white;}#mermaid-svg-1o2TAdwm5QvrQolA #sequencenumber{fill:#333;}#mermaid-svg-1o2TAdwm5QvrQolA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1o2TAdwm5QvrQolA .messageText{fill:#333;stroke:none;}#mermaid-svg-1o2TAdwm5QvrQolA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1o2TAdwm5QvrQolA .labelText,#mermaid-svg-1o2TAdwm5QvrQolA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1o2TAdwm5QvrQolA .loopText,#mermaid-svg-1o2TAdwm5QvrQolA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1o2TAdwm5QvrQolA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1o2TAdwm5QvrQolA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1o2TAdwm5QvrQolA .noteText,#mermaid-svg-1o2TAdwm5QvrQolA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1o2TAdwm5QvrQolA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1o2TAdwm5QvrQolA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1o2TAdwm5QvrQolA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1o2TAdwm5QvrQolA .actorPopupMenu{position:absolute;}#mermaid-svg-1o2TAdwm5QvrQolA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1o2TAdwm5QvrQolA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1o2TAdwm5QvrQolA .actor-man circle,#mermaid-svg-1o2TAdwm5QvrQolA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1o2TAdwm5QvrQolA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 点击分享按钮 insert('shared_recipes', {name, ingredients, steps}) INSERT INTO shared_recipes rowId ✅ Toast "已分享到社区"

图一解读 :分享流程极简------用户点击按钮,数据直接写入 SQLite,返回成功提示。没有网络请求、没有队列、没有审核。这不是阉割版,是 MVP(最小可行产品)的哲学:先验证用户是否愿意分享,再考虑内容审核和云端同步。

4.3 设计考量:为什么固定用户名"灵犀大厨"?

当前版本还没有完整的用户登录与身份管理功能,用户名使用固定值"灵犀大厨"占位。当后续完成登录功能对接后,只需将此处替换为 authViewModel.username,即可实现真实用户名分享。


五、社区广场页面:SQLite 的"流式读取"

5.1 CommunityPage.ets

新建页面 entry/src/main/ets/pages/CommunityPage.ets

typescript 复制代码
@Entry
@ComponentV2
struct CommunityPage {
  @Local recipes: SharedRecipe[] = [];
  @Local isLoading: boolean = true;

  async aboutToAppear(): Promise<void> {
    await this.loadSharedRecipes();
  }

  private async loadSharedRecipes(): Promise<void> {
    try {
      const rs = await storeHelper.querySql(
        'SELECT * FROM shared_recipes ORDER BY shared_at DESC LIMIT 50'
      );
      this.recipes = [];
      while (rs.goToNextRow()) {
        this.recipes.push({
          id: rs.getLong(rs.getColumnIndex('id')),
          user_name: rs.getString(rs.getColumnIndex('user_name')),
          recipe_name: rs.getString(rs.getColumnIndex('recipe_name')),
          ingredients: rs.getString(rs.getColumnIndex('ingredients')),
          steps: rs.getString(rs.getColumnIndex('steps')),
          shared_at: rs.getLong(rs.getColumnIndex('shared_at'))
        });
      }
      rs.close();
      this.isLoading = false;
    } catch (err) {
      console.error('[Community] 加载失败:', JSON.stringify(err));
      this.isLoading = false;
    }
  }
}

5.2 UI 布局

每个 ListItem 展示:

复制代码
┌──────────────────────────────────┐
│ 👤 灵犀大厨          5/28 14:30 │
│ 🍳 番茄牛腩煲                    │
│ 食材:牛腩、番茄、洋葱、胡萝卜... │
│ 步骤:焯水:牛腩切块...; 炒香...  │
└──────────────────────────────────┘
  • 分享者名 + 时间戳在顶部
  • 菜谱名用主题色高亮
  • 食材和步骤各占两行,超出省略
  • 卡片背景为 $r('app.color.bg_card'),自动适配深色模式

5.3 路由注册

需在 main_pages.json 中添加路由:

json 复制代码
{ "src": ["pages/CommunityPage"] }

5.4 入口位置

在"我的"Tab 中新增社区广场入口:

typescript 复制代码
// MainContainer.ets → ProfileTabContent
Row() {
  Row({ space: 10 }) {
    Text('🌐').fontSize(18)
    Text('社区广场').fontSize(15).fontColor('#333')
  }
  Blank()
  SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(14).fontColor(['#CCC'])
}
.onClick(() => {
  this.getUIContext().getRouter().pushUrl({ url: 'pages/CommunityPage' })
})

六、代码交付清单

文件 新增/修改 行数 说明
RecipeDetailPage.ets 修改 +15 新增分享按钮 + shareToCommunity() 方法
CommunityPage.ets 新文件 +105 社区广场页面
MainContainer.ets 修改 +10 "我的"Tab 新增社区广场入口
main_pages.json 修改 +1 新增路由注册
RelationalStoreHelper.ets 无需修改 0 shared_recipes 表已在第 28 篇预建

七、设计决策

决策 选择 理由
纯本地数据库 不依赖后端 当前阶段无需服务器;后续可扩展为云端同步
ingredients/steps 用分隔符字符串 分隔食材,; 分隔步骤 只展示不查询,分隔符比 JSON 更直观
固定用户名"灵犀大厨" 占位值 登录系统对接后替换为 authViewModel.username,当前保证功能可跑通
shared_at 用 Unix 时间戳 整数排序比字符串快 展示时用 formatDate() 转换即可
查询 LIMIT 50 限制单次加载量 本地数据库中数据量有限,50 条足够展示全部历史分享

八、设计哲学:MVP 的"先跑通再完善"

本篇的分享功能是一个典型的 MVP(最小可行产品)实现。它有三个"不完美":

  1. 用户名是固定的:"灵犀大厨"不是真实用户名,但现阶段没有登录系统,真实用户名需要等登录功能完善后对接。
  2. 没有内容审核:用户可以分享任何内容,但当前是本地数据库,数据只在用户自己的设备上,不存在合规风险。
  3. 没有云端同步 :分享只在本地,其他设备看不到,但后续接入后端 API 后,只需将 storeHelper.insert 替换为 apiService.shareRecipe(),其他代码零改动。

这三个"不完美"是有意为之,不是能力不够。MVP 的核心哲学是:先验证核心行为(用户是否愿意分享),再逐步完善周边设施(身份、审核、同步)。 如果用户根本不愿意分享,你把审核系统做得再完善也是浪费。


九、本阶段总结与下篇预告

本篇用约 130 行新代码,为《灵犀厨房》打开了从"独享"到"共享"的大门:

  • 分享按钮:在菜谱详情页一键分享菜谱到社区,食材和步骤自动格式化
  • 社区广场:按时间倒序展示所有分享,卡片式布局清晰展示菜谱信息
  • 纯本地实现 :不依赖后端服务器,shared_recipes 表在第 28 篇已预建
  • MVP 哲学:先验证分享行为,再逐步完善身份和审核

现在的社交体验

🍳 做了一道拿手的番茄牛腩煲 → 点击分享 → 出现在社区广场 → 其他人打开 App 就能看到!

下篇预告:第 31 篇《应用权限管理与隐私保护最佳实践》。我们将系统梳理《灵犀厨房》需要的权限,按"最小权限原则"清理不必要的权限声明,并讲解 Health Kit 授权和 OAuth 隐私合规的最佳实践。


📚 本系列持续更新中:下一篇将让 App 在权限和隐私上合规,为发布到 AppGallery 做最后的准备。

🔗 专栏入口《HarmonyOS6.1全场景实战》合集
📦 获取基线版本源码包包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对您有所帮助,麻烦您动动发财之手点赞 👍、收藏 ⭐ 和评论 💬。谢谢大家!!

纯血鸿蒙,用心造厨。我们下一篇见!

相关推荐
若兰幽竹3 小时前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十九):【偏好持久化】偏好设置与推荐引擎联动——让 App 越用越“懂你”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹3 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十七):告别 UI 冻结——使用 TaskPool 实现高性能并发图像分析
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹3 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十八):【数据持久化】收藏与浏览历史——让数据在 App 重启后依然“活着”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹4 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十六):【响应式布局】折叠屏与平板完美适配——一套代码,多端呈现
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹5 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十五):【深色模式】一键切换暗色主题——让 App 在深夜也温柔
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹6 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十二) | 多媒体 | AVPlayer嵌入教学视频——让智慧屏真正“活”起来
音视频·华为鸿蒙系统·harmonyos6.1.0·灵犀厨房·harmonyos6.1
若兰幽竹6 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十三):【交互动效】转场、列表动画与趣味反馈——让每一次点击都有温度
交互·华为鸿蒙系统·harmonyos6.1
若兰幽竹7 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十一):【服务卡片】在桌面查看烹饪进度——主进程强推与跨进程桥接
服务卡片·华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹7 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(排错指南):【服务卡片跳转】页面栈“迷航”——从“回不去的主页”到精准 Tab 唤醒的全链路修复
华为鸿蒙系统·灵犀厨房·harmonyos6.1·排错指南