【鸿蒙原生开发会议随记 Pro】用 AppStorage 做会议列表和联系人列表的刷新信号
前言
我在《会议随记 Pro》里处理会议保存、会议删除、联系人新增这些操作时,最早遇到的不是数据库问题,而是页面之间怎么同步的问题。
新建会议以后,会议列表要更新,工作台里的统计数据也要重新计算。删除会议以后,当前列表可以先把这一项移除,但工作台、桌面卡片、其他筛选入口也要知道会议数据已经变化。新增联系人以后,联系人列表要更新,新建会议页里的联系人选择入口也可能需要重新读取数据。
这类问题刚开始很容易用页面之间互相调用来处理。比如新增会议页保存成功后,直接调用会议列表刷新;联系人弹窗新增成功后,直接调用联系人列表刷新;项目编辑完成后,再去通知项目列表。页面少的时候,这种写法能跑通。继续往后,工作台统计、桌面卡片、详情页、列表页、选择器都开始关心同一类数据变化,调用关系就会变成一张很难维护的网。
我后来在项目里采用了一套很轻的刷新信号实现思路。会议相关操作推进 MeetingReloadKey,联系人相关操作推进 ContactReloadKey,项目相关操作推进 ProjectReloadKey。页面监听自己关心的 key,再结合当前 Tab 是否可见、本地版本是否落后,决定马上刷新还是先记录下来。
这里我不会把它包装成通用方案。它更像《会议随记 Pro》当前阶段的一套项目实现思路:数据仍然由 Repository 管理,页面仍然负责自己的分页、搜索和筛选,AppStorage 只承担跨页面版本信号。对一个本地数据为主、页面数量可控、刷新事件不高频的鸿蒙原生应用来说,这个方案足够轻,也方便继续维护。
这套机制里会用到 ArkUI 的 AppStorage、@StorageProp 和 @Watch。我在项目里把它们放在工作台、会议列表、联系人列表这些页面之间使用。页面当前可见时马上刷新,页面隐藏时先记录状态,等切回来再检查版本差异。这样可以减少隐藏页面的无效查询,也能让列表、统计、选择器在需要的时候拿到最新数据。

一、刷新信号只表达数据变了
项目里有一个很小的工具类 RefreshUtil。它不查询数据库,也不更新 UI,只负责把全局 key 往前推进一次。
会议相关操作调用 notifyMeetingUpdate(),联系人相关操作调用 notifyContactUpdate(),项目相关操作调用 notifyProjectUpdate()。会议刷新信号会读取当前 MeetingReloadKey,再加一写回 AppStorage;联系人刷新信号也采用同样的处理。项目刷新信号可以直接写入时间戳,因为项目列表只需要感知版本变化,不依赖连续数字。项目源码里的 RefreshUtil 正是按照这个方向处理,会议、联系人和项目分别维护自己的刷新 key。
const KEY_MEETING_RELOAD = 'MeetingReloadKey';
const KEY_CONTACT_RELOAD = 'ContactReloadKey';
export class RefreshUtil {
static notifyMeetingUpdate() {
const current = AppStorage.get<number>(KEY_MEETING_RELOAD) || 0;
AppStorage.setOrCreate(KEY_MEETING_RELOAD, current + 1);
}
static notifyContactUpdate() {
const current = AppStorage.get<number>(KEY_CONTACT_RELOAD) || 0;
AppStorage.setOrCreate(KEY_CONTACT_RELOAD, current + 1);
}
static notifyProjectUpdate() {
AppStorage.setOrCreate('ProjectReloadKey', Date.now());
}
}
这段代码轻,但边界很重要。RefreshUtil 只告诉页面数据版本已经变化,不替页面决定怎么加载数据。会议列表怎么分页,联系人列表怎么搜索,工作台怎么统计,这些逻辑都留在各自页面里。
我会把它拆成下面这种关系。
| 方法 | 表达的业务含义 | 不处理的内容 |
|---|---|---|
notifyMeetingUpdate() |
会议数据已经变化 | 不查询会议列表,不重置分页 |
notifyContactUpdate() |
联系人数据已经变化 | 不控制联系人列表,也不打开联系人弹窗 |
notifyProjectUpdate() |
项目数据已经变化 | 不决定项目详情是否重新加载 |
这里容易写乱的地方,是把信号和动作混在一起。比如新增会议成功后,顺手让会议列表刷新,短期看起来省事。后面联系人选择器、工作台统计、桌面卡片都要同步时,这个新增页面就会知道太多别的页面细节。项目写到这个阶段,我更愿意让新增页只发出会议数据变化的信号,至于谁刷新、什么时候刷新,由对应页面自己判断。
会议列表里会通过 @StorageProp 接收全局 key,再用 @Watch 监听变化。页面内部还会保存一份 lastLoadedKey,记录自己上一次加载到哪个版本。真实项目的 MeetingListPage 里同时保留了 reloadKey、currentTabIndex 和 lastLoadedKey,这些状态一起决定页面是否需要重新加载。
@StorageProp('MeetingReloadKey')
@Watch('onReloadKeyChanged')
reloadKey: number = 0;
private lastLoadedKey: number = -1;
lastLoadedKey 这个变量很容易被忽略。没有它,页面只知道全局 key 变化了,却不知道自己是否已经加载过当前版本。用户多次切换 Tab,或者同一个页面多次显示时,就容易重复查询。保留本地版本以后,页面可以先做一次判断:本地版本和全局版本一致,就跳过这次刷新;本地版本落后,再重新加载数据。
联系人列表和项目列表也是同样的思路。它们监听不同的 key,但页面内部都有自己的本地版本。全局信号只负责告诉页面数据变了,页面自己决定要不要重新加载。

二、页面可见时再加载
如果全局 key 一变化,所有页面都立刻查询数据库,代码确实容易写,但这个应用里不合适。
《会议随记 Pro》是一个典型的 Tab 结构。用户当前只会观察一个 Tab,其他页面不在屏幕上。会议列表、联系人列表和工作台都有自己的数据状态。会议列表还有搜索关键词、筛选条件、分页偏移、是否还有更多数据、是否正在加载这些状态。隐藏状态下立刻刷新,不一定符合用户重新回到页面时的预期,也会带来很多看不见的查询。
会议列表里我会先判断当前 Tab。当前页面就是会议列表时,再调用 checkAndLoad() 和 loadFilters()。如果用户当前在工作台或联系人页,会议列表先不查询,只等 Tab 切回来时再检查。
项目里的 onReloadKeyChanged() 会先收到全局 reload 信号,再结合 currentTabIndex 判断当前是否在会议列表 Tab。当前就是会议列表时才执行 checkAndLoad() 和 loadFilters();如果不是当前 Tab,则把刷新留到页面重新显示时处理。
private onReloadKeyChanged(): void {
if (this.currentTabIndex === MY_TAB_INDEX) {
this.checkAndLoad();
this.loadFilters();
}
}
private onTabIndexChange(): void {
if (this.currentTabIndex === MY_TAB_INDEX) {
setTimeout(() => {
this.checkAndLoad();
}, 50);
}
}
这里留了一个短延迟,是为了等 Tab 切换状态完成以后再检查版本。页面刚切回来的瞬间,有些组件状态还在更新,马上查数据不一定有必要。实际项目里,这个延迟很短,只是让页面切换和数据刷新不要挤在同一个时刻。
真正加载前,页面还会比较本地版本和全局版本。
private checkAndLoad(force: boolean = false) {
if (!force && this.lastLoadedKey === this.reloadKey) {
return;
}
this.loadData(true);
}
项目里的 checkAndLoad() 也是这种逻辑:本地版本和全局版本一致时跳过刷新,本地版本落后时重新加载列表。loadData(true) 会把分页偏移重置为 0,并在刷新成功后把 lastLoadedKey 更新成当前 reloadKey。
会议列表执行刷新时,会把分页偏移重置为 0,把 hasMore 重新设为 true,再按当前搜索关键词和筛选条件查询。刷新成功后,页面把 lastLoadedKey 更新成当前 reloadKey。这一步很重要,因为页面只有在真正加载成功后,才能说自己已经同步到最新版本。
我在这里会保留一个页面状态原则:页面层负责判断刷新时机,子组件只负责展示数据。刷新信号可以全局共享,但分页、搜索、筛选、选中项这些状态不要交给全局工具类处理。工具类一旦知道页面怎么分页、怎么筛选、怎么展示,后面每多一个页面,刷新工具都会变得越来越重。
| 状态 | 放置位置 | 原因 |
|---|---|---|
MeetingReloadKey |
AppStorage |
多个页面都要知道会议数据变化 |
lastLoadedKey |
页面内部 | 每个页面自己记录加载到哪个版本 |
searchKeyword |
会议列表页面 | 只有会议列表知道当前搜索条件 |
pageOffset |
会议列表页面 | 分页属于列表自己的加载状态 |
currentTabIndex |
主 Tab 传入页面 | 页面根据可见性决定刷新时机 |
这个表里最值得留意的是 lastLoadedKey。它不是全局状态,因为每个页面加载节奏不同。会议列表可能已经刷新,工作台可能还没刷新,联系人列表也可能和会议数据没有关系。每个页面保留自己的本地版本,刷新判断才不会互相干扰。
这个思路适合当前项目的一个原因,是会议数据不是高频实时数据。会议新增、删除、编辑都属于低频业务动作,推进一个全局版本号就够用。如果后面做多人协作、云端实时同步、会议实时转写列表,那就不能只靠这种轻量 key 处理了。到那个阶段,数据版本、更新时间、冲突处理都要进入数据层设计。

三、当前页面先响应,其他页面再同步
真实项目里,触发刷新信号的位置很多。新建会议保存、会议详情编辑、会议列表删除、联系人新增、项目新增,都会让一部分页面的数据过期。
会议列表删除一条会议时,我不会等待全局信号再更新当前页面。当前列表已经知道用户删的是哪一条会议,就可以先从数组里移除这一项。删除成功后,再调用 RefreshUtil.notifyMeetingUpdate()。这个信号是给其他会议相关页面看的,比如工作台统计、其他筛选入口、桌面卡片数据。项目里的删除逻辑就是先删除数据库记录,再从当前 meetings 数组移除,最后发送会议刷新信号。
await deleteMeeting(this.hostCtx, meeting.id);
const index = this.meetings.findIndex((m) => m.id === meeting.id);
if (index !== -1) {
this.meetings.splice(index, 1);
}
RefreshUtil.notifyMeetingUpdate();
这个顺序和用户操作有关。用户刚删除一条会议,当前列表应该马上有反馈。如果页面等待全局 key 变化后重新查询,删除动作会显得慢,尤其是在会议记录多、筛选条件复杂的时候。当前页面先响应,其他页面稍后同步,这个节奏更适合这类列表操作。
联系人新增也类似。联系人编辑弹窗确认后,当前联系人列表可以直接刷新一次,然后再调用 RefreshUtil.notifyContactUpdate()。这个信号不只是给当前页面用,还会影响联系人选择器、会议参会人列表、其他联系人入口。
我一般会按下面这张表处理。
| 操作场景 | 当前页面处理 | 全局信号处理 |
|---|---|---|
| 删除会议 | 当前列表先移除这一项 | 通知工作台、其他会议页面数据变化 |
| 新建会议 | 保存后返回上层页面 | 通知会议列表和统计数据变化 |
| 编辑会议标题 | 当前详情页通过返回回调重新加载 | 通知列表标题和工作台统计变化 |
| 新增联系人 | 当前联系人页刷新列表 | 通知联系人选择器和其他联系人入口 |
| 新增项目 | 当前项目页刷新列表 | 通知项目详情和会议筛选入口 |
这里的取舍很明确。当前页面负责把用户刚做的操作反馈出来,全局信号负责通知其他页面版本已经变化。这样页面之间不用互相调用,也不会把刷新逻辑写成一张复杂的调用网。
如果后面要处理更复杂的场景,比如正在编辑中的表单收到全局刷新信号,我不会直接覆盖用户输入。编辑页可以提示数据已经变化,也可以在保存前做冲突检查,但不能因为别的页面发出刷新信号,就把当前输入框里的内容重置掉。列表和统计页适合自动刷新,编辑页要保守一些。
这里也能看出这套方案的边界。它适合通知列表、统计、选择器重新读取数据,不适合承载复杂业务事件。比如会议保存失败、云端同步冲突、录音文件上传进度,这些都不应该塞进一个 reloadKey 里。刷新 key 只表达数据变化,不表达业务过程。

四、用一个页面验证刷新关系
我把这个机制压缩成一个 Index.ets 示例。页面里有三个 Tab:工作台、会议列表、联系人列表。顶部按钮模拟新增会议和新增联系人。每个 Tab 都展示自己的全局 key、本地 key、刷新次数和待刷新状态。
这里没有连接真实数据库,所有数据都保存在页面状态里。这样可以把刷新信号的行为展示得更清楚:当前可见的页面马上刷新,隐藏页面记录待刷新状态,切回对应 Tab 后再比较本地 key 和全局 key。
这个小页面和真实项目的对应关系如下。
| 小页面里的内容 | 真实项目里的位置 |
|---|---|
meetingReloadKey |
MeetingReloadKey |
contactReloadKey |
ContactReloadKey |
meetingListLoadedKey |
会议列表里的 lastLoadedKey |
contactListLoadedKey |
联系人列表里的 lastLoadedKey |
checkMeetingList() |
会议列表里的 checkAndLoad() |
switchTab() |
主 Tab 切换后触发页面检查 |
这个演示页要观察两条路径。
第一条路径是会议数据变化。停留在工作台,点击新增会议,工作台当前可见,会立即同步会议统计;会议列表不在当前 Tab,只记录待刷新。切换到会议列表以后,会议列表发现本地 lastLoadedKey 落后于全局 MeetingReloadKey,再执行刷新。
第二条路径是联系人数据变化。停留在会议列表,点击新增联系人。会议列表不会刷新,因为它只关心会议数据。工作台和联系人列表会记录联系人数据变化。切换到联系人列表以后,联系人列表会根据 ContactReloadKey 完成延迟刷新。
这两个路径能说明一个页面状态原则:全局信号只推动版本变化,页面加载仍然属于页面自己的职责。如果把查询逻辑写进 RefreshUtil,工具类就会知道会议列表怎么分页、联系人列表怎么搜索、工作台怎么统计,边界会越来越模糊。



五、迁回项目时保留边界
回到真实项目时,这个小页面里的按钮和模拟数据都要删掉,保留这套刷新关系就够了。
| 小页面里的逻辑 | 真实项目里的处理 |
|---|---|
meetingReloadKey += 1 |
RefreshUtil.notifyMeetingUpdate() |
contactReloadKey += 1 |
RefreshUtil.notifyContactUpdate() |
页面本地 lastLoadedKey |
MeetingListPage、ContactListPage、ProjectListPage 内部版本记录 |
| Tab 切换后检查版本 | onTabIndexChange() |
| 立即刷新和延迟刷新 | onReloadKeyChanged() 里的可见性判断 |
refreshWorkbench() |
工作台重新计算会议和联系人统计 |
refreshMeetingList() |
会议列表重新查询分页数据 |
refreshContactList() |
联系人列表重新查询联系人数据 |
这个刷新机制适合列表、统计、选择器这类读多写少的页面。它不适合直接覆盖正在编辑的表单。比如用户正在编辑会议标题,另一个入口发出了 MeetingReloadKey,编辑页不能马上把输入框覆盖掉。更稳的处理方式是提示数据可能变化,或者在保存前做冲突判断。
我会把刷新信号看成一个版本提醒,而不是强制同步命令。它提醒页面某类数据发生过变化,页面要不要刷新、什么时候刷新、怎么保留当前输入,都应该由页面自己决定。这样 AppStorage 的职责会保持很轻,页面也不会被全局信号牵着走。
后续如果项目里出现更多数据域,比如录音文件上传状态、转写任务状态、云端同步状态,我不会继续把所有内容都塞进 reloadKey。列表刷新仍然可以保留轻量 key,任务进度和同步状态则要用更明确的数据结构来表达。千万不要把刷新信号写成万能事件中心。

总结
这套刷新信号实现思路适合《会议随记 Pro》当前阶段。它没有试图接管所有页面状态,只是让会议、联系人、项目这几类数据变化能被相关页面感知到。RefreshUtil 推进 MeetingReloadKey、ContactReloadKey、ProjectReloadKey,页面通过 @StorageProp 和 @Watch 接收变化,再结合当前 Tab、分页状态和本地版本决定是否重新加载。
这个边界保留下来以后,新增会议不需要知道会议列表、工作台和桌面卡片谁在监听;会议列表也不需要知道数据来自新建页、编辑页还是删除操作。页面只要比较全局 key 和本地 lastLoadedKey,就能判断自己是否落后。对列表和统计页来说,这个判断已经足够。对正在编辑的表单页,我会继续保守处理,不让全局刷新信号直接覆盖用户输入。
这套方案的适用范围也要放在心里。它适合本地数据为主、页面数量可控、刷新事件不高频的应用。如果后面变成多人协作、云端实时同步或者高频任务进度更新,就要把数据版本、同步状态和冲突处理放到更完整的数据层里,不能继续只靠一个全局 key 承接所有变化。
这套刷新机制已经放进我的《会议随记 Pro》里使用,应用目前已经上架华为应用市场。里面包含会议录音、时间轴笔记、联系人、项目、标签管理和多设备适配这些功能。对鸿蒙原生应用的完整实现感兴趣的话,可以下载体验一下:会议随记 Pro。
完整代码
interface RefreshLog {
id: number;
source: string;
content: string;
}
interface MeetingItem {
id: string;
title: string;
summary: string;
updatedAt: number;
}
interface ContactItem {
id: string;
name: string;
company: string;
updatedAt: number;
}
enum DemoTab {
Workbench = 0,
MeetingList = 1,
ContactList = 2
}
@Entry
@Component
struct Index {
@State currentTab: DemoTab = DemoTab.Workbench;
@State meetingReloadKey: number = 0;
@State contactReloadKey: number = 0;
@State workbenchMeetingKey: number = 0;
@State workbenchContactKey: number = 0;
@State meetingListLoadedKey: number = -1;
@State contactListLoadedKey: number = -1;
@State workbenchRefreshCount: number = 0;
@State meetingRefreshCount: number = 0;
@State contactRefreshCount: number = 0;
@State meetingPending: boolean = false;
@State contactPending: boolean = false;
@State workbenchPending: boolean = false;
@State workbenchMeetingCount: number = 3;
@State workbenchContactCount: number = 5;
@State workbenchLastReason: string = '工作台已读取初始统计';
@State meetingLastReason: string = '会议列表尚未加载';
@State contactLastReason: string = '联系人列表尚未加载';
@State meetingDb: MeetingItem[] = [
{
id: 'meeting-001',
title: '产品评审会',
summary: '确认多设备适配范围',
updatedAt: 1717819200000
},
{
id: 'meeting-002',
title: '录音链路复盘',
summary: '整理录音状态机和保存流程',
updatedAt: 1717905600000
},
{
id: 'meeting-003',
title: '桌面卡片讨论',
summary: '确认卡片刷新和数据入口',
updatedAt: 1717992000000
}
];
@State contactDb: ContactItem[] = [
{
id: 'contact-001',
name: '张晨',
company: '产品组',
updatedAt: 1717819200000
},
{
id: 'contact-002',
name: '林夏',
company: '设计组',
updatedAt: 1717905600000
},
{
id: 'contact-003',
name: '周远',
company: '研发组',
updatedAt: 1717992000000
},
{
id: 'contact-004',
name: '陈安',
company: '测试组',
updatedAt: 1718078400000
},
{
id: 'contact-005',
name: '赵宁',
company: '运营组',
updatedAt: 1718164800000
}
];
@State meetingRows: MeetingItem[] = [];
@State contactRows: ContactItem[] = [];
@State logSeed: number = 0;
@State logs: RefreshLog[] = [];
private addLog(source: string, content: string): void {
const next: RefreshLog = {
id: this.logSeed + 1,
source: source,
content: content
};
this.logSeed = next.id;
this.logs = [next, ...this.logs].slice(0, 16);
}
private getNextMeetingId(nextIndex: number): string {
return `meeting-${nextIndex.toString().padStart(3, '0')}`;
}
private getNextContactId(nextIndex: number): string {
return `contact-${nextIndex.toString().padStart(3, '0')}`;
}
private notifyMeetingUpdate(reason: string): void {
const nextKey = this.meetingReloadKey + 1;
const nextIndex = this.meetingDb.length + 1;
const now = Date.now();
const nextMeeting: MeetingItem = {
id: this.getNextMeetingId(nextIndex),
title: `新增会议 ${nextIndex}`,
summary: `由按钮模拟创建,会议版本=${nextKey}`,
updatedAt: now
};
const nextMeetingDb: MeetingItem[] = [nextMeeting, ...this.meetingDb];
this.meetingReloadKey = nextKey;
this.meetingDb = nextMeetingDb;
this.addLog('MeetingReloadKey', `${reason},全局会议版本推进到 ${nextKey}`);
this.receiveMeetingSignal(nextKey, nextMeetingDb);
}
private notifyContactUpdate(reason: string): void {
const nextKey = this.contactReloadKey + 1;
const nextIndex = this.contactDb.length + 1;
const now = Date.now();
const nextContact: ContactItem = {
id: this.getNextContactId(nextIndex),
name: `新联系人 ${nextIndex}`,
company: `模拟来源,联系人版本=${nextKey}`,
updatedAt: now
};
const nextContactDb: ContactItem[] = [nextContact, ...this.contactDb];
this.contactReloadKey = nextKey;
this.contactDb = nextContactDb;
this.addLog('ContactReloadKey', `${reason},全局联系人版本推进到 ${nextKey}`);
this.receiveContactSignal(nextKey, nextContactDb);
}
private receiveMeetingSignal(nextMeetingKey: number, nextMeetingDb: MeetingItem[]): void {
if (this.currentTab === DemoTab.Workbench) {
this.refreshWorkbench(
'工作台当前可见,立即读取会议统计',
nextMeetingKey,
this.contactReloadKey,
nextMeetingDb.length,
this.workbenchContactCount
);
} else {
this.workbenchPending = true;
this.addLog('Workbench', '工作台不在当前 Tab,先记录统计待刷新');
}
if (this.currentTab === DemoTab.MeetingList) {
this.refreshMeetingList(
'会议列表当前可见,立即读取会议列表快照',
nextMeetingKey,
nextMeetingDb
);
} else {
this.meetingPending = true;
this.addLog('MeetingList', '会议列表不在当前 Tab,等待切换回来后再刷新');
}
if (this.currentTab === DemoTab.ContactList) {
this.addLog('ContactList', '联系人列表不依赖会议数据,本页保持当前状态');
}
}
private receiveContactSignal(nextContactKey: number, nextContactDb: ContactItem[]): void {
if (this.currentTab === DemoTab.Workbench) {
this.refreshWorkbench(
'工作台当前可见,立即读取联系人统计',
this.meetingReloadKey,
nextContactKey,
this.workbenchMeetingCount,
nextContactDb.length
);
} else {
this.workbenchPending = true;
this.addLog('Workbench', '工作台不在当前 Tab,先记录统计待刷新');
}
if (this.currentTab === DemoTab.ContactList) {
this.refreshContactList(
'联系人列表当前可见,立即读取联系人列表快照',
nextContactKey,
nextContactDb
);
} else {
this.contactPending = true;
this.addLog('ContactList', '联系人列表不在当前 Tab,等待切换回来后再刷新');
}
if (this.currentTab === DemoTab.MeetingList) {
this.addLog('MeetingList', '会议列表不依赖联系人数据,本页保持当前状态');
}
}
private switchTab(target: DemoTab): void {
this.currentTab = target;
if (target === DemoTab.Workbench) {
this.addLog('Tab', '切换到工作台,检查会议和联系人版本差异');
this.checkWorkbench();
return;
}
if (target === DemoTab.MeetingList) {
this.addLog('Tab', '切换到会议列表,检查 MeetingReloadKey');
this.checkMeetingList();
return;
}
this.addLog('Tab', '切换到联系人列表,检查 ContactReloadKey');
this.checkContactList();
}
private checkWorkbench(): void {
if (this.workbenchMeetingKey !== this.meetingReloadKey ||
this.workbenchContactKey !== this.contactReloadKey) {
this.refreshWorkbench(
'工作台重新显示,发现统计依赖的数据已经变化',
this.meetingReloadKey,
this.contactReloadKey,
this.meetingDb.length,
this.contactDb.length
);
return;
}
this.workbenchPending = false;
this.addLog('Workbench', '工作台本地统计已经同步,不需要重新加载');
}
private checkMeetingList(): void {
if (this.meetingListLoadedKey !== this.meetingReloadKey) {
this.refreshMeetingList(
'会议列表重新显示,发现会议版本已经变化',
this.meetingReloadKey,
this.meetingDb
);
return;
}
this.meetingPending = false;
this.addLog('MeetingList', '会议列表本地版本已经同步,不需要重新加载');
}
private checkContactList(): void {
if (this.contactListLoadedKey !== this.contactReloadKey) {
this.refreshContactList(
'联系人列表重新显示,发现联系人版本已经变化',
this.contactReloadKey,
this.contactDb
);
return;
}
this.contactPending = false;
this.addLog('ContactList', '联系人列表本地版本已经同步,不需要重新加载');
}
private refreshWorkbench(
reason: string,
meetingKey: number,
contactKey: number,
meetingCount: number,
contactCount: number
): void {
this.workbenchMeetingKey = meetingKey;
this.workbenchContactKey = contactKey;
this.workbenchMeetingCount = meetingCount;
this.workbenchContactCount = contactCount;
this.workbenchRefreshCount += 1;
this.workbenchPending = false;
this.workbenchLastReason = reason;
this.addLog(
'Workbench',
`${reason},会议=${meetingCount},联系人=${contactCount},刷新次数 ${this.workbenchRefreshCount}`
);
}
private refreshMeetingList(reason: string, meetingKey: number, sourceRows: MeetingItem[]): void {
this.meetingRows = sourceRows.slice(0, 8);
this.meetingListLoadedKey = meetingKey;
this.meetingRefreshCount += 1;
this.meetingPending = false;
this.meetingLastReason = reason;
this.addLog(
'MeetingList',
`${reason},列表快照=${sourceRows.slice(0, 8).length} 条,刷新次数 ${this.meetingRefreshCount}`
);
}
private refreshContactList(reason: string, contactKey: number, sourceRows: ContactItem[]): void {
this.contactRows = sourceRows.slice(0, 8);
this.contactListLoadedKey = contactKey;
this.contactRefreshCount += 1;
this.contactPending = false;
this.contactLastReason = reason;
this.addLog(
'ContactList',
`${reason},列表快照=${sourceRows.slice(0, 8).length} 条,刷新次数 ${this.contactRefreshCount}`
);
}
@Builder
private TabButton(label: string, target: DemoTab) {
Button(label)
.layoutWeight(1)
.height(38)
.fontSize(13)
.fontColor(this.currentTab === target ? Color.White : '#334155')
.backgroundColor(this.currentTab === target ? '#2563EB' : '#E2E8F0')
.borderRadius(19)
.onClick(() => {
this.switchTab(target);
})
}
build() {
Scroll() {
Column({ space: 18 }) {
Column({ space: 8 }) {
Text('全局刷新信号实验')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
Text('用 MeetingReloadKey 和 ContactReloadKey 模拟真实项目里的跨页面刷新。数据源变化后,当前相关页面立即刷新自己的快照,隐藏页面等切换回来后再补刷新。')
.fontSize(14)
.fontColor('#475569')
.lineHeight(22)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Row({ space: 10 }) {
Button('新增会议')
.layoutWeight(1)
.height(42)
.fontColor(Color.White)
.backgroundColor('#2563EB')
.borderRadius(21)
.onClick(() => {
this.notifyMeetingUpdate('模拟新增会议');
})
Button('新增联系人')
.layoutWeight(1)
.height(42)
.fontColor(Color.White)
.backgroundColor('#0F766E')
.borderRadius(21)
.onClick(() => {
this.notifyContactUpdate('模拟新增联系人');
})
}
.width('100%')
Row({ space: 8 }) {
this.TabButton('工作台', DemoTab.Workbench)
this.TabButton('会议列表', DemoTab.MeetingList)
this.TabButton('联系人列表', DemoTab.ContactList)
}
.width('100%')
Column() {
if (this.currentTab === DemoTab.Workbench) {
WorkbenchDemoPanel({
meetingReloadKey: $meetingReloadKey,
contactReloadKey: $contactReloadKey,
workbenchMeetingKey: $workbenchMeetingKey,
workbenchContactKey: $workbenchContactKey,
workbenchMeetingCount: $workbenchMeetingCount,
workbenchContactCount: $workbenchContactCount,
workbenchRefreshCount: $workbenchRefreshCount,
workbenchPending: $workbenchPending,
workbenchLastReason: $workbenchLastReason
})
} else if (this.currentTab === DemoTab.MeetingList) {
MeetingListDemoPanel({
meetingReloadKey: $meetingReloadKey,
meetingListLoadedKey: $meetingListLoadedKey,
meetingRefreshCount: $meetingRefreshCount,
meetingPending: $meetingPending,
meetingLastReason: $meetingLastReason,
meetingRows: $meetingRows
})
} else {
ContactListDemoPanel({
contactReloadKey: $contactReloadKey,
contactListLoadedKey: $contactListLoadedKey,
contactRefreshCount: $contactRefreshCount,
contactPending: $contactPending,
contactLastReason: $contactLastReason,
contactRows: $contactRows
})
}
}
.width('100%')
RefreshLogPanel({
logs: $logs
})
}
.width('100%')
.padding(20)
}
.width('100%')
.height('100%')
.backgroundColor('#EEF2F7')
}
}
@Component
struct WorkbenchDemoPanel {
@Link meetingReloadKey: number;
@Link contactReloadKey: number;
@Link workbenchMeetingKey: number;
@Link workbenchContactKey: number;
@Link workbenchMeetingCount: number;
@Link workbenchContactCount: number;
@Link workbenchRefreshCount: number;
@Link workbenchPending: boolean;
@Link workbenchLastReason: string;
@Builder
private SectionTitle(title: string, desc: string) {
Column({ space: 8 }) {
Text(title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
.width('100%')
Text(desc)
.fontSize(14)
.fontColor('#475569')
.lineHeight(22)
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
@Builder
private PendingTag() {
if (this.workbenchPending) {
Text('待刷新')
.fontSize(11)
.fontColor('#B45309')
.padding({
left: 8,
right: 8,
top: 3,
bottom: 3
})
.backgroundColor('#FEF3C7')
.borderRadius(10)
}
}
@Builder
private MeetingCountCard() {
Column({ space: 6 }) {
Row() {
Text('会议统计快照')
.fontSize(13)
.fontColor('#64748B')
Blank()
this.PendingTag()
}
.width('100%')
Text(`${this.workbenchMeetingCount} 场会议`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
Text(`本地会议 key=${this.workbenchMeetingKey},全局会议 key=${this.meetingReloadKey}`)
.fontSize(12)
.fontColor('#94A3B8')
.lineHeight(18)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 10,
color: '#12000000',
offsetX: 0,
offsetY: 3
})
}
@Builder
private ContactCountCard() {
Column({ space: 6 }) {
Row() {
Text('联系人统计快照')
.fontSize(13)
.fontColor('#64748B')
Blank()
this.PendingTag()
}
.width('100%')
Text(`${this.workbenchContactCount} 位联系人`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
Text(`本地联系人 key=${this.workbenchContactKey},全局联系人 key=${this.contactReloadKey}`)
.fontSize(12)
.fontColor('#94A3B8')
.lineHeight(18)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 10,
color: '#12000000',
offsetX: 0,
offsetY: 3
})
}
@Builder
private RefreshCountCard() {
Column({ space: 6 }) {
Text('工作台刷新次数')
.fontSize(13)
.fontColor('#64748B')
Text(this.workbenchRefreshCount.toString())
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
Text(this.workbenchLastReason)
.fontSize(12)
.fontColor('#94A3B8')
.lineHeight(18)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 10,
color: '#12000000',
offsetX: 0,
offsetY: 3
})
}
build() {
Column({ space: 14 }) {
this.SectionTitle(
'工作台',
'工作台展示的是自己的统计快照。会议或联系人变化时,如果工作台当前可见,会立即接收最新数量;如果隐藏,就等切回来再补一次刷新。'
)
this.MeetingCountCard()
this.ContactCountCard()
this.RefreshCountCard()
}
.width('100%')
}
}
@Component
struct MeetingListDemoPanel {
@Link meetingReloadKey: number;
@Link meetingListLoadedKey: number;
@Link meetingRefreshCount: number;
@Link meetingPending: boolean;
@Link meetingLastReason: string;
@Link meetingRows: MeetingItem[];
@Builder
private SectionTitle(title: string, desc: string) {
Column({ space: 8 }) {
Text(title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
.width('100%')
Text(desc)
.fontSize(14)
.fontColor('#475569')
.lineHeight(22)
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
@Builder
private PendingTag() {
if (this.meetingPending) {
Text('待刷新')
.fontSize(11)
.fontColor('#B45309')
.padding({
left: 8,
right: 8,
top: 3,
bottom: 3
})
.backgroundColor('#FEF3C7')
.borderRadius(10)
}
}
@Builder
private StatCard(title: string, value: string, desc: string, pending: boolean) {
Column({ space: 6 }) {
Row() {
Text(title)
.fontSize(13)
.fontColor('#64748B')
Blank()
if (pending) {
this.PendingTag()
}
}
.width('100%')
Text(value)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(desc)
.fontSize(12)
.fontColor('#94A3B8')
.lineHeight(18)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 10,
color: '#12000000',
offsetX: 0,
offsetY: 3
})
}
@Builder
private MeetingRow(item: MeetingItem) {
Column({ space: 6 }) {
Row() {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#0F172A')
.layoutWeight(1)
Text(item.id)
.fontSize(11)
.fontColor('#64748B')
}
.width('100%')
Text(item.summary)
.fontSize(12)
.fontColor('#64748B')
.lineHeight(18)
.width('100%')
Text(`updatedAt=${item.updatedAt}`)
.fontSize(11)
.fontColor('#94A3B8')
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor('#F8FAFC')
.borderRadius(14)
}
build() {
Column({ space: 14 }) {
this.SectionTitle(
'会议列表',
'会议列表维护自己的列表快照。当前 Tab 可见时,新增会议会立即刷新这份快照;隐藏时只记录待刷新,切回来以后再读取。'
)
this.StatCard(
'全局 MeetingReloadKey',
this.meetingReloadKey.toString(),
'新增、删除、编辑会议都会推进这个版本',
false
)
this.StatCard(
'本地 lastLoadedKey',
this.meetingListLoadedKey.toString(),
'列表刷新成功后,本地版本会追平全局版本',
this.meetingPending
)
this.StatCard(
'会议列表刷新次数',
this.meetingRefreshCount.toString(),
this.meetingLastReason,
false
)
Column({ space: 10 }) {
Row() {
Text('会议列表快照')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
Blank()
Text(`${this.meetingRows.length} 条`)
.fontSize(12)
.fontColor('#64748B')
}
.width('100%')
if (this.meetingRows.length === 0) {
Text('会议列表还没有加载。切换到会议列表时会根据 MeetingReloadKey 拉取一次本地快照。')
.fontSize(13)
.fontColor('#94A3B8')
.lineHeight(20)
.width('100%')
.padding(12)
.backgroundColor('#F8FAFC')
.borderRadius(14)
} else {
ForEach(this.meetingRows, (item: MeetingItem) => {
this.MeetingRow(item)
}, (item: MeetingItem) => `${item.id}-${item.updatedAt}`)
}
}
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(18)
}
.width('100%')
}
}
@Component
struct ContactListDemoPanel {
@Link contactReloadKey: number;
@Link contactListLoadedKey: number;
@Link contactRefreshCount: number;
@Link contactPending: boolean;
@Link contactLastReason: string;
@Link contactRows: ContactItem[];
@Builder
private SectionTitle(title: string, desc: string) {
Column({ space: 8 }) {
Text(title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
.width('100%')
Text(desc)
.fontSize(14)
.fontColor('#475569')
.lineHeight(22)
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
@Builder
private PendingTag() {
if (this.contactPending) {
Text('待刷新')
.fontSize(11)
.fontColor('#B45309')
.padding({
left: 8,
right: 8,
top: 3,
bottom: 3
})
.backgroundColor('#FEF3C7')
.borderRadius(10)
}
}
@Builder
private StatCard(title: string, value: string, desc: string, pending: boolean) {
Column({ space: 6 }) {
Row() {
Text(title)
.fontSize(13)
.fontColor('#64748B')
Blank()
if (pending) {
this.PendingTag()
}
}
.width('100%')
Text(value)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(desc)
.fontSize(12)
.fontColor('#94A3B8')
.lineHeight(18)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 10,
color: '#12000000',
offsetX: 0,
offsetY: 3
})
}
@Builder
private ContactRow(item: ContactItem) {
Column({ space: 6 }) {
Row() {
Text(item.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#0F172A')
.layoutWeight(1)
Text(item.id)
.fontSize(11)
.fontColor('#64748B')
}
.width('100%')
Text(item.company)
.fontSize(12)
.fontColor('#64748B')
.lineHeight(18)
.width('100%')
Text(`updatedAt=${item.updatedAt}`)
.fontSize(11)
.fontColor('#94A3B8')
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor('#F8FAFC')
.borderRadius(14)
}
build() {
Column({ space: 14 }) {
this.SectionTitle(
'联系人列表',
'联系人列表维护自己的列表快照。新增联系人时,如果联系人列表当前可见,会立即刷新;如果隐藏,就等页面重新显示后再读取。'
)
this.StatCard(
'全局 ContactReloadKey',
this.contactReloadKey.toString(),
'新增、编辑、删除联系人都会推进这个版本',
false
)
this.StatCard(
'本地 lastLoadedKey',
this.contactListLoadedKey.toString(),
'联系人列表刷新成功后,本地版本会追平全局版本',
this.contactPending
)
this.StatCard(
'联系人列表刷新次数',
this.contactRefreshCount.toString(),
this.contactLastReason,
false
)
Column({ space: 10 }) {
Row() {
Text('联系人列表快照')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
Blank()
Text(`${this.contactRows.length} 条`)
.fontSize(12)
.fontColor('#64748B')
}
.width('100%')
if (this.contactRows.length === 0) {
Text('联系人列表还没有加载。切换到联系人列表时会根据 ContactReloadKey 拉取一次本地快照。')
.fontSize(13)
.fontColor('#94A3B8')
.lineHeight(20)
.width('100%')
.padding(12)
.backgroundColor('#F8FAFC')
.borderRadius(14)
} else {
ForEach(this.contactRows, (item: ContactItem) => {
this.ContactRow(item)
}, (item: ContactItem) => `${item.id}-${item.updatedAt}`)
}
}
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(18)
}
.width('100%')
}
}
@Component
struct RefreshLogPanel {
@Link logs: RefreshLog[];
build() {
Column({ space: 12 }) {
Text('刷新日志')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#0F172A')
.width('100%')
if (this.logs.length === 0) {
Text('还没有刷新记录')
.fontSize(13)
.fontColor('#94A3B8')
.width('100%')
.padding(14)
.backgroundColor('#F8FAFC')
.borderRadius(14)
} else {
ForEach(this.logs, (item: RefreshLog) => {
Row({ space: 10 }) {
Text(item.source)
.fontSize(11)
.fontColor('#1D4ED8')
.padding({
left: 8,
right: 8,
top: 3,
bottom: 3
})
.backgroundColor('#DBEAFE')
.borderRadius(10)
Text(item.content)
.fontSize(13)
.fontColor('#334155')
.lineHeight(20)
.layoutWeight(1)
}
.width('100%')
.alignItems(VerticalAlign.Top)
.padding(12)
.backgroundColor('#F8FAFC')
.borderRadius(14)
}, (item: RefreshLog) => item.id.toString())
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(20)
}
}
更多推荐




所有评论(0)