【鸿蒙原生开发会议随记 Pro】用 AppStorage 做会议列表和联系人列表的刷新信号 原创
头像 小雨同学 2026-06-10 09:27:51    发布
1 浏览 0 点赞 0 收藏


前言

我在《会议随记 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 里同时保留了 reloadKeycurrentTabIndexlastLoadedKey,这些状态一起决定页面是否需要重新加载。

@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。这一步很重要,因为页面只有在真正加载成功后,才能说自己已经同步到最新版本。

我在这里会保留一个页面状态原则:页面层负责判断刷新时机,子组件只负责展示数据。刷新信号可以全局共享,但分页、搜索、筛选、选中项这些状态不要交给全局工具类处理。工具类一旦知道页面怎么分页、怎么筛选、怎么展示,后面每多一个页面,刷新工具都会变得越来越重。


状态放置位置原因
MeetingReloadKeyAppStorage多个页面都要知道会议数据变化
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。

这个小页面和真实项目的对应关系如下。


小页面里的内容真实项目里的位置
meetingReloadKeyMeetingReloadKey
contactReloadKeyContactReloadKey
meetingListLoadedKey会议列表里的 lastLoadedKey
contactListLoadedKey联系人列表里的 lastLoadedKey
checkMeetingList()会议列表里的 checkAndLoad()
switchTab()主 Tab 切换后触发页面检查

这个演示页要观察两条路径。

第一条路径是会议数据变化。停留在工作台,点击新增会议,工作台当前可见,会立即同步会议统计;会议列表不在当前 Tab,只记录待刷新。切换到会议列表以后,会议列表发现本地 lastLoadedKey 落后于全局 MeetingReloadKey,再执行刷新。

第二条路径是联系人数据变化。停留在会议列表,点击新增联系人。会议列表不会刷新,因为它只关心会议数据。工作台和联系人列表会记录联系人数据变化。切换到联系人列表以后,联系人列表会根据 ContactReloadKey 完成延迟刷新。

这两个路径能说明一个页面状态原则:全局信号只推动版本变化,页面加载仍然属于页面自己的职责。如果把查询逻辑写进 RefreshUtil,工具类就会知道会议列表怎么分页、联系人列表怎么搜索、工作台怎么统计,边界会越来越模糊。

五、迁回项目时保留边界

回到真实项目时,这个小页面里的按钮和模拟数据都要删掉,保留这套刷新关系就够了。


小页面里的逻辑真实项目里的处理
meetingReloadKey += 1RefreshUtil.notifyMeetingUpdate()
contactReloadKey += 1RefreshUtil.notifyContactUpdate()
页面本地 lastLoadedKeyMeetingListPage、ContactListPage、ProjectListPage 内部版本记录
Tab 切换后检查版本onTabIndexChange()
立即刷新和延迟刷新onReloadKeyChanged() 里的可见性判断
refreshWorkbench()工作台重新计算会议和联系人统计
refreshMeetingList()会议列表重新查询分页数据
refreshContactList()联系人列表重新查询联系人数据

这个刷新机制适合列表、统计、选择器这类读多写少的页面。它不适合直接覆盖正在编辑的表单。比如用户正在编辑会议标题,另一个入口发出了 MeetingReloadKey,编辑页不能马上把输入框覆盖掉。更稳的处理方式是提示数据可能变化,或者在保存前做冲突判断。

我会把刷新信号看成一个版本提醒,而不是强制同步命令。它提醒页面某类数据发生过变化,页面要不要刷新、什么时候刷新、怎么保留当前输入,都应该由页面自己决定。这样 AppStorage 的职责会保持很轻,页面也不会被全局信号牵着走。

后续如果项目里出现更多数据域,比如录音文件上传状态、转写任务状态、云端同步状态,我不会继续把所有内容都塞进 reloadKey。列表刷新仍然可以保留轻量 key,任务进度和同步状态则要用更明确的数据结构来表达。千万不要把刷新信号写成万能事件中心。

总结

这套刷新信号实现思路适合《会议随记 Pro》当前阶段。它没有试图接管所有页面状态,只是让会议、联系人、项目这几类数据变化能被相关页面感知到。RefreshUtil 推进 MeetingReloadKeyContactReloadKeyProjectReloadKey,页面通过 @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)  }}


©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS

暂无评论数据

加载中...

发布

头像

小雨同学

产品总监、独立开发者社群主理人、资深全栈工程师,HarmonyOS应用开发者高级认证,PMP认证,CSDN博客专家,鸿蒙极客,Trae Fellow,阿里云社区专家博主、51CTO 博客专家、OpenTiny 优秀布道师、科大讯飞荣誉讲师。

23

帖子

0

提问

145

粉丝

关注
热门推荐
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 商务内容合作QQ:2291221 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255