Merge branch 'main' of http://113.45.67.59:3003/lzq/test
This commit is contained in:
commit
4bf2c78132
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useWeatherStore } from "@/store/weatherStore";
|
||||||
|
import { Cloud } from "lucide-react";
|
||||||
|
|
||||||
|
export function WeatherCard(){
|
||||||
|
const{ currentWeather} = useWeatherStore();
|
||||||
|
//样式待修改
|
||||||
|
return (
|
||||||
|
<div className ="relative overflow-hidden rounded-3xl bg-white/80 dark:bg-slate-800/80
|
||||||
|
backdrop-blur-xl border border-slate-200/80 dark:border-slate-700/80 shadow-2xl shadow-slate-200/50
|
||||||
|
dark:shadow-slate-900/50 transition-all duration-300">
|
||||||
|
<WeatherSearchForm />
|
||||||
|
{currentWeather ? (
|
||||||
|
<div className="p-6">
|
||||||
|
<WeatherDisplay />
|
||||||
|
<WeatherDetailsGrid />
|
||||||
|
</div>
|
||||||
|
) :(
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
|
||||||
|
<div className="relative mb-6">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-400 to-cyan-400 rounded-full opacity-10 blur-2xl"></div>
|
||||||
|
<div className="relative w-20 h-20 rounded-full bg-gradient-to-br from-blue-100 to-cyan-100 dark:from-blue-900/30 dark:to-cyan-900/30 flex items-center justify-center">
|
||||||
|
<Cloud className="w-10 h-10 text-blue-500 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-700 dark:text-slate-200 mb-2">
|
||||||
|
暂无天气数据
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
请搜索城市查看实时天气信息
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { Cloud, Droplets, Gauge, ThermometerSun, Wind } from "lucide-react";
|
||||||
|
import { useWeatherStore } from "@/store/weatherStore";
|
||||||
|
|
||||||
interface WeatherDetailsGridProps {
|
interface WeatherDetailsGridProps {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -5,3 +8,53 @@ interface WeatherDetailsGridProps {
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WeatherInfoItem({ icon, label, value, subtitle }: WeatherDetailsGridProps) {
|
||||||
|
return(
|
||||||
|
<div className="group rounded-xl bg-slate-50/50 border border-slate-200 p-4 hover:bg-slate-100/80 hover:border-slate-300 transition-all duration-300">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="flex-shrink-0 w-9 h-9 rounded-full bg-white flex items-center justify-center group-hover:scale-100 transition-tramsform duration-300">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-slate-500">{label}</p>
|
||||||
|
<p className="text-slate-900 text-sm font-semibold">{value}</p>
|
||||||
|
{subtitle && <p className="text-xs text-slate-400 mt-1">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeatherDetailsGrid(){
|
||||||
|
const {currentWeather} = useWeatherStore();
|
||||||
|
if(!currentWeather) return null;
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
|
||||||
|
<WeatherInfoItem icon={<Droplets className="w-4 h-4 text-blue-500"/>} label="湿度" value={`${currentWeather.current.humidity}%`} />
|
||||||
|
<WeatherInfoItem icon={<Wind className="w-4 h-4 text-blue-500"/>} label="风速" value={`${currentWeather.current.wind_speed} m/s`} />
|
||||||
|
<WeatherInfoItem icon={<Gauge className="w-4 h-4 text-blue-500"/>} label="气压" value={`${currentWeather.current.pressure} hPa`} />
|
||||||
|
<WeatherInfoItem icon={<Cloud className="w-4 h-4 text-blue-500"/>} label="云量" value={`${currentWeather.current.cloudcover}%`} />
|
||||||
|
<WeatherInfoItem icon={<ThermometerSun className="w-4 h-4 text-blue-500"/>} label="紫外线" value={`${currentWeather.current.uv_index} UV`} />
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 border-t border-slate-200">
|
||||||
|
<div className="flex items-center justify-between text-sm mb-2">
|
||||||
|
<div className="flex items-center gap-2 text-slate-600">
|
||||||
|
<Droplets className="w-4 h-4 text-blue-500"/>
|
||||||
|
<span>降水量:\
|
||||||
|
<span className="font-semibold">
|
||||||
|
{currentWeather.current.precip} mm
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className=" text-slate-600">
|
||||||
|
{currentWeather.current.is_day === "yes" ? "白天" : "夜晚"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-center text-slate-500">
|
||||||
|
观测时间: {currentWeather.current.observation_time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,12 @@ import { twMerge } from "tailwind-merge"
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatTemperature = (
|
||||||
|
temp: number,
|
||||||
|
unit: 'celsius' | 'fahrenheit' = 'celsius'
|
||||||
|
): string => {
|
||||||
|
const value = unit === 'fahrenheit' ? (temp * 9) / 5 + 32 : temp;
|
||||||
|
const symbol = unit === 'fahrenheit' ? '°F' : '°C';
|
||||||
|
return `${Math.round(value)}${symbol}`;
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import { type RouteConfig, index } from "@react-router/dev/routes";
|
import { type RouteConfig, index,route } from "@react-router/dev/routes";
|
||||||
|
|
||||||
export default [index("routes/home.tsx")] satisfies RouteConfig;
|
export default [index("routes/home.tsx"),route('weather',"routes/weather.tsx")] satisfies RouteConfig;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { Loader2 } from "lucide-react"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useWeatherStore } from "../store/weatherStore"
|
||||||
|
import { WeatherCard } from "@/components/WeatherCard"
|
||||||
|
export function meta() {
|
||||||
|
return [
|
||||||
|
{ title: "天气查询页面" },
|
||||||
|
{ name: "description", content: "查询天气信息" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
export function Weather() {
|
||||||
|
const { currentWeather, isLoading, error, setError } = useWeatherStore()
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
toast.error(error)
|
||||||
|
setError("")
|
||||||
|
}
|
||||||
|
}, [ErrorEvent, setError])
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<header className="flex flex-col items-center justify-center">
|
||||||
|
<h1 className="text-4xl font-bold">天气查询</h1>
|
||||||
|
<p className="text-sm">实时天气</p>
|
||||||
|
</header>
|
||||||
|
{isLoading && !currentWeather && (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center justify-center"></div>
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-20 w-20 border-t-2 border-b-2 border-gray-900">
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-900 text-lg font-bold">
|
||||||
|
<Loader2 className="h-10 w-10 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-900 text-lg font-bold">正在加载...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{isLoading || currentWeather ? (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<WeatherCard />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1 +1,138 @@
|
||||||
export const useWeatherStore =
|
import { create } from 'zustand'
|
||||||
|
import {persist,createJSONStorage} from 'zustand/middleware'
|
||||||
|
const CACHE_EXPIRY = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
export interface Location {
|
||||||
|
name: string;
|
||||||
|
country: string;
|
||||||
|
region: string;
|
||||||
|
lat: string;
|
||||||
|
lon: string;
|
||||||
|
timezone_id: string
|
||||||
|
localtime: string;
|
||||||
|
localtime_epoch: number;
|
||||||
|
utc_offset: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Current {
|
||||||
|
observation_time: string;
|
||||||
|
temperature: number;
|
||||||
|
weather_code: number;
|
||||||
|
weather_icons: string[];
|
||||||
|
weather_descriptions: string[];
|
||||||
|
wind_speed: number;
|
||||||
|
wind_degree: number;
|
||||||
|
wind_dir: string;
|
||||||
|
pressure: number;
|
||||||
|
precip: number;
|
||||||
|
humidity: number;
|
||||||
|
cloudcover: number;
|
||||||
|
feelslike: number;
|
||||||
|
uv_index: number;
|
||||||
|
visibility: number;
|
||||||
|
is_day: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherData {
|
||||||
|
request?: {
|
||||||
|
type: string;
|
||||||
|
query: string;
|
||||||
|
language: string;
|
||||||
|
unit: string;
|
||||||
|
};
|
||||||
|
location: Location;
|
||||||
|
current: Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherCache {
|
||||||
|
data: WeatherData;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useWeatherStore = create<WeatherStore>()(
|
||||||
|
persist(
|
||||||
|
(set,get)=>({
|
||||||
|
currentWeather:null,weatherCache:new Map(),isLoading:false,error:null,
|
||||||
|
setCurrentWeather:(weather)=>set({currentWeather:weather}),
|
||||||
|
getWeatherFromCache:(city)=>{
|
||||||
|
const cashe = get().weatherCache.get(city.toLowerCase());
|
||||||
|
if(!cashe)return null;
|
||||||
|
const isExpired = Date.now()-cashe.timestamp>CACHE_EXPIRY;
|
||||||
|
return isExpired?null:cashe.data;
|
||||||
|
},
|
||||||
|
cacheWeather:(city,weather)=>set((state)=>{
|
||||||
|
const newCache = new Map(state.weatherCache);
|
||||||
|
newCache.set(city.toLowerCase(),{data:weather,timestamp:Date.now()});
|
||||||
|
return{weatherCache:newCache}
|
||||||
|
}),
|
||||||
|
setLoading:(loading)=>set({isLoading:loading}),
|
||||||
|
setError:(error)=>set({error}),
|
||||||
|
reset:()=>set({currentWeather:null,weatherCache:new Map(),isLoading:false,error:null}),
|
||||||
|
searchWeather:async(city:string)=>{
|
||||||
|
try{
|
||||||
|
set({isLoading:true,error:null});
|
||||||
|
const cachedWeather = get().getWeatherFromCache(city);
|
||||||
|
if(cachedWeather){
|
||||||
|
set({currentWeather:cachedWeather,isLoading:false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const WeatherData = await WeatherAPI.getcurrentWeather(city);
|
||||||
|
set({currentWeather:WeatherData,isLoading:false});
|
||||||
|
get().cacheWeather(city,WeatherData);
|
||||||
|
}catch(error){
|
||||||
|
set({error:'获取失败',isLoading:false});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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(error){
|
||||||
|
set({error:'刷新失败',isLoading:false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name:'weather-strorage',
|
||||||
|
storage:createJSONStorage(()=>localStorage),
|
||||||
|
partialize:(state)=>({
|
||||||
|
weatherCache:Array.from(state.weatherCache.entries())
|
||||||
|
}),
|
||||||
|
onRehydrateStorage:(state)=>{
|
||||||
|
if(state && Array.isArray(state.weatherCache)){
|
||||||
|
state.weatherCache = new Map(state.weatherCache as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
689
pnpm-lock.yaml
689
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue