HarmonyOS 6.1 全场景实战|《灵犀厨房一键推荐》元服务【功能篇】三段式数据流:从 CloudDB 查询到毛玻璃 UI 的全链路实现
HarmonyOS 6.1 全场景实战|《灵犀厨房一键推荐》元服务【功能篇】三段式数据流:从 CloudDB 查询到毛玻璃 UI 的全链路实现
摘要:开发调试接点,在我的《灵犀厨房一键推荐》元服务的负一屏元服务刚打开就是一片白——隐私协议未同意时页面空转,CloudDB 查询失败时只抛了个 Error 不管 UI,用户点了"重新加载"按钮结果啥也没变。你可能觉得"数据加载嘛,try-catch 兜底就好了"。但当你面对隐私未同意 / CloudDB 超时 / 查询结果为空 / 用户偏好缺失 / 重复点击 5 种状态交织时,你就知道什么叫"状态地狱"。这就像你开车同时面对红灯、爆胎、没油、导航失灵、后车催促——任何一个环节处理不当,整个旅程就崩了。本篇将基于 HarmonyOS 6.1.0(API 23),详细拆解《灵犀厨房》负一屏元服务的三大功能模块——隐私协议毛玻璃弹窗、CloudDB 菜谱查询与推荐引擎、一键重新加载——的完整实现链路与设计决策。读完本篇,你将掌握一套可复用的"三段式数据流"架构模式,适用于任何需要数据加载 + 状态管理的元服务场景。
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。
一、引言:五种状态交织的"状态地狱"
《灵灵犀厨房一键推荐》负一屏元服务的核心功能看似简单——打开即显示一道推荐菜谱,下面有 6 个候选,用户可以"换一道"或点击"看看做法"跳转。但这背后涉及的状态网极其复杂:
| 场景 | 问题 | 后果 |
|---|---|---|
| 隐私未同意 | 页面空白,用户不知该做什么 | 跳出,留存率归零 |
| CloudDB Zone 未初始化 | getAllRecipes() 直接 crash |
元服务不可用,负一屏白屏 |
| 查询返回空数组 | 数据表中确实没有菜谱 | 用户看到白屏 + 无提示,以为 bug |
| 用户偏好缺失 | getCurrentUserPreference() 失败 |
推荐引擎无输入,返回空列表 |
| 用户点"重新加载" | 旧数据未清理,UI 不刷新 | 按钮点击无效,用户反复点击触发多次加载 |
而《灵犀厨房》的三段式数据流解决所有问题:
🎯 隐私确认 → CloudDB 查询 → 推荐引擎计算 → UI 渲染。每一段都有降级兜底:未登录返回默认偏好,查询失败返回空数组,推荐为空显示"暂无推荐"。重新加载时全量重置状态 + 刷新 key,确保 UI 重新渲染。 就像一条高速公路,每个出口都有应急通道——主路堵了,应急通道照样把你送到目的地。
二、功能总览:三大模块的协同工作流

UI 架构图(三模块在页面中的位置):
三、模块一:隐私协议毛玻璃弹窗
3.1 问题:裸启动的合规风险
HarmonyOS 元服务上架要求:首次启动必须展示隐私政策并获取用户同意。直接加载数据 = 违规。这就像餐厅开业前必须先公示卫生许可证——不是"可选项",而是"必选项"。
3.2 设计:三态流转的协议状态机
核心代码实现:
@Entry
@ComponentV2
struct AtomicRecommendPage {
// —— 隐私协议状态 ——
@Local showPrivacyDialog: boolean = false; // 底部弹窗显示
@Local showFullAgreement: boolean = false; // 全屏协议页显示
@Local agreementTitle: string = '';
@Local agreementContent: string = '';
// —— 动画状态 ——
@Local dialogTranslateY: number = 1000; // 弹窗初始位移(屏幕外)
@Local fullViewTranslateY: number = 1500; // 全屏页初始位移
@Local statusBarHeight: number = 36; // 状态栏高度
// —— 数据状态 ——
@Local isLoading: boolean = true;
@Local errorMsg: string = '';
async aboutToAppear(): Promise<void> {
// 1. 获取状态栏高度(避免内容被遮挡)
try {
const win = await window.getLastWindow(getContext(this));
const area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
this.statusBarHeight = px2vp(area.topRect.height);
} catch (e) {
this.statusBarHeight = 36; // 兜底值
}
// 2. 检查隐私协议状态
await this.checkPrivacyAgreement();
}
private async checkPrivacyAgreement(): Promise<void> {
try {
const pref = await preferences.getPreferences(getContext(this), 'app_settings');
const agreed = await pref.get('privacy_agreed', false) as boolean;
if (agreed) {
// 已同意 → 直接加载数据
await this.loadRecipeData();
return;
}
} catch (err) {
Logger.warn(TAG, '读取隐私协议状态失败,默认未同意');
}
// 未同意 → 显示底部弹窗
this.showPrivacyDialog = true;
this.dialogTranslateY = 1000; // 初始在屏幕外
// 弹窗滑入动画
animateTo({ duration: 350, curve: Curve.EaseOut }, () => {
this.dialogTranslateY = 0;
});
this.isLoading = false; // 停止全局 Loading,让用户操作
}
// —— 用户操作回调 ——
private async agreePrivacy(): Promise<void> {
// 保存同意状态
const pref = await preferences.getPreferences(getContext(this), 'app_settings');
await pref.put('privacy_agreed', true);
await pref.flush();
// 关闭弹窗
animateTo({ duration: 300, curve: Curve.EaseIn }, () => {
this.dialogTranslateY = 1000;
});
this.showPrivacyDialog = false;
// 开始加载数据
await this.loadRecipeData();
}
private disagreePrivacy(): void {
// 用户拒绝 → 退出元服务
getContext(this).terminateSelf();
}
private showFullAgreementView(): void {
this.showFullAgreement = true;
this.fullViewTranslateY = 1500;
animateTo({ duration: 350, curve: Curve.EaseOut }, () => {
this.fullViewTranslateY = 0;
});
}
private closeFullAgreement(): void {
animateTo({ duration: 300, curve: Curve.EaseIn }, () => {
this.fullViewTranslateY = 1500;
});
this.showFullAgreement = false;
}
}
3.3 核心实现:三层毛玻璃渐变叠加
毛玻璃设计遵循 HarmonyOS 新空间设计指南中"沉浸光感"的原则:通过多层模糊叠加营造空间层次感,让协议内容在模糊背景上"浮"出来。
代码实现:全屏协议页完整结构
@Builder
FullAgreementView() {
Column() {
// —— 顶部栏(毛玻璃第一层:Thin) ——
Column() {
Row() {
Button() {
Image($r('app.media.ic_back')).width(22).height(22)
}
.width(44).height(44)
.borderRadius(22)
.backgroundColor(AppColors.White80)
.onClick(() => this.closeFullAgreement())
Text(this.agreementTitle)
.fontSize(AppDimensions.FontTitleLG)
.fontWeight(FontWeight.Bold)
.fontColor(AppColors.TextPrimary)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Blank().width(44) // 右侧占位保持对称
}
.width('100%')
.height(56)
}
.width('100%')
.padding({ top: this.statusBarHeight })
.linearGradient({
angle: 180,
colors: [
[AppColors.GlassGradientStart, 0.0], // rgba(255,255,255,0.95)
[AppColors.GlassGradientMiddle, 1.0] // rgba(245,245,247,0.9)
]
})
.backgroundBlurStyle(BlurStyle.Thin) // ← 第一层毛玻璃:轻薄透
Divider().color(AppColors.Divider).strokeWidth(0.5)
// —— 正文区(毛玻璃第二层:Regular) ——
Scroll() {
Text(this.agreementContent)
.fontSize(AppDimensions.FontXL)
.fontColor(AppColors.TextSecondary)
.lineHeight(AppDimensions.LineHeightLoose)
.letterSpacing(0.5)
.padding(AppDimensions.PaddingContent)
}
.layoutWeight(1)
.linearGradient({
angle: 180,
colors: [
[AppColors.GlassGradientMiddle, 0.0],
[AppColors.GlassGradientEnd, 1.0] // rgba(240,240,242,0.85)
]
})
.backgroundBlurStyle(BlurStyle.Regular) // ← 第二层毛玻璃:中等模糊
// —— 底部操作栏 ——
Row() {
Button('我知道了')
.width('100%')
.height(AppDimensions.ButtonHeight)
.backgroundColor(AppColors.Primary)
.fontColor(Color.White)
.borderRadius(AppDimensions.RadiusXL)
.onClick(() => this.closeFullAgreement())
}
.width('100%')
.padding(AppDimensions.PaddingPage)
.backgroundColor(AppColors.White95)
}
.width('100%').height('100%')
.linearGradient({
angle: 180,
colors: [
[AppColors.GlassGradientStart, 0.0],
[AppColors.GlassGradientMiddle, 0.3],
[AppColors.GlassGradientEnd, 1.0]
]
})
.backgroundBlurStyle(BlurStyle.Thick) // ← 第三层毛玻璃:最厚(基底)
}
三层毛玻璃叠加效果:
| 层级 | BlurStyle | 视觉作用 | 代码位置 |
|---|---|---|---|
| 第一层 | Thin |
顶部栏轻薄透亮,让状态栏融入背景 | TopBar Column |
| 第二层 | Regular |
正文区中模糊,提升文字可读性 | Scroll |
| 第三层 | Thick |
基底高模糊,彻底隐藏底层内容 | 最外层 Column |
四、模块二:CloudDB 菜谱加载与预处理
4.1 问题:CloudDB Zone 初始化时机与错误码 1008230001
在 HarmonyOS 元服务中,cloudDatabase 必须在 Ability 的 onCreate 中调用 cloudCommon.init() 后,才能在其他地方通过 cloudDatabase.zone() 获取数据库区。但 Index.ets 的 aboutToAppear 可能先于 init 完成执行,导致 Zone 为 null、query 失败。这就像你还没把钥匙插进锁孔就开始拧——必然空转。
4.2 设计:懒初始化 + 三段式降级兜底
核心代码实现:
// helper/CloudDBHelper.ets
import { cloudCommon, cloudDatabase } from '@kit.CloudFoundationKit';
const ZONE_NAME = 'LingxiKitchenStorage';
const TAG = 'CloudDBHelper';
export class CloudDBHelper {
private static zone: cloudDatabase.Zone | null = null;
private static initialized: boolean = false;
/**
* 懒初始化 Zone(幂等设计)
* 调用方不关心初始化细节,CloudDBHelper 自己负责
*/
static async init(): Promise<void> {
if (CloudDBHelper.initialized && CloudDBHelper.zone) {
return; // 幂等:已初始化则直接返回
}
try {
// 等待 100ms 确保 cloudCommon.init 已完成
// 这是关键!避免与 Ability 的初始化竞态
await new Promise<void>((resolve) => setTimeout(resolve, 100));
CloudDBHelper.zone = cloudDatabase.zone(ZONE_NAME);
CloudDBHelper.initialized = true;
Logger.info(TAG, '✅ CloudDB Zone 初始化成功: %{public}s', ZONE_NAME);
} catch (err) {
const error = err as Error;
Logger.error(TAG, '❌ CloudDB Zone 初始化失败: %{public}s', error.message);
// ⚠️ 关键设计:不抛出错误,而是标记为未初始化
// 让调用方优雅降级
CloudDBHelper.initialized = false;
CloudDBHelper.zone = null;
}
}
/**
* 获取所有菜谱(三段式降级)
* 段一:Zone 未初始化 → 尝试初始化 → 失败返回 []
* 段二:查询成功 → 返回数据
* 段三:查询异常 → 返回 [](不 crash)
*/
static async getAllRecipes(): Promise<Recipe[]> {
// —— 段一:防御性检查 ——
if (!CloudDBHelper.zone) {
Logger.warn(TAG, '⚠️ CloudDB Zone 未初始化,尝试重新初始化');
await CloudDBHelper.init();
if (!CloudDBHelper.zone) {
Logger.error(TAG, '❌ CloudDB Zone 初始化失败,返回空数组');
return []; // ← 降级兜底,不 crash
}
}
// —— 段二:执行查询 ——
const condition = new cloudDatabase.DatabaseQuery(Recipes);
condition.orderByAsc('id');
try {
const resultArray = await CloudDBHelper.zone.query(condition);
Logger.info(TAG, '✅ 查询菜谱成功,共 %{public}d 条', resultArray.length);
// 映射到 Recipe 接口
const recipes: Recipe[] = resultArray.map((item: Record<string, Object>) => {
return {
id: item['id'] as number,
name: item['name'] as string || '未命名菜谱',
ingredients: (item['ingredients'] as string || '').split(',').filter((s: string) => s.length > 0),
steps: (item['steps'] as string || '').split(';').filter((s: string) => s.length > 0),
tags: (item['tags'] as string || '').split(',').filter((s: string) => s.length > 0),
calories: item['calories'] as number || 0,
cookTime: item['cook_time'] as string || '',
difficulty: item['difficulty'] as string || '简单',
servings: item['servings'] as number || 2,
cover: $r('app.media.placeholder_food')
};
});
return recipes;
} catch (err) {
// —— 段三:异常降级 ——
Logger.error(TAG, '❌ 查询菜谱失败: %{public}s', JSON.stringify(err));
return []; // ← 查询失败也降级
}
}
}
4.3 用户偏好三段式兜底
用户偏好是推荐引擎的"燃料"。燃料缺失,引擎空转。我们的设计确保无论什么情况,都有燃料可用:
核心代码实现:
static async getCurrentUserPreference(): Promise<UserPreferenceResult> {
try {
// —— 段一:检查登录状态 ——
const currentUser = await auth.getCurrentUser();
if (!currentUser) {
Logger.info(TAG, '👤 用户未登录,返回默认偏好');
return { favoriteTags: ['快手菜', '高蛋白'], allergies: [], maxCalories: 800 };
}
// —— 段二:查询用户偏好 ——
const uid = currentUser.getUid();
const numericId = parseInt(uid, 10) || 1; // uid 转数字,兜底为 1
// 查询 CloudDB 中的用户偏好表
const condition = new cloudDatabase.DatabaseQuery(UserPreference);
condition.equalTo('userId', numericId);
const result = await CloudDBHelper.zone.query(condition);
if (result && result.length > 0) {
const pref = result[0];
Logger.info(TAG, '✅ 获取用户偏好成功');
return {
favoriteTags: (pref['favoriteTags'] as string || '').split(',').filter((s: string) => s.length > 0),
allergies: (pref['allergies'] as string || '').split(',').filter((s: string) => s.length > 0),
maxCalories: pref['maxCalories'] as number || 800
};
}
// —— 段三:未命中偏好 → 降级 ——
Logger.info(TAG, '⚠️ 用户偏好未命中,返回默认偏好');
return { favoriteTags: ['快手菜', '高蛋白'], allergies: [], maxCalories: 800 };
} catch (err) {
// —— 段四:任何异常 → 降级 ——
Logger.error(TAG, '❌ 获取用户偏好异常: %{public}s', JSON.stringify(err));
return { favoriteTags: ['快手菜', '高蛋白'], allergies: [], maxCalories: 800 };
}
}
五、模块三:一键重新加载与 UI 刷新
5.1 问题:点击"重新加载"后 UI 纹丝不动
原始实现中,reloadData() 只是再次调用 loadRecipeData(),但旧的 mainRecipe 和 backupRecipes 没有被重置。ArkUI 的 @Local 状态追踪发现值"没变"(同一个对象引用),跳过重渲染。这就像你按了电梯按钮,但电梯以为你已经在那一层——什么都不会发生。
5.2 设计:全量状态重置 + refreshKey 强制刷新
核心代码实现:
@ComponentV2
struct AtomicRecommendPage {
// —— 核心数据状态 ——
@Local mainRecipe: Recipe | null = null; // 主推荐卡片
@Local backupRecipes: Recipe[] = []; // 候选列表(6个)
@Local allRecipes: Recipe[] = []; // 全量缓存
// —— UI 状态 ——
@Local isLoading: boolean = true;
@Local errorMsg: string = '';
@Local refreshKey: number = 0; // ★ 强制刷新键
// —— 动画状态 ——
@Local animationState: AnimationState = new AnimationState();
/**
* 一键重新加载 - 全量重置 + 强制刷新
*/
private async reloadData(): Promise<void> {
Logger.info(TAG, '🔄 开始重新加载数据');
// —— ① 全量重置所有 UI 状态 ——
this.isLoading = true;
this.errorMsg = '';
this.mainRecipe = null; // ← 必须置空!否则 UI 不刷新
this.backupRecipes = []; // ← 必须置空!
this.animationState = new AnimationState(); // ← 重建动画状态
// —— ② 强制刷新 key(ForEach 依赖) ——
this.refreshKey++;
Logger.info(TAG, '🔄 状态已重置,refreshKey: %{public}d', this.refreshKey);
// —— ③ 重新执行加载 ——
await this.loadRecipeData();
Logger.info(TAG, '🔄 重新加载完成,isLoading: %{public}s', this.isLoading.toString());
}
/**
* 三段式数据流:隐私 → CloudDB → 推荐
*/
private async loadRecipeData(): Promise<void> {
try {
// —— 段一:从 CloudDB 获取全量菜谱 ——
this.allRecipes = await CloudDBHelper.getAllRecipes();
if (this.allRecipes.length === 0) {
this.errorMsg = AppTexts.NoData; // '暂无菜谱数据'
this.isLoading = false;
return;
}
// —— 段二:获取用户偏好(三段式兜底) ——
const userPref = await CloudDBHelper.getCurrentUserPreference();
this.currentUserPreference = new UserPreference(
userPref.favoriteTags,
userPref.allergies,
userPref.maxCalories
);
// —— 段三:推荐引擎计算 ——
const recommendations = recommendEngine.getRecommendations(
this.currentUserPreference,
this.allRecipes,
[], // 已浏览列表(暂时为空)
7 // 推荐数量:1 主 + 6 候选
);
if (recommendations.length > 0) {
this.mainRecipe = recommendations[0]; // 主推荐
this.backupRecipes = recommendations.slice(1, 7); // 候选列表
// 启动入场动画
this.startEnterAnimation();
Logger.info(TAG, '✅ 推荐成功:1 主 + %{public}d 候选', this.backupRecipes.length);
} else {
this.errorMsg = AppTexts.NoRecommend; // '暂无推荐数据'
Logger.warn(TAG, '⚠️ 推荐引擎返回空结果');
}
} catch (err) {
Logger.error(TAG, '❌ 加载数据异常: %{public}s', JSON.stringify(err));
this.errorMsg = AppTexts.LoadFailed; // '加载失败,请重试'
}
this.isLoading = false;
}
}
5.3 为什么需要 refreshKey?—— ArkUI 渲染原理图解
ForEach key 函数实现:
// —— 主推荐卡片 ——
ForEach([this.mainRecipe], (recipe: Recipe | null) => {
if (recipe) {
Column() {
this.HeroCard(recipe)
}
}
}, (recipe: Recipe | null) => {
// ★ key 包含 refreshKey,每次 reload 都会变化
return `hero-${recipe?.id ?? 'empty'}-${this.refreshKey}`;
})
// —— 候选列表 ——
ForEach(this.backupRecipes, (recipe: Recipe, index: number) => {
ListItem() {
this.RecommendItem(recipe, index)
}
}, (recipe: Recipe, index: number) => {
// ★ key 包含 refreshKey + index,确保每个候选都重建
return `backup-${recipe.id}-${index}-${this.refreshKey}`;
})
六、代码增删改清单
| 文件 | 操作 | 新增/修改行数 | 职责 |
|---|---|---|---|
pages/AtomicRecommendPage.ets |
修改 | +85 | 新增 reloadData() 全量状态重置;loadRecipeData() 三段式数据流整合;隐私协议状态机 |
helper/CloudDBHelper.ets |
新增 | +180 | 懒初始化 Zone;getAllRecipes() 三段式降级;getCurrentUserPreference() 四段式兜底 |
constants/AppConstants.ets |
修改 | +15 | 新增毛玻璃渐变常量;AppTexts 提示文案(NoData/NoRecommend/LoadFailed/Retry) |
constants/AppDimensions.ets |
修改 | +8 | 新增 FontTitleLG、LineHeightLoose、PaddingContent |
七、血泪避坑总结
| 现象 | 真相 | 解决方案 |
|---|---|---|
| 隐私未同意页面空白 | isLoading = true 一直显示 Loading,用户困惑 |
未同意时 isLoading = false + 弹出协议弹窗,让用户知道"需要你操作" |
| CloudDB 查询报 1008230001 | Zone 未初始化直接 query,cloudCommon.init 异步但无 await |
init() 中用 setTimeout 100ms 等待 init 完成 + 懒初始化 |
reloadData 调用后 UI 不变 |
mainRecipe 被赋值为同一对象引用,@Local 认为没变化 |
先置 null 再赋值新对象;refreshKey++ 强制 ForEach 重建 |
| 协议页状态栏遮挡顶部按钮 | 没有获取状态栏高度就直接 paddingTop |
aboutToAppear 中通过 window.getLastWindow() → getWindowAvoidArea → px2vp 获取高度 |
| 用户偏好未登录时 crash | auth.getCurrentUser() 返回 null 直接 .getUid() |
先判空,null 时返回默认偏好 |
Promise<void> 类型推断报错 |
ArkTS 要求泛型函数显式指定类型参数 | 写全 new Promise<void>((resolve) => setTimeout(resolve, 100)) |
| 毛玻璃三层叠加后性能下降 | 每层独立 backgroundBlurStyle 增加渲染负担 |
只在最关键的两层使用(最外层 Thick + 正文区 Regular),顶部栏用渐变替代毛玻璃 |
八、设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| CloudDB Zone 初始化时机 | 懒初始化 + setTimeout 100ms | 避免与 cloudCommon.init 竞态;幂等设计允许多次调用 |
| 数据加载失败策略 | 返回空数组而非抛异常 | UI 层统一处理 length === 0 显示"暂无数据",比 try-catch 更优雅 |
| 用户偏好兜底 | 四段式降级(未登录/无记录/异常/兜底) | 每个分支都有明确的默认值,推荐引擎永不空转 |
| 重新加载实现 | 全量重置 + refreshKey | 不仅更新数据,连动画状态都重建,彻底避免残留 |
| 协议页动画 | translateY 1500 → 0 | 从屏幕底部滑入,符合 HarmonyOS 原生转场规范 |
| 毛玻璃实现 | 三层 BlurStyle 叠加 | 从底层到顶层依次 Thick → Regular → Thin,营造空间层次感 |
| 状态管理 | @Local + 手动重置 | 不引入额外状态管理库,保持元服务轻量 |
九、运行验证
验证场景 1:首次启动隐私协议流程
- 清除应用数据后启动
- 期望结果:
- 显示底部弹窗(从底部滑入)
- 点击"查看协议"→ 全屏毛玻璃协议页滑入
- 返回 → 点击"同意"→ Loading → Hero 卡片入场动画

验证场景 2:CloudDB 降级运行
- 断网启动应用
- 期望结果:
getAllRecipes()返回空数组- 显示"暂无菜谱数据" + 重新加载按钮
- 不 crash,不白屏

验证场景 3:重新加载按钮
- 正常加载完成后点击"重新加载"
- 期望结果:
- 卡片先消失(透明度动画)
- Loading 出现
- 新推荐卡片入场动画完整播放


验证场景 4:用户未登录的偏好降级
- 在未登录状态下首次查看菜谱
- 期望结果:
- 使用默认偏好
['快手菜', '高蛋白']成功推荐 - 不会因为 uid 为 null 而 crash
- 控制台打印"用户未登录,返回默认偏好"
- 使用默认偏好
十、总结与下篇预告
本篇我们基于 HarmonyOS 6.1.0(API 23),详解了《灵犀厨房》元服务的三大功能模块:
- 隐私协议毛玻璃弹窗:三态状态机 + 三层 BlurStyle 叠加,打造沉浸式协议阅读体验
- CloudDB 菜谱加载:懒初始化 Zone + 四段式偏好兜底 + 查询失败降级返回空数组
- 一键重新加载:全量状态重置 +
refreshKey强制刷新,彻底解决"按钮点击无效"问题
三段式数据流速查

| 阶段 | 正常流 | 降级流 |
|---|---|---|
| 获取菜谱 | CloudDB → 全量 Recipe[] | 返回 [] → 显示"暂无菜谱数据" |
| 获取偏好 | auth.getCurrentUser() → CloudDB 查询 | 未登录 → 默认偏好;查无记录 → 默认偏好 |
| 推荐计算 | recommendEngine.getRecommendations(7) | 返回 [] → 显示"暂无推荐数据" |
| 重新加载 | 全量重置 + refreshKey → 新数据 | 数据为空 → 显示错误提示 + 重试按钮 |
📚 本系列持续更新中,敬请期待,下一篇更精彩。
🔗 专栏入口:《HarmonyOS6.1全场景实战》合集
📦 获取基线版本源码包:包括本系列所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。
纯血鸿蒙,用心造厨。我们下一篇见!
更多推荐



所有评论(0)