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