feat: Implement initial PolyWeather dashboard with city list, detail view, map, and API endpoints.

This commit is contained in:
2569718930@qq.com
2026-03-06 09:05:40 +08:00
parent dc4167b514
commit af1d8ab4ed
10 changed files with 443 additions and 77 deletions
+10 -3
View File
@@ -1,17 +1,24 @@
import { NextResponse } from "next/server";
const API_BASE =
process.env.POLYWEATHER_API_BASE_URL || "http://127.0.0.1:8000";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET() {
if (!API_BASE) {
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
}
try {
const res = await fetch(`${API_BASE}/api/cities`, {
headers: { Accept: "application/json" },
next: { revalidate: 120 },
});
if (!res.ok) {
const raw = await res.text();
return NextResponse.json(
{ error: `Backend returned ${res.status}` },
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
{ status: 502 },
);
}
+10 -3
View File
@@ -1,12 +1,18 @@
import { NextRequest, NextResponse } from "next/server";
const API_BASE =
process.env.POLYWEATHER_API_BASE_URL || "http://127.0.0.1:8000";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET(
req: NextRequest,
context: { params: Promise<{ name: string }> },
) {
if (!API_BASE) {
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
}
const { name } = await context.params;
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}`;
@@ -17,8 +23,9 @@ export async function GET(
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
return NextResponse.json(
{ error: `Backend returned ${res.status}` },
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
{ status: 502 },
);
}
+80 -8
View File
@@ -3,17 +3,17 @@
@tailwind utilities;
:root {
--background: 222 47% 6%;
--background: 223 53% 4%;
--foreground: 210 40% 98%;
--card: 222 47% 9%;
--card: 223 46% 8%;
--card-foreground: 210 40% 98%;
--primary: 190 95% 52%;
--primary: 190 95% 56%;
--primary-foreground: 222 47% 8%;
--secondary: 223 36% 16%;
--secondary: 224 30% 14%;
--secondary-foreground: 210 40% 98%;
--accent: 217 33% 18%;
--accent: 217 30% 18%;
--accent-foreground: 210 40% 98%;
--border: 220 33% 20%;
--border: 221 38% 22%;
}
* {
@@ -21,9 +21,11 @@
}
body {
font-family: "Sora", "Avenir Next", "Segoe UI", sans-serif;
background:
radial-gradient(circle at 15% 5%, rgba(6, 78, 99, 0.2), transparent 45%),
radial-gradient(circle at 85% 85%, rgba(30, 41, 59, 0.4), transparent 42%),
radial-gradient(circle at 10% -10%, rgba(34, 211, 238, 0.2), transparent 40%),
radial-gradient(circle at 90% 0%, rgba(59, 130, 246, 0.14), transparent 36%),
radial-gradient(circle at 80% 100%, rgba(8, 47, 73, 0.5), transparent 48%),
hsl(var(--background));
color: hsl(var(--foreground));
}
@@ -33,3 +35,73 @@ body {
height: 100%;
font-family: inherit;
}
.leaflet-control-zoom a {
background: rgba(2, 6, 23, 0.82) !important;
border-color: rgba(71, 85, 105, 0.5) !important;
color: #e2e8f0 !important;
}
.leaflet-control-zoom a:hover {
background: rgba(15, 23, 42, 0.95) !important;
}
.map-pill {
min-width: 52px;
border-radius: 9999px;
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 5px 10px;
color: #f8fafc;
font-size: 11px;
font-weight: 700;
line-height: 1;
letter-spacing: 0.04em;
text-transform: uppercase;
backdrop-filter: blur(8px);
box-shadow:
0 2px 14px rgba(2, 6, 23, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.map-pill.high {
background: linear-gradient(135deg, #ef4444, #b91c1c);
}
.map-pill.medium {
background: linear-gradient(135deg, #f59e0b, #b45309);
}
.map-pill.low {
background: linear-gradient(135deg, #10b981, #047857);
}
.map-pill.active {
transform: translateY(-2px) scale(1.05);
box-shadow:
0 8px 24px rgba(34, 211, 238, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.glass {
background: linear-gradient(
180deg,
rgba(15, 23, 42, 0.9) 0%,
rgba(2, 6, 23, 0.75) 100%
);
backdrop-filter: blur(10px);
}
.fade-up {
animation: fadeUp 450ms ease-out;
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}