/** * weatherStore.ts - Zustand 全局状态管理 * * 学习要点: * 1. Zustand 的基本使用方法 * 2. 如何创建和使用 store * 3. 状态更新的不可变性(immutability) * 4. TypeScript 与 Zustand 的结合 * 5. 本地存储(localStorage)的集成 */ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { WeatherAPI } from '@/services/weatherApi'; /** * 缓存过期时间(毫秒) * 10分钟后缓存过期,需要重新获取 */ const CACHE_EXPIRY = 10 * 60 * 1000; /** * WeatherCache - 天气数据缓存项 */ export interface Location { name: string; // 城市名称 country: string; // 国家代码 region: string; // 地区/省份 lat: string; // 纬度 lon: string; // 经度 timezone_id: string; // 时区ID localtime: string; // 本地时间(YYYY-MM-DD HH:mm) localtime_epoch: number; // 本地时间Unix时间戳 utc_offset: string; // UTC偏移量 } /** * Current - 当前天气数据接口 * WeatherStack 返回的当前天气信息 */ export interface Current { observation_time: string; // 观测时间 temperature: number; // 温度(摄氏度) weather_code: number; // 天气代码 weather_icons: string[]; // 天气图标URL数组 weather_descriptions: string[]; // 天气描述数组 wind_speed: number; // 风速(km/h) wind_degree: number; // 风向角度 wind_dir: string; // 风向(如 "N", "NE") pressure: number; // 气压(mb) precip: number; // 降水量(mm) humidity: number; // 湿度(%) cloudcover: number; // 云量(%) feelslike: number; // 体感温度 uv_index: number; // 紫外线指数 visibility: number; // 能见度(km) is_day: string; // 是否白天("yes" / "no") } /** * WeatherData - 完整的天气数据接口 * 这是 WeatherStack API 返回的完整数据结构 */ export interface WeatherData { request?: { type: string; query: string; language: string; unit: string; }; location: Location; current: Current; } export interface WeatherCache { data: WeatherData; timestamp: number; } /** * WeatherStore - Zustand store 的状态接口 * 定义了全局状态管理的完整类型 */ export interface WeatherStore { // 状态 currentWeather: WeatherData | null; weatherCache: Map; // 城市名 -> 缓存数据 isLoading: boolean; error: string | null; // Actions setCurrentWeather: (weather: WeatherData | null) => void; getWeatherFromCache: (city: string) => WeatherData | null; cacheWeather: (city: string, weather: WeatherData) => void; setLoading: (loading: boolean) => void; setError: (error: string | null) => void; reset: () => void; // API Actions searchWeather: (city: string) => Promise;//查找缓存 无:查找 refreshWeather: () => Promise;//刷新 缓存 } /** * useWeatherStore - 天气应用的全局状态管理 * * Zustand 的核心概念: * 1. create() 函数创建 store * 2. set() 函数更新状态 * 3. get() 函数读取当前状态 * 4. persist 中间件实现数据持久化 * * 缓存策略: * - 使用 Map 存储城市天气数据 * - 每条数据包含时间戳,10分钟后过期 * - 优先从缓存读取,减少 API 调用 */ export const useWeatherStore = create()( persist( (set, get) => ({ // ========== 状态定义 ========== /** * currentWeather - 当前天气数据 */ currentWeather: null, /** * weatherCache - 天气数据缓存 * Key: 城市名(小写) * Value: { data, timestamp } */ weatherCache: new Map(), /** * isLoading - 加载状态 */ isLoading: false, /** * error - 错误信息 */ error: null, // ========== Action 方法定义 ========== /** * setCurrentWeather - 设置当前天气 * * @param weather - 新的天气数据 */ setCurrentWeather: (weather) => set({ currentWeather: weather }), /** * getWeatherFromCache - 从缓存获取天气数据 * * @param city - 城市名 * @returns 缓存的天气数据,如果过期或不存在则返回 null * * 学习要点: * - 使用 get() 读取当前状态 * - 缓存过期检查 * - Zustand 的纯函数特性 */ getWeatherFromCache: (city) => { const cache = get().weatherCache.get(city.toLowerCase()); if (!cache) return null; // 检查缓存是否过期 const isExpired = Date.now() - cache.timestamp > CACHE_EXPIRY; return isExpired ? null : cache.data; }, /** * cacheWeather - 缓存天气数据 * * @param city - 城市名 * @param weather - 天气数据 * * 学习要点: * - Map 的不可变更新 * - 使用 new Map() 创建新实例确保响应式更新 */ cacheWeather: (city, weather) => set((state) => { const newCache = new Map(state.weatherCache); newCache.set(city.toLowerCase(), { data: weather, timestamp: Date.now(), }); return { weatherCache: newCache }; }), /** * setLoading - 设置加载状态 */ setLoading: (loading) => set({ isLoading: loading }), /** * setError - 设置错误信息 */ setError: (error) => set({ error, isLoading: false }), /** * reset - 重置所有状态 */ reset: () => set({ currentWeather: null, isLoading: false, error: null, // 保留缓存数据 }), /** * searchWeather - 搜索天气(使用缓存策略) * * @param city - 城市名 * * 学习要点: * - 在 store 中封装 API 调用逻辑 * - 优先使用缓存,减少 API 调用 * - 统一管理 loading 和 error 状态 */ searchWeather: async (city: string) => { try { set({ isLoading: true, error: null }); // 1. 先从缓存获取 const cachedWeather = get().getWeatherFromCache(city); if (cachedWeather) { set({ currentWeather: cachedWeather, isLoading: false }); return; } // 2. 缓存未命中,调用 API const weatherData = await WeatherAPI.getCurrentWeather(city); // 3. 更新当前天气并缓存 set({ currentWeather: weatherData, isLoading: false }); get().cacheWeather(city, weatherData); } catch (err) { set({ error: err instanceof Error ? err.message : '获取天气数据失败', isLoading: false, }); } }, /** * refreshWeather - 刷新当前天气(强制刷新,忽略缓存) * * 学习要点: * - 强制刷新,不使用缓存 * - 更新缓存中的数据 */ refreshWeather: async () => { const currentWeather = get().currentWeather; if (!currentWeather) return; try { set({ isLoading: true, error: null }); const city = currentWeather.location.name; const weatherData = await WeatherAPI.getCurrentWeather(city); // 更新当前天气和缓存 set({ currentWeather: weatherData, isLoading: false }); get().cacheWeather(city, weatherData); } catch (err) { set({ error: err instanceof Error ? err.message : '刷新失败', isLoading: false, }); } }, }), { /** * persist 中间件配置 * * 学习要点: * - 持久化缓存数据到 localStorage * - Map 需要序列化为数组存储 */ name: 'weather-storage', storage: createJSONStorage(() => localStorage), partialize: (state) => ({ weatherCache: Array.from(state.weatherCache.entries()), }), // 反序列化:将数组转回 Map onRehydrateStorage: () => (state) => { if (state && Array.isArray(state.weatherCache)) { state.weatherCache = new Map(state.weatherCache as any); } }, } ) );