修复(core): 全局强制歌曲ID为字符串类型
引入`normalizeSongId`工具函数以确保应用内数据类型统一: - 规范化API响应数据(搜索、排行榜、歌单) - 初始化时清理LocalStorage数据(歌单、收藏、当前歌曲) - 同步远程数据时进行类型规范化,避免类型不匹配 - 修复`toggleLike`和播放状态中潜在的ID比较错误
This commit is contained in:
59
index.html
59
index.html
@@ -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,26 +1075,26 @@
|
|||||||
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);
|
||||||
setLastSuccessToken(syncToken);
|
setLastSuccessToken(syncToken);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Token switch sync failed", e);
|
console.error("Token switch sync failed", e);
|
||||||
throw e; // Let UI show error
|
throw e; // Let UI show error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 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]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user