test/1114/app/components/weather/WeatherSearchForm.tsx

106 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<HTMLInputElement>) => {
setCity(e.target.value);
if (inputError) setInputError('');
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const trimmed = city.trim();
if (!trimmed) {
setInputError('请输入城市名称');
return;
}
if (trimmed.length < 2) {
setInputError('城市名称至少需要2个字符');
return;
}
if (!/^[\u4e00-\u9fa5a-zA-Z\s-]+$/.test(trimmed)) {
setInputError('城市名称包含无效字符');
return;
}
await searchWeather(trimmed);
};
return (
<div className="p-6 border-b border-slate-200/50 dark:border-slate-700/50">
<form onSubmit={handleSubmit} className="space-y-3">
<div className="relative">
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<Search className="w-5 h-5 text-slate-400 dark:text-slate-500" />
</div>
<Input
type="text"
value={city}
onChange={handleInputChange}
placeholder="搜索城市如北京、上海、London..."
disabled={isLoading}
className={cn(
'h-12 pl-12 pr-4 text-base rounded-xl',
'bg-slate-50/50 dark:bg-slate-900/50',
'focus:bg-white dark:focus:bg-slate-900',
'transition-all duration-200',
inputError && 'border-red-300 dark:border-red-700'
)}
/>
</div>
{inputError && (
<div className="flex items-start gap-2 px-3 py-2 rounded-lg bg-red-50/80 dark:bg-red-900/20 animate-in slide-in-from-top-1 fade-in">
<AlertCircle className="w-4 h-4 text-red-500 mt-0.5" />
<p className="text-xs text-red-600 dark:text-red-400 font-medium">{inputError}</p>
</div>
)}
<div className="flex gap-2">
<Button
type="submit"
disabled={isLoading || !city.trim()}
className="flex-1 h-11 rounded-xl bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 shadow-lg shadow-blue-500/30 hover:shadow-xl hover:shadow-blue-500/40 disabled:opacity-50 transition-all duration-300"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span className="ml-2"></span>
</>
) : (
<>
<Search className="w-4 h-4" />
<span className="ml-2"></span>
</>
)}
</Button>
{currentWeather && (
<Button
type="button"
variant="outline"
onClick={refreshWeather}
disabled={isLoading}
className="h-11 px-4 rounded-xl"
title="刷新数据"
>
<RefreshCw className={cn("w-4 h-4", isLoading && "animate-spin")} />
</Button>
)}
</div>
</form>
</div>
);
}