小雨同学 2026-06-10 09:27:51 发布前言
我在《会议随记 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@Componentstruct 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') }}@Componentstruct 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%') }}@Componentstruct 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%') }}@Componentstruct 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%') }}@Componentstruct 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
0
start
0
0
我不是呆头
0
0
1xss
203
0
云上秋名山
167
0
小雨同学
产品总监、独立开发者社群主理人、资深全栈工程师,HarmonyOS应用开发者高级认证,PMP认证,CSDN博客专家,鸿蒙极客,Trae Fellow,阿里云社区专家博主、51CTO 博客专家、OpenTiny 优秀布道师、科大讯飞荣誉讲师。
帖子
提问
粉丝
【鸿蒙原生开发会议随记 Pro】用 NavPathStack 收拢会议页面跳转和返回刷新
2026-06-09 10:28:07 发布【鸿蒙原生开发会议随记 Pro】EntryAbility、SplashPage 与首页路由处理
2026-06-08 09:09:36 发布
京公网安备:11010502051901号