import { create } from "zustand"; import { api, SearchResult, PlayRecord } from "@/services/api"; import { PlayRecordManager } from "@/services/storage"; import useAuthStore from "./authStore"; import { useSettingsStore } from "./settingsStore"; export type RowItem = (SearchResult | PlayRecord) & { id: string; source: string; title: string; poster: string; progress?: number; play_time?: number; lastPlayed?: number; episodeIndex?: number; sourceName?: string; totalEpisodes?: number; year?: string; rate?: string; }; export interface Category { title: string; type?: "movie" | "tv" | "record"; tag?: string; tags?: string[]; } const initialCategories: Category[] = [ { title: "最近播放", type: "record" }, { title: "热门剧集", type: "tv", tag: "热门" }, { title: "电视剧", type: "tv", tags: ["国产剧", "美剧", "英剧", "韩剧", "日剧", "港剧", "日本动画", "动画"] }, { title: "电影", type: "movie", tags: [ "热门", "最新", "经典", "豆瓣高分", "冷门佳片", "华语", "欧美", "韩国", "日本", "动作", "喜剧", "爱情", "科幻", "悬疑", "恐怖", ], }, { title: "综艺", type: "tv", tag: "综艺" }, { title: "豆瓣 Top250", type: "movie", tag: "top250" }, ]; // 添加缓存项接口 interface CacheItem { data: RowItem[]; timestamp: number; type: 'movie' | 'tv' | 'record'; hasMore: boolean; } const CACHE_EXPIRE_TIME = 5 * 60 * 1000; // 5分钟过期 const MAX_CACHE_SIZE = 10; // 最大缓存容量 const MAX_ITEMS_PER_CACHE = 40; // 每个缓存最大条目数 const getCacheKey = (category: Category) => { return `${category.type || 'unknown'}-${category.title}-${category.tag || ''}`; }; const isValidCache = (cacheItem: CacheItem) => { return Date.now() - cacheItem.timestamp < CACHE_EXPIRE_TIME; }; interface HomeState { categories: Category[]; selectedCategory: Category; contentData: RowItem[]; loading: boolean; loadingMore: boolean; pageStart: number; hasMore: boolean; error: string | null; fetchInitialData: () => Promise; loadMoreData: () => Promise; selectCategory: (category: Category) => void; refreshPlayRecords: () => Promise; clearError: () => void; } // 内存缓存,应用生命周期内有效 const dataCache = new Map(); const useHomeStore = create((set, get) => ({ categories: initialCategories, selectedCategory: initialCategories[0], contentData: [], loading: true, loadingMore: false, pageStart: 0, hasMore: true, error: null, fetchInitialData: async () => { const { apiBaseUrl } = useSettingsStore.getState(); await useAuthStore.getState().checkLoginStatus(apiBaseUrl); const { selectedCategory } = get(); const cacheKey = getCacheKey(selectedCategory); // 最近播放不缓存,始终实时获取 if (selectedCategory.type === 'record') { set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null }); await get().loadMoreData(); return; } // 检查缓存 if (dataCache.has(cacheKey) && isValidCache(dataCache.get(cacheKey)!)) { const cachedData = dataCache.get(cacheKey)!; set({ loading: false, contentData: cachedData.data, pageStart: cachedData.data.length, hasMore: cachedData.hasMore, error: null }); return; } set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null }); await get().loadMoreData(); }, loadMoreData: async () => { const { selectedCategory, pageStart, loadingMore, hasMore } = get(); if (loadingMore || !hasMore) return; if (pageStart > 0) { set({ loadingMore: true }); } try { if (selectedCategory.type === "record") { const { isLoggedIn } = useAuthStore.getState(); if (!isLoggedIn) { set({ contentData: [], hasMore: false }); return; } const records = await PlayRecordManager.getAll(); const rowItems = Object.entries(records) .map(([key, record]) => { const [source, id] = key.split("+"); return { ...record, id, source, progress: record.play_time / record.total_time, poster: record.cover, sourceName: record.source_name, episodeIndex: record.index, totalEpisodes: record.total_episodes, lastPlayed: record.save_time, play_time: record.play_time, }; }) // .filter((record) => record.progress !== undefined && record.progress > 0 && record.progress < 1) .sort((a, b) => (b.lastPlayed || 0) - (a.lastPlayed || 0)); set({ contentData: rowItems, hasMore: false }); } else if (selectedCategory.type && selectedCategory.tag) { const result = await api.getDoubanData( selectedCategory.type, selectedCategory.tag, 20, pageStart ); const newItems = result.list.map((item) => ({ ...item, id: item.title, source: "douban", })) as RowItem[]; const cacheKey = getCacheKey(selectedCategory); if (pageStart === 0) { // 清理过期缓存 for (const [key, value] of dataCache.entries()) { if (!isValidCache(value)) { dataCache.delete(key); } } // 如果缓存太大,删除最旧的项 if (dataCache.size >= MAX_CACHE_SIZE) { const oldestKey = Array.from(dataCache.keys())[0]; dataCache.delete(oldestKey); } // 限制缓存的数据条目数,但不限制显示的数据 const cacheItems = newItems.slice(0, MAX_ITEMS_PER_CACHE); // 存储新缓存 dataCache.set(cacheKey, { data: cacheItems, timestamp: Date.now(), type: selectedCategory.type, hasMore: true // 始终为 true,因为我们允许继续加载 }); set({ contentData: newItems, // 使用完整的新数据 pageStart: newItems.length, hasMore: result.list.length !== 0, }); } else { // 增量加载时更新缓存 const existingCache = dataCache.get(cacheKey); if (existingCache) { // 只有当缓存数据少于最大限制时才更新缓存 if (existingCache.data.length < MAX_ITEMS_PER_CACHE) { const updatedData = [...existingCache.data, ...newItems]; const limitedCacheData = updatedData.slice(0, MAX_ITEMS_PER_CACHE); dataCache.set(cacheKey, { ...existingCache, data: limitedCacheData, hasMore: true // 始终为 true,因为我们允许继续加载 }); } } // 更新状态时使用所有数据 set((state) => ({ contentData: [...state.contentData, ...newItems], pageStart: state.pageStart + newItems.length, hasMore: result.list.length !== 0, })); } } else if (selectedCategory.tags) { // It's a container category, do not load content, but clear current content set({ contentData: [], hasMore: false }); } else { set({ hasMore: false }); } } catch (err: any) { let errorMessage = "加载失败,请重试"; if (err.message === "API_URL_NOT_SET") { errorMessage = "请点击右上角设置按钮,配置您的服务器地址"; } else if (err.message === "UNAUTHORIZED") { errorMessage = "认证失败,请重新登录"; } else if (err.message.includes("Network")) { errorMessage = "网络连接失败,请检查网络连接"; } else if (err.message.includes("timeout")) { errorMessage = "请求超时,请检查网络或服务器状态"; } else if (err.message.includes("404")) { errorMessage = "服务器API路径不正确,请检查服务器配置"; } else if (err.message.includes("500")) { errorMessage = "服务器内部错误,请联系管理员"; } else if (err.message.includes("403")) { errorMessage = "访问被拒绝,请检查权限设置"; } set({ error: errorMessage }); } finally { set({ loading: false, loadingMore: false }); } }, selectCategory: (category: Category) => { const currentCategory = get().selectedCategory; const cacheKey = getCacheKey(category); if (currentCategory.title !== category.title || currentCategory.tag !== category.tag) { set({ selectedCategory: category, contentData: [], pageStart: 0, hasMore: true, error: null }); if (category.type === 'record') { get().fetchInitialData(); return; } const cachedData = dataCache.get(cacheKey); if (cachedData && isValidCache(cachedData)) { set({ contentData: cachedData.data, pageStart: cachedData.data.length, hasMore: cachedData.hasMore, loading: false }); } else { // 删除过期缓存 if (cachedData) { dataCache.delete(cacheKey); } get().fetchInitialData(); } } }, refreshPlayRecords: async () => { const { apiBaseUrl } = useSettingsStore.getState(); await useAuthStore.getState().checkLoginStatus(apiBaseUrl); const { isLoggedIn } = useAuthStore.getState(); if (!isLoggedIn) { set((state) => { const recordCategoryExists = state.categories.some((c) => c.type === "record"); if (recordCategoryExists) { const newCategories = state.categories.filter((c) => c.type !== "record"); if (state.selectedCategory.type === "record") { get().selectCategory(newCategories[0] || null); } return { categories: newCategories }; } return {}; }); return; } const records = await PlayRecordManager.getAll(); const hasRecords = Object.keys(records).length > 0; set((state) => { const recordCategoryExists = state.categories.some((c) => c.type === "record"); if (hasRecords && !recordCategoryExists) { return { categories: [initialCategories[0], ...state.categories] }; } if (!hasRecords && recordCategoryExists) { const newCategories = state.categories.filter((c) => c.type !== "record"); if (state.selectedCategory.type === "record") { get().selectCategory(newCategories[0] || null); } return { categories: newCategories }; } return {}; }); get().fetchInitialData(); }, clearError: () => { set({ error: null }); }, })); export default useHomeStore;