From 8344bdf14c5a618159d72f7c3cee73e4da4c45b0 Mon Sep 17 00:00:00 2001
From: qiuchenfan <2035024011@qq.com>
Date: Mon, 17 Nov 2025 11:13:06 +0800
Subject: [PATCH] 1117
---
1114/app/components/weather/WeatherCard.tsx | 12 +
.../components/weather/WeatherSearchForm.tsx | 195 +++++------
1114/app/store/weatherStore.ts | 305 ++++++++++++++++++
3 files changed, 415 insertions(+), 97 deletions(-)
diff --git a/1114/app/components/weather/WeatherCard.tsx b/1114/app/components/weather/WeatherCard.tsx
index dc1648d..1d2383a 100644
--- a/1114/app/components/weather/WeatherCard.tsx
+++ b/1114/app/components/weather/WeatherCard.tsx
@@ -1,3 +1,13 @@
+/**
+ * WeatherCard.tsx - 统一的天气卡片组件
+ * 集成搜索表单、天气展示、搜索历史于一体
+ *
+ * 学习要点:
+ * - 直接使用 store,无需通过 props 传递
+ * - 组件更加独立和可复用
+ * - 减少 prop drilling
+ */
+
import React, { useEffect } from 'react';
import { WeatherSearchForm } from './WeatherSearchForm';
import { WeatherDisplay } from './WeatherDisplay';
@@ -17,6 +27,7 @@ export function WeatherCard() {
{currentWeather ? (
+
) : (
@@ -38,3 +49,4 @@ export function WeatherCard() {
);
}
+
diff --git a/1114/app/components/weather/WeatherSearchForm.tsx b/1114/app/components/weather/WeatherSearchForm.tsx
index b9db5c1..b4b5a83 100644
--- a/1114/app/components/weather/WeatherSearchForm.tsx
+++ b/1114/app/components/weather/WeatherSearchForm.tsx
@@ -1,104 +1,105 @@
-//天气搜索表单组件
-import { cn } from "@/lib/utils";
-import React ,{useState,type FormEvent,type ChangeEvent, use} from "react";
-import { Button } from "@/components/ui/button";
-import { Input} from "../ui/input";
-import {useWeatherStore} from "@/store/weatherStore";
-import { AlertCircle, Loader2, RefreshCw, Search } from "lucide-react";
-//城市输入验证搜索刷新
-export function WeatherSearchForm() {
-const {isLoading,currentWeather,setCurrentWeather,refreshWeather,searchWeather} = useWeatherStore();
-const [city, setCity] = useState('');
-const [inputError, setInputError] = useState('');
-//输入框内容
-const handleInputChange = (event: ChangeEvent) => {
- setCity(event.target.value);
- setInputError('');
-};
-//表单提交验证
-const handleSubmit = async (e: FormEvent) => {
+import React, { useState, type FormEvent, type ChangeEvent } from 'react';
+import { Search, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { cn } from '@/lib/utils';
+import { useWeatherStore } from '@/store/weatherStore';
+
+export function WeatherSearchForm() {
+ const { isLoading, currentWeather, searchWeather, refreshWeather } = useWeatherStore();
+ const [city, setCity] = useState('');
+ const [inputError, setInputError] = useState('');
+
+ const handleInputChange = (e: ChangeEvent) => {
+ setCity(e.target.value);
+ if (inputError) setInputError('');
+ };
+
+ const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const trimmed = city.trim();
-//创建axios实例 统一请求头
-const apiClient: AxiosInstance = axios.create({
- baseURL: BASE_URL,
- timeout: 10000,
- headers: {
- 'Content-Type': 'application/json',
- },
-});
-//请求拦截器
-apiClient.interceptors.request.use(
- (config) => {
- // 在发送请求之前做些什么
- config.params = {
- ...config.params,
- access_key: API_KEY,
- };
- //打印请求信息
- if (process.env.NODE_ENV === 'development') {
- console.log('API Request:', config.method, config.url, config.params);
+ if (!trimmed) {
+ setInputError('请输入城市名称');
+ return;
}
- return config;
- },
- (error) => {
- // 对请求错误做些什么
- console.error('Request Error:', error);
- return Promise.reject(error);
- }
-);
-// 添加响应拦截器
-apiClient.interceptors.response.use(
- (response) => {
- //状态为200时的错误
- if (response.data && response.data.error ) {
- const error = response.data.error;
- console.error('API Error:', error);
- throw new Error(error.info || '请求失败');
- }//打印响应
- if (process.env.NODE_ENV === 'development') {
- console.log('API Response:', response.status, response.data);
+ if (trimmed.length < 2) {
+ setInputError('城市名称至少需要2个字符');
+ return;
}
- return response;
- },
- (error:AxiosError) => {
- //统一错误处理
- console.error('Response Error:', error.message);
- if (error.response) {
- const status = error.response.status;
- switch (status) {
- case 404:
- return Promise.reject(new Error('未找到该城市,请检查城市名称'));
- case 401:
- return Promise.reject(new Error('API认证失败,请检查API密钥'));
- case 403:
- return Promise.reject(new Error('访问被拒绝,请稍后再试'));
- case 429:
- return Promise.reject(new Error('请求次数过多,请稍后再试'));
- default:
- return Promise.reject(new Error('服务器错误,请稍后再试'));
- }
- }else if(error.request){
- return Promise.reject(new Error('网络连接失败,请检查网络状态'));
- }else {
- return Promise.reject(new Error('请求配置错误'));
- }
- }
-);
-//WeatherAPI
-export class WeatherAPI {
- static async getCurrentWeather(city: string): Promise {
- try {
- const response = await apiClient.get(
- '/current',
- {params : {query: city },
- });
- return response.data;
- } catch (error) {
- throw error;
- }
- }
+ if (!/^[\u4e00-\u9fa5a-zA-Z\s-]+$/.test(trimmed)) {
+ setInputError('城市名称包含无效字符');
+ return;
}
- export default apiClient;
+
+ await searchWeather(trimmed);
+ };
+
+ return (
+
+ );
+}
+
diff --git a/1114/app/store/weatherStore.ts b/1114/app/store/weatherStore.ts
index e69de29..ed40d15 100644
--- a/1114/app/store/weatherStore.ts
+++ b/1114/app/store/weatherStore.ts
@@ -0,0 +1,305 @@
+/**
+ * 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);
+ }
+ },
+ }
+ )
+);
+
+