0228.5
All checks were successful
部署 Next.js 站点到 Gitea / deploy (push) Successful in 3m7s

This commit is contained in:
2025-02-28 23:28:56 +08:00
parent f5dc9a4843
commit 2521db74a9
5 changed files with 184 additions and 65 deletions

60
src/app/api/sun/route.ts Normal file
View 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) || '--:--';
}
}

View File

@@ -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 "未知日期";
}
} }

View File

@@ -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>

View File

@@ -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>
</> </>
); );
}; };

View File

@@ -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;