This commit is contained in:
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() {
|
export async function GET() {
|
||||||
const apiKey = process.env.CAIYUN_API_KEY;
|
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 {
|
try {
|
||||||
const response = await fetch(apiUrl, { next: { revalidate: 600 } });
|
// 同时获取实时天气和6天预报
|
||||||
const data = await response.json();
|
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 = {
|
const formattedData = {
|
||||||
temp: Math.round(data.result.realtime.temperature),
|
temp: Math.round(realtimeData.result.realtime.temperature),
|
||||||
feelsLike: Math.round(data.result.realtime.apparent_temperature),
|
feelsLike: Math.round(realtimeData.result.realtime.apparent_temperature),
|
||||||
condition: translateSkycon(data.result.realtime.skycon),
|
condition: translateSkycon(realtimeData.result.realtime.skycon),
|
||||||
sunrise: data.result.daily.astro[0].sunrise.time,
|
windSpeed: realtimeData.result.realtime.wind.speed.toFixed(1),
|
||||||
sunset: data.result.daily.astro[0].sunset.time,
|
windDirection: getWindDirection(realtimeData.result.realtime.wind.direction),
|
||||||
windSpeed: (data.result.realtime.wind.speed * 3.6).toFixed(1), // 转换为km/h
|
uvIndex: realtimeData.result.realtime.life_index.ultraviolet.desc,
|
||||||
windDirection: getWindDirection(data.result.realtime.wind.direction),
|
forecast: dailyData.result.daily.temperature.slice(0, 6).map((item: any, index: number) => ({
|
||||||
uvIndex: data.result.realtime.life_index.ultraviolet.desc,
|
|
||||||
forecast: data.result.daily.temperature.map((item: any, index: number) => ({
|
|
||||||
day: formatDailyDate(item.date),
|
day: formatDailyDate(item.date),
|
||||||
low: Math.round(item.min),
|
low: Math.round(item.min),
|
||||||
high: Math.round(item.max),
|
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) {
|
} catch (error) {
|
||||||
|
console.error('天气获取失败:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "天气数据获取失败" },
|
{ error: "天气数据获取失败" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -36,7 +56,7 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 天气状况翻译
|
// 天气状况翻译(根据最新文档更新)
|
||||||
function translateSkycon(skycon: string) {
|
function translateSkycon(skycon: string) {
|
||||||
const skyconMap: Record<string, string> = {
|
const skyconMap: Record<string, string> = {
|
||||||
"CLEAR_DAY": "晴",
|
"CLEAR_DAY": "晴",
|
||||||
@@ -48,20 +68,31 @@ function translateSkycon(skycon: string) {
|
|||||||
"MODERATE_RAIN": "中雨",
|
"MODERATE_RAIN": "中雨",
|
||||||
"HEAVY_RAIN": "大雨",
|
"HEAVY_RAIN": "大雨",
|
||||||
"STORM_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) {
|
function getWindDirection(degree: number) {
|
||||||
const directions = ["北风", "东北风", "东风", "东南风", "南风", "西南风", "西风", "西北风"];
|
const directions = ["北风", "东北风", "东风", "东南风", "南风", "西南风", "西风", "西北风"];
|
||||||
return directions[Math.round(degree % 360 / 45) % 8];
|
return directions[Math.floor((degree + 22.5) % 360 / 45)] || "未知风向";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日期格式化
|
// 日期格式化(显示星期)
|
||||||
function formatDailyDate(dateStr: string) {
|
function formatDailyDate(dateStr: string) {
|
||||||
const date = new Date(dateStr);
|
try {
|
||||||
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
const date = new Date(dateStr);
|
||||||
return weekdays[date.getDay()];
|
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||||
|
return weekdays[date.getDay()];
|
||||||
|
} catch {
|
||||||
|
return "未知日期";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,28 +11,44 @@ import CalendarGrid from "../components/CalendarGrid";
|
|||||||
import WeatherSection from "../components/WeatherSection";
|
import WeatherSection from "../components/WeatherSection";
|
||||||
import NewsSection from "../components/NewsSection";
|
import NewsSection from "../components/NewsSection";
|
||||||
import { generateCalendarDays } from "@/utils/calendar";
|
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 VoiceAssistant from "@/components/VoiceAssistant";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// 定义通用数据获取器
|
||||||
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
const MagicMirror = () => {
|
const MagicMirror = () => {
|
||||||
const [time, setTime] = useState(new Date());
|
const [time, setTime] = useState(new Date());
|
||||||
const calendarDays = useMemo(() => generateCalendarDays(), []);
|
const calendarDays = useMemo(() => generateCalendarDays(), []);
|
||||||
|
|
||||||
// 天气示例数据
|
// 天气示例数据
|
||||||
// 替换原有的weatherData定义部分
|
|
||||||
const { data: weatherData, error: weatherError } = useSWR<WeatherData>(
|
const { data: weatherData, error: weatherError } = useSWR<WeatherData>(
|
||||||
"/api/weather",
|
"/api/weather",
|
||||||
(url: string) => fetch(url).then((res) => res.json())
|
(url: string) => fetch(url).then((res) => res.json())
|
||||||
);
|
);
|
||||||
|
|
||||||
// 新闻数据
|
// 新闻数据
|
||||||
// 替换原有的newsItems定义部分:
|
|
||||||
const { data: newsItems = [], error: newsError } = useSWR<NewsItem[]>(
|
const { data: newsItems = [], error: newsError } = useSWR<NewsItem[]>(
|
||||||
"/api/news",
|
"/api/news",
|
||||||
(url: string) => fetch(url).then((res) => res.json())
|
(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(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => setTime(new Date()), 1000);
|
const timer = setInterval(() => setTime(new Date()), 1000);
|
||||||
@@ -102,7 +118,8 @@ const MagicMirror = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WeatherSection data={weatherData} />
|
{/*<WeatherSection data={weatherData} />*/}
|
||||||
|
<WeatherSection data={mergedWeatherData} />
|
||||||
<NewsSection items={newsItems} />
|
<NewsSection items={newsItems} />
|
||||||
<VoiceAssistant greeting={greeting} />
|
<VoiceAssistant greeting={greeting} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,11 +8,10 @@
|
|||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { WeatherData } from "../types/magic-mirror";
|
import { WeatherData } from "../types/magic-mirror";
|
||||||
import WbSunnyIcon from "@mui/icons-material/WbSunny";
|
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
|
||||||
interface WeatherSectionProps {
|
interface WeatherSectionProps {
|
||||||
data?: WeatherData; // 允许undefined状态(加载中)
|
data?: WeatherData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
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 {
|
try {
|
||||||
return new Date(isoTime).toLocaleTimeString("zh-CN", {
|
return new Date(isoTime).toLocaleTimeString("zh-CN", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@@ -35,12 +35,12 @@ const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
|||||||
hour12: false,
|
hour12: false,
|
||||||
});
|
});
|
||||||
} catch {
|
} 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> = {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 当前天气模块 */}
|
{/* 当前天气模块 */}
|
||||||
<div className="absolute top-8 right-0 w-64 space-y-4">
|
<div className="absolute top-8 right-0 w-64 space-y-4">
|
||||||
{/* 风速和日出日落信息 */}
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-gray-400">风速</div>
|
<div className="text-gray-400">风速</div>
|
||||||
<div className="text-gray-300">
|
<div className="text-gray-300">
|
||||||
{data.windSpeed}km/h {data.windDirection}
|
{data.windSpeed || "--"}km/h {data.windDirection || ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -73,42 +74,42 @@ const WeatherSection: FC<WeatherSectionProps> = ({ data }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 温度显示区域 */}
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-4xl">{getWeatherIcon(data.condition)}</div>
|
<div className="text-4xl">{getWeatherIcon(data.condition)}</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-3xl">{data.temp}°C</div>
|
<div className="text-3xl">{data.temp ?? "--"}°C</div>
|
||||||
<div className="text-gray-400 text-sm">体感 {data.feelsLike}°C</div>
|
<div className="text-gray-400 text-sm">
|
||||||
|
体感 {data.feelsLike ?? "--"}°C
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 紫外线指数 */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-gray-400 text-sm">紫外线指数</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* 天气预报模块 */}
|
{/* 天气预报模块(安全渲染) */}
|
||||||
<div className="absolute top-64 right-8 w-64 space-y-2">
|
{forecastData.length > 0 && (
|
||||||
{data.forecast.slice(0, 5).map((day, index) => (
|
<div className="absolute top-64 right-8 w-64 space-y-2">
|
||||||
<div
|
{forecastData.map((day, index) => (
|
||||||
key={day.day}
|
<div
|
||||||
className="flex justify-between items-center text-sm group"
|
key={day.day || index}
|
||||||
style={{ opacity: 1 - index * 0.15 }}
|
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">
|
<div className="text-gray-300 w-12">{day.day || "未知"}</div>
|
||||||
{getWeatherIcon(day.condition)}
|
<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>
|
||||||
<div className="flex gap-2">
|
))}
|
||||||
<span className="text-blue-300">{day.low}°</span>
|
</div>
|
||||||
<span className="text-red-300">{day.high}°</span>
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* 包含天气、新闻、日历等数据结构的类型定义
|
* 包含天气、新闻、日历等数据结构的类型定义
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 新增天气数据类型定义
|
||||||
export interface WeatherData {
|
export interface WeatherData {
|
||||||
temp: number;
|
temp: number;
|
||||||
feelsLike: number;
|
feelsLike: number;
|
||||||
@@ -20,6 +21,14 @@ export interface WeatherData {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增日出日落数据类型定义
|
||||||
|
export interface SunData {
|
||||||
|
date: string;
|
||||||
|
sunrise: string;
|
||||||
|
sunset: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增新闻数据类型定义
|
||||||
export interface NewsItem {
|
export interface NewsItem {
|
||||||
uniquekey: string;
|
uniquekey: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -31,6 +40,7 @@ export interface NewsItem {
|
|||||||
is_content: string;
|
is_content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增日历数据类型定义
|
||||||
export type CalendarDay = {
|
export type CalendarDay = {
|
||||||
day: number;
|
day: number;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user