實(shí)戰(zhàn)(11):護(hù)照收藏與進(jìn)度統(tǒng)計(jì))
一、把探索行為收斂成一份可恢復(fù)的進(jìn)度文化知識(shí)應(yīng)用中的“收藏”并不只是給卡片切換一個(gè)圖標(biāo)。山海萬靈把一次神獸發(fā)現(xiàn)同時(shí)映射到圖鑒收錄、地域探索比例、護(hù)照印章和下次進(jìn)入時(shí)的區(qū)域位置。如果這些結(jié)果由各頁面各自保存重復(fù)發(fā)現(xiàn)、切換區(qū)域和重啟恢復(fù)很容易出現(xiàn)不一致。工程把可變的探索數(shù)據(jù)定義為ShanhaiLocalProgress版本號(hào)、已發(fā)現(xiàn)神獸 ID、已選地域、已選展廳與更新時(shí)間放在同一份記錄中。圖鑒資料、地域資料和展廳資料仍然屬于啟動(dòng)數(shù)據(jù)不與用戶進(jìn)度混存。這樣重置進(jìn)度只影響本機(jī)探索記錄不會(huì)刪除知識(shí)內(nèi)容或遠(yuǎn)端資料。export interface ShanhaiLocalProgress { schemaVersion: number; discoveredIds: string[]; selectedRegionId: string; selectedHallId: string; updatedAt: number; } export function createEmptyShanhaiLocalProgress(): ShanhaiLocalProgress { return { schemaVersion: SHANHAI_LOCAL_PROGRESS_SCHEMA_VERSION, discoveredIds: [], selectedRegionId: , selectedHallId: , updatedAt: 0 }; }二、Preferences 只保存原子字段領(lǐng)域狀態(tài)保持完整形狀倉儲(chǔ)以shanhai_local_progress作為 Preferences 文件名分別寫入 schema、發(fā)現(xiàn) ID 串、地域 ID、展廳 ID 和更新時(shí)間。ID 在寫入前去重并以分隔符序列化讀取失敗、類型不符或存儲(chǔ)不可用時(shí)返回空進(jìn)度而不是把異常值直接交給頁面。async save(progress: ShanhaiLocalProgress): Promisevoid { const store: preferences.Preferences | null await this.getStore(); if (store null) { return; } await store.put(KEY_SCHEMA_VERSION, SHANHAI_LOCAL_PROGRESS_SCHEMA_VERSION); await store.put(KEY_DISCOVERED_IDS, this.serializeIds(progress.discoveredIds)); await store.put(KEY_SELECTED_REGION_ID, progress.selectedRegionId); await store.put(KEY_SELECTED_HALL_ID, progress.selectedHallId); await store.put(KEY_UPDATED_AT, progress.updatedAt); await store.flush(); }這里有兩個(gè)邊界需要保留。第一flush()使一次用戶動(dòng)作對應(yīng)一次明確的落盤完成點(diǎn)第二倉儲(chǔ)層吞掉的是持久化異常頁面仍可繼續(xù)瀏覽當(dāng)前內(nèi)容并由同步狀態(tài)提示后續(xù)動(dòng)作。頁面不需要知道鍵名或序列化格式。三、發(fā)現(xiàn)神獸時(shí)一次性更新圖鑒、護(hù)照和位置ShanhaiAppViewModel.discoverBeast是狀態(tài)更新入口。它先判斷目標(biāo)是否存在再判斷是否已經(jīng)發(fā)現(xiàn)新發(fā)現(xiàn)的 ID 只追加到復(fù)制后的數(shù)組。隨后把神獸所屬地域和展廳寫為當(dāng)前探索位置持久化后用啟動(dòng)數(shù)據(jù)與本地進(jìn)度重新生成聚合結(jié)果。const alreadyDiscovered: boolean this.localProgress.discoveredIds.indexOf(beast.id) 0; const discoveredIds: string[] this.localProgress.discoveredIds.slice(); if (!alreadyDiscovered) { discoveredIds.push(beast.id); } this.localProgress this.normalizeLocalProgress(bootstrap, { schemaVersion: SHANHAI_LOCAL_PROGRESS_SCHEMA_VERSION, discoveredIds: discoveredIds, selectedRegionId: beast.regionId, selectedHallId: beast.hallId, updatedAt: Date.now() }); await this.persistLocalProgress(); const mergedBootstrap: ShanhaiBootstrap this.applyLocalProgress(bootstrap, this.localProgress);這種順序避免了“頁面先改了計(jì)數(shù)、存儲(chǔ)失敗后無法恢復(fù)”的雙源狀態(tài)。重復(fù)發(fā)現(xiàn)不會(huì)再次增加記錄但仍會(huì)返回已有發(fā)現(xiàn)的提示無效 ID 則保持原進(jìn)度不變。四、進(jìn)度和護(hù)照印章都由發(fā)現(xiàn)記錄推導(dǎo)地域進(jìn)度不單獨(dú)保存百分比。視圖模型遍歷地域的beastIds統(tǒng)計(jì)已經(jīng)發(fā)現(xiàn)的數(shù)量再計(jì)算百分比和“已收錄 x/y”狀態(tài)。護(hù)照印章也遵循同一條規(guī)則一個(gè)地域的全部神獸都在discoveredIds中時(shí)該地域才進(jìn)入passportRegionIds。private derivePassportRegionIds(regions: RegionItem[], discoveredIds: string[]): string[] { return regions .filter((region: RegionItem) region.beastIds.length 0 region.beastIds.every((beastId: string) discoveredIds.indexOf(beastId) 0)) .map((region: RegionItem) region.id); } private applyDiscoveryProgressToRegions(regions: RegionItem[], discoveredIds: string[]): RegionItem[] { return regions.map((region: RegionItem): RegionItem { const discoveredCount: number region.beastIds .filter((beastId: string) discoveredIds.indexOf(beastId) 0).length; const progress: number region.beastIds.length 0 ? 0 : Math.round(discoveredCount * 100 / region.beastIds.length); return { ...region, progress: progress, status: 已收錄 discoveredCount / region.beastIds.length }; }); }從同一個(gè)事實(shí)源派生統(tǒng)計(jì)能保證檔案頁的“已發(fā)現(xiàn)”和“護(hù)照印章”與地圖卡片的區(qū)域進(jìn)度始終對應(yīng)。地域沒有目錄時(shí)進(jìn)度固定為 0避免除零和虛假的完成印章。五、頁面只消費(fèi)結(jié)果并保留可見的失敗出口本次驗(yàn)證安裝了當(dāng)前構(gòu)建的山海萬靈包在圖鑒中完成九尾狐的發(fā)現(xiàn)動(dòng)作后回到檔案頁。頁面回讀顯示“已發(fā)現(xiàn) 3 種神獸”和“護(hù)照印章 1 枚”說明發(fā)現(xiàn)記錄已經(jīng)驅(qū)動(dòng)統(tǒng)計(jì)和印章結(jié)果而不是只改變詳情頁上的局部控件。隨后結(jié)束應(yīng)用進(jìn)程并重新啟動(dòng)同一包檔案頁仍保留 3 條發(fā)現(xiàn)記錄、1 枚護(hù)照印章以及最近探索地域“青丘”。這只驗(yàn)證本機(jī) Preferences 的恢復(fù)鏈路本文不把它擴(kuò)展為云端同步、跨設(shè)備遷移或網(wǎng)絡(luò)請求成功的結(jié)論。驗(yàn)收時(shí)還應(yīng)把“發(fā)現(xiàn)”和“完成地域”分開觀察前者直接增加 discoveredIds后者要等一個(gè)地域目錄中的全部神獸都已收錄才產(chǎn)生護(hù)照印章。這樣既能避免單次發(fā)現(xiàn)被誤記為地域完成也讓 UI 上的兩個(gè)數(shù)字能追溯到不同的派生規(guī)則。若存儲(chǔ)層暫時(shí)不可用應(yīng)用仍可保留本次會(huì)話中的知識(shí)瀏覽和狀態(tài)展示下一次啟動(dòng)按空進(jìn)度恢復(fù)不把未落盤的數(shù)據(jù)偽裝成持久結(jié)果。頁面層調(diào)用syncDiscovery后回寫discoveredIds、passportRegionIds、地域和展廳數(shù)據(jù)。新出現(xiàn)的印章會(huì)被標(biāo)記為高亮目標(biāo)本地 fallback 狀態(tài)會(huì)轉(zhuǎn)換為頁面可讀的同步提示。失敗時(shí)不偽造成功結(jié)果而是保留當(dāng)前內(nèi)容并顯示重試路徑。檔案頁把發(fā)現(xiàn)數(shù)和護(hù)照印章數(shù)作為明確的Prop傳入統(tǒng)計(jì)面板統(tǒng)計(jì)組件不讀取 Preferences。這樣窄屏、寬屏和主題切換只影響排版不會(huì)改變進(jìn)度計(jì)算。場景進(jìn)度寫入頁面結(jié)果首次發(fā)現(xiàn)神獸追加 ID、更新地域與展廳位置圖鑒、地域進(jìn)度和護(hù)照候選一起重算重復(fù)發(fā)現(xiàn)保留原 ID 集合不重復(fù)計(jì)數(shù)繼續(xù)展示關(guān)聯(lián)探索提示存儲(chǔ)讀取異常使用空進(jìn)度知識(shí)資料仍可瀏覽進(jìn)度從初始狀態(tài)開始本地進(jìn)度重置清空進(jìn)度倉儲(chǔ)圖鑒資料、個(gè)人檔案和遠(yuǎn)端數(shù)據(jù)保持不變六、驗(yàn)收要覆蓋恢復(fù)、重復(fù)動(dòng)作和局部重置這條鏈路的驗(yàn)收不應(yīng)只看一個(gè)數(shù)字。應(yīng)先在圖鑒中完成一次發(fā)現(xiàn)確認(rèn)檔案統(tǒng)計(jì)、地域進(jìn)度和護(hù)照印章基于同一條記錄更新再重啟應(yīng)用檢查發(fā)現(xiàn) ID、當(dāng)前地域和展廳位置是否恢復(fù)。隨后重復(fù)觸發(fā)同一神獸確認(rèn)計(jì)數(shù)不增加最后打開本地進(jìn)度重置確認(rèn)框檢查取消不會(huì)變更記錄確認(rèn)重置后只回到探索初始狀態(tài)。對于網(wǎng)絡(luò)數(shù)據(jù)源不可用的場景繼續(xù)檢查本地 fallback 是否保留可瀏覽內(nèi)容并讓同步橫幅提供重試入口。這樣持久化失敗、無效目標(biāo)和重復(fù)動(dòng)作都有明確的狀態(tài)邊界而不是由頁面臨時(shí)補(bǔ)丁掩蓋。七、總結(jié)山海萬靈用一份版本化的本地進(jìn)度記錄承載發(fā)現(xiàn)、地域和展廳選擇Preferences 負(fù)責(zé)持久化原子字段視圖模型負(fù)責(zé)歸一化、去重和派生統(tǒng)計(jì)ArkUI 頁面只展示已經(jīng)聚合的結(jié)果。收藏、護(hù)照印章與探索進(jìn)度因此共享同一條數(shù)據(jù)鏈路重置也能準(zhǔn)確限定在本機(jī)探索狀態(tài)內(nèi)。