This commit is contained in:
parent
f5dc9a4843
commit
2521db74a9
60
src/app/api/sun/route.ts
Normal file
60
src/app/api/sun/route.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
const apiKey = process.env.QWEATHER_API_KEY;
|
||||
// 使用北京坐标(示例)
|
||||
const [longitude, latitude] = [116.3974, 39.9093];
|
||||
const today = new Date();
|
||||
|
||||
try {
|
||||
// 获取未来6天日出日落数据
|
||||
const datePromises = Array.from({ length: 6 }).map((_, i) => {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() + i);
|
||||
return date.toISOString().split('T')[0].replace(/-/g, ''); // 格式化为yyyyMMdd
|
||||
});
|
||||
|
||||
const responses = await Promise.all(
|
||||
datePromises.map(date =>
|
||||
fetch(`https://devapi.qweather.com/v7/astronomy/sun?location=${longitude},${latitude}&date=${date}&key=${apiKey}`)
|
||||
)
|
||||
);
|
||||
|
||||
const sunData = await Promise.all(responses.map(res => res.json()));
|
||||
|
||||
// 格式化数据
|
||||
const formattedData = sunData.map((item, index) => ({
|
||||
date: new Date(today.getTime() + index * 86400000).toISOString().split('T')[0],
|
||||
sunrise: formatSunTime(item.sunrise),
|
||||
sunset: formatSunTime(item.sunset)
|
||||
}));
|
||||
|
||||
return NextResponse.json(formattedData, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, s-maxage=3600',
|
||||
'CDN-Cache-Control': 'public, s-maxage=7200'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('日出日落数据获取失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: "日出日落数据获取失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 时间格式化函数
|
||||
function formatSunTime(isoTime: string) {
|
||||
try {
|
||||
const date = new Date(isoTime);
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
} catch {
|
||||
return isoTime.split('T')[1]?.substring(0, 5) || '--:--';
|
||||
}
|
||||
}
|
@ -3,32 +3,52 @@ import { NextResponse } from 'next/server';
|
||||
export async function GET() {
|
||||
const apiKey = process.env.CAIYUN_API_KEY;
|
||||
// 请替换为你的实际坐标(示例使用北京坐标)
|
||||
const apiUrl = `https://api.caiyunapp.com/v2.6/${apiKey}/116.3974,39.9093/weather?alert=true&dailysteps=5`;
|
||||
|
||||
const [longitude, latitude] = [116.3974, 39.9093];
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, { next: { revalidate: 600 } });
|
||||
const data = await response.json();
|
||||
|
||||
// 同时获取实时天气和6天预报
|
||||
const [realtimeRes, dailyRes] = await Promise.all([
|
||||
fetch(`https://api.caiyunapp.com/v2.6/${apiKey}/${longitude},${latitude}/realtime`),
|
||||
fetch(`https://api.caiyunapp.com/v2.6/${apiKey}/${longitude},${latitude}/daily?dailysteps=6`)
|
||||
]);
|
||||
|
||||
// 检查响应状态
|
||||
if (!realtimeRes.ok || !dailyRes.ok) {
|
||||
throw new Error('API请求失败');
|
||||
}
|
||||
|
||||
const realtimeData = await realtimeRes.json();
|
||||
const dailyData = await dailyRes.json();
|
||||
|
||||
// 检查API返回状态
|
||||
if (realtimeData.status !== 'ok' || dailyData.status !== 'ok') {
|
||||
throw new Error('天气数据异常');
|
||||
}
|
||||
|
||||
// 数据格式转换
|
||||
const formattedData = {
|
||||
temp: Math.round(data.result.realtime.temperature),
|
||||
feelsLike: Math.round(data.result.realtime.apparent_temperature),
|
||||
condition: translateSkycon(data.result.realtime.skycon),
|
||||
sunrise: data.result.daily.astro[0].sunrise.time,
|
||||
sunset: data.result.daily.astro[0].sunset.time,
|
||||
windSpeed: (data.result.realtime.wind.speed * 3.6).toFixed(1), // 转换为km/h
|
||||
windDirection: getWindDirection(data.result.realtime.wind.direction),
|
||||
uvIndex: data.result.realtime.life_index.ultraviolet.desc,
|
||||
forecast: data.result.daily.temperature.map((item: any, index: number) => ({
|
||||
temp: Math.round(realtimeData.result.realtime.temperature),
|
||||
feelsLike: Math.round(realtimeData.result.realtime.apparent_temperature),
|
||||
condition: translateSkycon(realtimeData.result.realtime.skycon),
|
||||
windSpeed: realtimeData.result.realtime.wind.speed.toFixed(1),
|
||||
windDirection: getWindDirection(realtimeData.result.realtime.wind.direction),
|
||||
uvIndex: realtimeData.result.realtime.life_index.ultraviolet.desc,
|
||||
forecast: dailyData.result.daily.temperature.slice(0, 6).map((item: any, index: number) => ({
|
||||
day: formatDailyDate(item.date),
|
||||
low: Math.round(item.min),
|
||||
high: Math.round(item.max),
|
||||
condition: translateSkycon(data.result.daily.skycon[index].value)
|
||||
condition: translateSkycon(dailyData.result.daily.skycon[index]?.value)
|
||||
}))
|
||||
};
|
||||
|
||||
return NextResponse.json(formattedData);
|
||||
return NextResponse.json(formattedData, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, s-maxage=600', // 缓存10分钟
|
||||
'CDN-Cache-Control': 'public, s-maxage=1800' // CDN缓存30分钟
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('天气获取失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: "天气数据获取失败" },
|
||||
{ status: 500 }
|
||||
@ -36,7 +56,7 @@ export async function GET() {
|
||||
}
|
||||
}
|
||||
|
||||
// 天气状况翻译
|
||||
// 天气状况翻译(根据最新文档更新)
|
||||
function translateSkycon(skycon: string) {
|
||||
const skyconMap: Record<string, string> = {
|
||||
"CLEAR_DAY": "晴",
|
||||
@ -48,20 +68,31 @@ function translateSkycon(skycon: string) {
|
||||
"MODERATE_RAIN": "中雨",
|
||||
"HEAVY_RAIN": "大雨",
|
||||
"STORM_RAIN": "暴雨",
|
||||
// 其他天气代码可继续补充...
|
||||
"FOG": "雾",
|
||||
"LIGHT_SNOW": "小雪",
|
||||
"MODERATE_SNOW": "中雪",
|
||||
"HEAVY_SNOW": "大雪",
|
||||
"STORM_SNOW": "暴雪",
|
||||
"DUST": "浮尘",
|
||||
"SAND": "沙尘",
|
||||
"WIND": "大风"
|
||||
};
|
||||
return skyconMap[skycon] || skycon;
|
||||
return skyconMap[skycon] || "未知天气";
|
||||
}
|
||||
|
||||
// 风向转换
|
||||
// 风向转换(根据官方文档说明)
|
||||
function getWindDirection(degree: number) {
|
||||
const directions = ["北风", "东北风", "东风", "东南风", "南风", "西南风", "西风", "西北风"];
|
||||
return directions[Math.round(degree % 360 / 45) % 8];
|
||||
return directions[Math.floor((degree + 22.5) % 360 / 45)] || "未知风向";
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
// 日期格式化(显示星期)
|
||||
function formatDailyDate(dateStr: string) {
|
||||
const date = new Date(dateStr);
|
||||
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||
return weekdays[date.getDay()];
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||
return weekdays[date.getDay()];
|
||||
} catch {
|
||||
return "未知日期";
|
||||
}
|
||||
}
|
@ -11,28 +11,44 @@ import CalendarGrid from "../components/CalendarGrid";
|
||||
import WeatherSection from "../components/WeatherSection";
|
||||
import NewsSection from "../components/NewsSection";
|
||||
import { generateCalendarDays } from "@/utils/calendar";
|
||||
import { WeatherData, NewsItem } from "@/types/magic-mirror";
|
||||
import { WeatherData, NewsItem, SunData } from "@/types/magic-mirror";
|
||||
import VoiceAssistant from "@/components/VoiceAssistant";
|
||||
import useSWR from "swr";
|
||||
// 定义通用数据获取器
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
const MagicMirror = () => {
|
||||
const [time, setTime] = useState(new Date());
|
||||
const calendarDays = useMemo(() => generateCalendarDays(), []);
|
||||
|
||||
// 天气示例数据
|
||||
// 替换原有的weatherData定义部分
|
||||
const { data: weatherData, error: weatherError } = useSWR<WeatherData>(
|
||||
"/api/weather",
|
||||
(url: string) => fetch(url).then((res) => res.json())
|
||||
);
|
||||
|
||||
// 新闻数据
|
||||
// 替换原有的newsItems定义部分:
|
||||
const { data: newsItems = [], error: newsError } = useSWR<NewsItem[]>(
|
||||
"/api/news",
|
||||
(url: string) => fetch(url).then((res) => res.json())
|
||||
);
|
||||
|
||||
// 在组件顶部新增SWR请求
|
||||
// 日出日落数据
|
||||
const { data: sunData = [] } = useSWR<SunData[]>("/api/sun", fetcher);
|
||||
|
||||
// 合并天气数据和日出日落数据
|
||||
const mergedWeatherData = useMemo(() => {
|
||||
if (weatherData && sunData.length > 0) {
|
||||
return {
|
||||
...weatherData,
|
||||
sunrise: sunData[0]?.sunrise || "06:00",
|
||||
sunset: sunData[0]?.sunset || "18:00",
|
||||
};
|
||||
}
|
||||
return weatherData;
|
||||
}, [weatherData, sunData]);
|
||||
|
||||
// 时间更新
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setTime(new Date()), 1000);
|
||||
@ -102,7 +118,8 @@ const MagicMirror = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WeatherSection data={weatherData} />
|
||||
{/*<WeatherSection data={weatherData} />*/}
|
||||
<WeatherSection data={mergedWeatherData} />
|
||||
<NewsSection items={newsItems} />
|
||||
<VoiceAssistant greeting={greeting} />
|
||||
</div>
|
||||
|
@ -8,11 +8,10 @@
|
||||
|
||||
import { FC } from "react";
|
||||
import { WeatherData } from "../types/magic-mirror";
|
||||
import WbSunnyIcon from "@mui/icons-material/WbSunny";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
interface WeatherSectionProps {
|
||||
data?: WeatherData; // 允许undefined状态(加载中)
|
||||
data?: WeatherData;
|
||||
}
|
||||
|
||||
const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
||||
@ -26,8 +25,9 @@ const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// 日出日落时间格式化(去除日期部分)
|
||||
const formatTime = (isoTime: string) => {
|
||||
// 日出日落时间格式化(安全访问)
|
||||
const formatTime = (isoTime?: string) => {
|
||||
if (!isoTime) return "--:--";
|
||||
try {
|
||||
return new Date(isoTime).toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
@ -35,12 +35,12 @@ const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
||||
hour12: false,
|
||||
});
|
||||
} catch {
|
||||
return isoTime.split("T")[1]?.slice(0, 5) || isoTime; // 回退处理
|
||||
return isoTime.split("T")[1]?.slice(0, 5) || "--:--";
|
||||
}
|
||||
};
|
||||
|
||||
// 获取天气图标
|
||||
const getWeatherIcon = (condition: string) => {
|
||||
// 获取天气图标(带默认值)
|
||||
const getWeatherIcon = (condition?: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
晴: "☀️",
|
||||
多云: "⛅",
|
||||
@ -49,21 +49,22 @@ const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
||||
中雨: "🌧️",
|
||||
大雨: "⛈️",
|
||||
暴雨: "🌧️💦",
|
||||
// 其他天气类型可继续扩展...
|
||||
};
|
||||
return iconMap[condition] || "🌤️";
|
||||
return condition ? iconMap[condition] || "🌤️" : "🌤️";
|
||||
};
|
||||
|
||||
// 安全获取天气预报数据
|
||||
const forecastData = data.forecast?.slice(0, 5) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 当前天气模块 */}
|
||||
<div className="absolute top-8 right-0 w-64 space-y-4">
|
||||
{/* 风速和日出日落信息 */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-400">风速</div>
|
||||
<div className="text-gray-300">
|
||||
{data.windSpeed}km/h {data.windDirection}
|
||||
{data.windSpeed || "--"}km/h {data.windDirection || ""}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@ -73,42 +74,42 @@ const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 温度显示区域 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-4xl">{getWeatherIcon(data.condition)}</div>
|
||||
<div>
|
||||
<div className="text-3xl">{data.temp}°C</div>
|
||||
<div className="text-gray-400 text-sm">体感 {data.feelsLike}°C</div>
|
||||
<div className="text-3xl">{data.temp ?? "--"}°C</div>
|
||||
<div className="text-gray-400 text-sm">
|
||||
体感 {data.feelsLike ?? "--"}°C
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 紫外线指数 */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-gray-400 text-sm">紫外线指数</div>
|
||||
<div className="text-gray-300">{data.uvIndex}</div>
|
||||
<div className="text-gray-300">{data.uvIndex || "--"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 天气预报模块 */}
|
||||
<div className="absolute top-64 right-8 w-64 space-y-2">
|
||||
{data.forecast.slice(0, 5).map((day, index) => (
|
||||
<div
|
||||
key={day.day}
|
||||
className="flex justify-between items-center text-sm group"
|
||||
style={{ opacity: 1 - index * 0.15 }}
|
||||
>
|
||||
<div className="text-gray-300 w-12">{day.day}</div>
|
||||
<div className="text-gray-400 transition-opacity opacity-70 group-hover:opacity-100">
|
||||
{getWeatherIcon(day.condition)}
|
||||
{/* 天气预报模块(安全渲染) */}
|
||||
{forecastData.length > 0 && (
|
||||
<div className="absolute top-64 right-8 w-64 space-y-2">
|
||||
{forecastData.map((day, index) => (
|
||||
<div
|
||||
key={day.day || index}
|
||||
className="flex justify-between items-center text-sm group"
|
||||
style={{ opacity: 1 - index * 0.15 }}
|
||||
>
|
||||
<div className="text-gray-300 w-12">{day.day || "未知"}</div>
|
||||
<div className="text-gray-400 transition-opacity opacity-70 group-hover:opacity-100">
|
||||
{getWeatherIcon(day.condition)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="text-blue-300">{day.low ?? "--"}°</span>
|
||||
<span className="text-red-300">{day.high ?? "--"}°</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="text-blue-300">{day.low}°</span>
|
||||
<span className="text-red-300">{day.high}°</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -3,6 +3,7 @@
|
||||
* 包含天气、新闻、日历等数据结构的类型定义
|
||||
*/
|
||||
|
||||
// 新增天气数据类型定义
|
||||
export interface WeatherData {
|
||||
temp: number;
|
||||
feelsLike: number;
|
||||
@ -20,6 +21,14 @@ export interface WeatherData {
|
||||
}[];
|
||||
}
|
||||
|
||||
// 新增日出日落数据类型定义
|
||||
export interface SunData {
|
||||
date: string;
|
||||
sunrise: string;
|
||||
sunset: string;
|
||||
}
|
||||
|
||||
// 新增新闻数据类型定义
|
||||
export interface NewsItem {
|
||||
uniquekey: string;
|
||||
title: string;
|
||||
@ -31,6 +40,7 @@ export interface NewsItem {
|
||||
is_content: string;
|
||||
}
|
||||
|
||||
// 新增日历数据类型定义
|
||||
export type CalendarDay = {
|
||||
day: number;
|
||||
isCurrent: boolean;
|
||||
|
Loading…
x
Reference in New Issue
Block a user