修复(core): 全局强制歌曲ID为字符串类型

引入`normalizeSongId`工具函数以确保应用内数据类型统一:
- 规范化API响应数据(搜索、排行榜、歌单)
- 初始化时清理LocalStorage数据(歌单、收藏、当前歌曲)
- 同步远程数据时进行类型规范化,避免类型不匹配
- 修复`toggleLike`和播放状态中潜在的ID比较错误
This commit is contained in:
史悦
2026-01-09 09:55:06 +08:00
parent 186733dccf
commit 33ab93aa33

View File

@@ -231,6 +231,14 @@
return getSongDurationSeconds(song); return getSongDurationSeconds(song);
}; };
// --- ID Normalization ---
const normalizeSongId = (song) => {
if (!song || song.id === undefined || song.id === null) return song;
const id = String(song.id);
return song.id === id ? song : { ...song, id };
};
const normalizeSongList = (songs) => Array.isArray(songs) ? songs.map(normalizeSongId) : [];
// --- API Services --- // --- API Services ---
const api = { const api = {
@@ -238,8 +246,11 @@
try { try {
const res = await fetch(`${API_BASE}/api/?type=search&keyword=${encodeURIComponent(keyword)}&source=${source}&page=${page}`); const res = await fetch(`${API_BASE}/api/?type=search&keyword=${encodeURIComponent(keyword)}&source=${source}&page=${page}`);
const data = await res.json(); const data = await res.json();
// Return full data object to get total and results if (data.code === 200) {
return data.code === 200 ? data.data : { results: [], total: 0 }; const payload = data.data || {};
return { ...payload, results: normalizeSongList(payload.results) };
}
return { results: [], total: 0 };
} catch (e) { } catch (e) {
console.error("Search failed", e); console.error("Search failed", e);
return { results: [], total: 0 }; return { results: [], total: 0 };
@@ -306,7 +317,8 @@
const res = await fetch(`${API_BASE}/api/?source=${source}&id=${id}&type=toplist`); const res = await fetch(`${API_BASE}/api/?source=${source}&id=${id}&type=toplist`);
const data = await res.json(); const data = await res.json();
if (data.code === 200) { if (data.code === 200) {
return Array.isArray(data.data) ? data.data : (data.data.list || data.data.tracks || []); const list = Array.isArray(data.data) ? data.data : (data.data.list || data.data.tracks || []);
return normalizeSongList(list);
} }
return []; return [];
} catch (e) { } catch (e) {
@@ -319,7 +331,7 @@
const res = await fetch(`${API_BASE}/api/?type=playlist&id=${id}&source=${source}`); const res = await fetch(`${API_BASE}/api/?type=playlist&id=${id}&source=${source}`);
const data = await res.json(); const data = await res.json();
if (data.code === 200 && data.data && Array.isArray(data.data.list)) { if (data.code === 200 && data.data && Array.isArray(data.data.list)) {
return data.data.list; return normalizeSongList(data.data.list);
} }
return []; return [];
} catch (e) { } catch (e) {
@@ -906,18 +918,21 @@
const observerTarget = useRef(null); const observerTarget = useRef(null);
// Player State // Player State
const [playlist, setPlaylist] = useState(() => JSON.parse(localStorage.getItem('th_playlist')) || []); const [playlist, setPlaylist] = useState(() => normalizeSongList(JSON.parse(localStorage.getItem('th_playlist')) || []));
const [currentSong, setCurrentSong] = useState(() => JSON.parse(localStorage.getItem('th_current')) || null); const [currentSong, setCurrentSong] = useState(() => {
const cached = JSON.parse(localStorage.getItem('th_current'));
return cached ? normalizeSongId(cached) : null;
});
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
const [lyrics, setLyrics] = useState([]); const [lyrics, setLyrics] = useState([]);
const [mode, setMode] = useState('loop'); // loop, one, shuffle const [mode, setMode] = useState('loop'); // loop, one, shuffle
const [volume, setVolume] = useState(1); const [volume, setVolume] = useState(1);
const [favorites, setFavorites] = useState(() => JSON.parse(localStorage.getItem('th_favorites')) || []); const [favorites, setFavorites] = useState(() => normalizeSongList(JSON.parse(localStorage.getItem('th_favorites')) || []));
// Add lastSyncedFavorites to track the state of favorites at the last successful sync // Add lastSyncedFavorites to track the state of favorites at the last successful sync
// This allows us to determine what the user actually changed (added or removed) // This allows us to determine what the user actually changed (added or removed)
const [lastSyncedFavorites, setLastSyncedFavorites] = useState(() => JSON.parse(localStorage.getItem('th_favorites_synced')) || []); const [lastSyncedFavorites, setLastSyncedFavorites] = useState(() => normalizeSongList(JSON.parse(localStorage.getItem('th_favorites_synced')) || []));
const [quality, setQuality] = useState(() => localStorage.getItem('th_quality') || '320k'); const [quality, setQuality] = useState(() => localStorage.getItem('th_quality') || '320k');
const [syncToken, setSyncToken] = useState(() => localStorage.getItem('th_sync_token') || ''); const [syncToken, setSyncToken] = useState(() => localStorage.getItem('th_sync_token') || '');
const [lastSuccessToken, setLastSuccessToken] = useState(() => localStorage.getItem('th_last_success_token') || ''); const [lastSuccessToken, setLastSuccessToken] = useState(() => localStorage.getItem('th_last_success_token') || '');
@@ -1011,7 +1026,7 @@
if (!syncToken || syncToken !== lastSuccessToken) return; if (!syncToken || syncToken !== lastSuccessToken) return;
try { try {
// Get latest remote state // Get latest remote state
const remoteFavorites = await syncService.get('favorites', syncToken) || []; const remoteFavorites = normalizeSongList(await syncService.get('favorites', syncToken) || []);
// Calculate Diff: What did the user do locally since last sync? // Calculate Diff: What did the user do locally since last sync?
// Added: In Local but not in LastSynced // Added: In Local but not in LastSynced
@@ -1060,10 +1075,10 @@
if (isSwitchingToken) { if (isSwitchingToken) {
// Mode: Switch Account/Token -> Remote Overwrites Local // Mode: Switch Account/Token -> Remote Overwrites Local
try { try {
const cloudFavorites = await syncService.get('favorites', syncToken); const cloudFavorites = normalizeSongList(await syncService.get('favorites', syncToken) || []);
// If cloud has data, use it. If null/empty, we assume new empty account. // If cloud has data, use it. If null/empty, we assume new empty account.
const newFavorites = Array.isArray(cloudFavorites) ? cloudFavorites : []; const newFavorites = cloudFavorites;
setFavorites(newFavorites); setFavorites(newFavorites);
setLastSyncedFavorites(newFavorites); setLastSyncedFavorites(newFavorites);
@@ -1077,9 +1092,9 @@
// Mode: Regular Sync -> Merge / Union // Mode: Regular Sync -> Merge / Union
// Pull Remote // Pull Remote
const cloudFavorites = await syncService.get('favorites', syncToken); const cloudFavorites = normalizeSongList(await syncService.get('favorites', syncToken) || []);
if (cloudFavorites && Array.isArray(cloudFavorites)) { if (cloudFavorites.length > 0) {
// Merge: Union // Merge: Union
const merged = [...favorites]; const merged = [...favorites];
cloudFavorites.forEach(cloudSong => { cloudFavorites.forEach(cloudSong => {
@@ -1400,11 +1415,13 @@
}; };
const toggleLike = (song) => { const toggleLike = (song) => {
const exists = favorites.find(s => s.id === song.id); const normalized = normalizeSongId(song);
if (!normalized) return;
const exists = favorites.find(s => s.id === normalized.id);
if (exists) { if (exists) {
setFavorites(favorites.filter(s => s.id !== song.id)); setFavorites(favorites.filter(s => s.id !== normalized.id));
} else { } else {
setFavorites([song, ...favorites]); setFavorites([normalized, ...favorites]);
} }
}; };