From b63f94fed45e2de2003a121f764f20dfadf211ca Mon Sep 17 00:00:00 2001 From: can4hou6joeng4 Date: Fri, 31 Jul 2026 18:28:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(sync):=20=E4=B8=A2=E5=BC=83=E7=BC=BA?= =?UTF-8?q?=E5=B0=91=20videoId=20=E7=9A=84=E5=90=8C=E6=AD=A5=E8=AE=B0?= =?UTF-8?q?=E5=BD=95,=E9=81=BF=E5=85=8D=E6=95=B4=E9=A1=B5=E5=B4=A9?= =?UTF-8?q?=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 服务端同步恢复回来的记录不保证结构完整(旧结构或半截写入)。缺少 videoId 的记录会让 HistoryItem.tsx:24 与 FavoritesItem.tsx:21 在渲染期拼播放地址 时对 undefined 调用 toString(),抛出未捕获异常。由于 getVideoUrl() 是在 渲染 href 时调用的,React 会直接卸载整棵树,首页白屏且刷新无效——该账号 只能靠直接改库才能恢复。 新增 lib/utils/sync-records.ts 做可渲染性判定(要求 videoId、source、 title 齐备),在云端拉取入口 useCloudSync 与 HistoryList / FavoritesList 各过滤一次:脏数据进不来,已经落库的也渲染不出来。 --- components/favorites/FavoritesList.tsx | 3 ++- components/history/HistoryList.tsx | 3 ++- lib/hooks/useCloudSync.ts | 12 ++++++---- lib/utils/sync-records.ts | 32 ++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 lib/utils/sync-records.ts diff --git a/components/favorites/FavoritesList.tsx b/components/favorites/FavoritesList.tsx index 52e8c43..ff8a058 100644 --- a/components/favorites/FavoritesList.tsx +++ b/components/favorites/FavoritesList.tsx @@ -5,6 +5,7 @@ import type { FavoriteItem } from '@/lib/types'; import { FavoritesItem } from './FavoritesItem'; import { FavoritesEmptyState } from './FavoritesEmptyState'; +import { keepRenderableFavorites } from '@/lib/utils/sync-records'; interface FavoritesListProps { favorites: FavoriteItem[]; @@ -19,7 +20,7 @@ export function FavoritesList({ favorites, onRemove, isPremium = false }: Favori return (
- {favorites.map((item) => ( + {keepRenderableFavorites(favorites).map((item) => ( ) : (
- {history.map((item) => ( + {keepRenderableHistory(history).map((item) => ( 0) { - historyStore.getState().importHistory(result.data.history); + const history = keepRenderableHistory(result.data.history); + const favorites = keepRenderableFavorites(result.data.favorites); + + if (history.length > 0) { + historyStore.getState().importHistory(history); } - if (result.data.favorites?.length > 0) { - favoritesStore.getState().importFavorites(result.data.favorites); + if (favorites.length > 0) { + favoritesStore.getState().importFavorites(favorites); } } } catch (error) { diff --git a/lib/utils/sync-records.ts b/lib/utils/sync-records.ts new file mode 100644 index 0000000..0d3dc7c --- /dev/null +++ b/lib/utils/sync-records.ts @@ -0,0 +1,32 @@ +import type { FavoriteItem, VideoHistoryItem } from '@/lib/types'; + +function hasIdentity(value: unknown): boolean { + if (!value || typeof value !== 'object') return false; + const record = value as { videoId?: unknown; source?: unknown; title?: unknown }; + const videoId = record.videoId; + const hasVideoId = typeof videoId === 'string' ? videoId.length > 0 : typeof videoId === 'number'; + + return hasVideoId && typeof record.source === 'string' && typeof record.title === 'string'; +} + +/** + * Records restored from server-side sync are not guaranteed to be well formed — + * they may come from an older schema or a partial write. Rendering one without + * `videoId` throws while building the player URL, which takes down the whole + * page, so anything lacking a usable identity is dropped on the way in. + */ +export function isRenderableHistoryItem(value: unknown): value is VideoHistoryItem { + return hasIdentity(value); +} + +export function isRenderableFavoriteItem(value: unknown): value is FavoriteItem { + return hasIdentity(value); +} + +export function keepRenderableHistory(items: unknown): VideoHistoryItem[] { + return Array.isArray(items) ? items.filter(isRenderableHistoryItem) : []; +} + +export function keepRenderableFavorites(items: unknown): FavoriteItem[] { + return Array.isArray(items) ? items.filter(isRenderableFavoriteItem) : []; +}