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

This commit is contained in:
298977887 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() {
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 "未知日期";
}
}

View File

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

View File

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

View File

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