306 lines
8.6 KiB
TypeScript
306 lines
8.6 KiB
TypeScript
/**
|
||
* 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<string, WeatherCache>; // 城市名 -> 缓存数据
|
||
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<void>;//查找缓存 无:查找
|
||
refreshWeather: () => Promise<void>;//刷新 缓存
|
||
}
|
||
|
||
|
||
/**
|
||
* useWeatherStore - 天气应用的全局状态管理
|
||
*
|
||
* Zustand 的核心概念:
|
||
* 1. create() 函数创建 store
|
||
* 2. set() 函数更新状态
|
||
* 3. get() 函数读取当前状态
|
||
* 4. persist 中间件实现数据持久化
|
||
*
|
||
* 缓存策略:
|
||
* - 使用 Map 存储城市天气数据
|
||
* - 每条数据包含时间戳,10分钟后过期
|
||
* - 优先从缓存读取,减少 API 调用
|
||
*/
|
||
export const useWeatherStore = create<WeatherStore>()(
|
||
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);
|
||
}
|
||
},
|
||
}
|
||
)
|
||
);
|
||
|
||
|