性能与用户体验全面优化

    前端性能:
    - 移除 Three.js 依赖(~600KB),天气粒子改为纯 CSS 动画 + Canvas 2D
    - Google Fonts 切换为 next/font 自托管,消除跨域字体请求
    - 合并 ScanTerminalLightTheme.module.css (37KB) 到主 CSS,亮/暗主题统一用 CSS 变量
    - 新增 /api/dashboard/init 聚合端点,首次加载 4 次往返 → 1 次
    - 添加 Service Worker 静态资源缓存,修复 PWA manifest 配置

    用户体验:
    - 新增全局错误边界 error.tsx / global-error.tsx,崩溃不再白屏
    - 决策卡和城市详情的更新时间改为相对时间("15秒前"),每秒自动刷新
    - 数据陈旧时状态标签从青色切换为琥珀色提示

    DEB 算法增强:
    - 市场扫描路径接入 Open-Meteo 多模型数据(ECMWF/GFS/ICON/JMA/HRDPS 等)
    - MAE 计算加入时间衰减(decay_factor=0.85),近期模型误差权重更高

    Scope-risk: MEDIUM — 全量 170 测试通过,前端 TypeScript/build 通过,ruff 零告警
    Tested: python -m pytest -q (170 passed), npx tsc --noEmit (0 errors), npm run build (success), ruff check .
@
This commit is contained in:
2569718930@qq.com
2026-05-14 21:31:05 +08:00
parent 37494a7192
commit 6c08a68413
21 changed files with 2062 additions and 1705 deletions
+101
View File
@@ -0,0 +1,101 @@
"use client";
import { RefreshCw } from "lucide-react";
import { useEffect } from "react";
export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Unhandled page error:", error);
}, [error]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
padding: "2rem",
gap: "1rem",
backgroundColor: "var(--color-bg-base, #0B1220)",
color: "var(--color-text-primary, #E6EDF3)",
fontFamily: "var(--font-data, Inter, sans-serif)",
textAlign: "center",
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: "var(--radius-xl, 20px)",
backgroundColor: "var(--color-bg-raised, #111A2E)",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: "0.5rem",
}}
>
<span style={{ fontSize: "1.8rem" }}></span>
</div>
<h1
style={{
fontSize: "1.25rem",
fontWeight: 600,
margin: 0,
}}
>
</h1>
<p
style={{
color: "var(--color-text-secondary, #9FB2C7)",
fontSize: "0.875rem",
margin: 0,
maxWidth: 400,
lineHeight: 1.6,
}}
>
</p>
{error.digest ? (
<code
style={{
fontSize: "0.75rem",
color: "var(--color-text-muted, #7D8FA3)",
fontFamily: "var(--font-mono, monospace)",
}}
>
{error.digest}
</code>
) : null}
<button
type="button"
onClick={reset}
style={{
marginTop: "0.5rem",
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.5rem 1.25rem",
borderRadius: "var(--radius-md, 10px)",
border: "1px solid var(--color-border-default, rgba(159,178,199,0.16))",
backgroundColor: "var(--color-bg-raised, #111A2E)",
color: "var(--color-accent-primary, #4DA3FF)",
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
<RefreshCw size={14} />
</button>
</div>
);
}
+115
View File
@@ -0,0 +1,115 @@
"use client";
import { RefreshCw } from "lucide-react";
import { useEffect } from "react";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Unhandled root error:", error);
}, [error]);
return (
<html lang="zh-CN" className="dark">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PolyWeather </title>
</head>
<body
style={{
margin: 0,
padding: 0,
backgroundColor: "var(--color-bg-base, #0B1220)",
minHeight: "100vh",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
padding: "2rem",
gap: "1rem",
color: "var(--color-text-primary, #E6EDF3)",
fontFamily: "var(--font-data, Inter, sans-serif)",
textAlign: "center",
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: "var(--radius-xl, 20px)",
backgroundColor: "var(--color-bg-raised, #111A2E)",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: "0.5rem",
}}
>
<span style={{ fontSize: "1.8rem" }}></span>
</div>
<h1
style={{
fontSize: "1.25rem",
fontWeight: 600,
margin: 0,
}}
>
</h1>
<p
style={{
color: "var(--color-text-secondary, #9FB2C7)",
fontSize: "0.875rem",
margin: 0,
maxWidth: 400,
lineHeight: 1.6,
}}
>
PolyWeather
</p>
{error.digest ? (
<code
style={{
fontSize: "0.75rem",
color: "var(--color-text-muted, #7D8FA3)",
fontFamily: "var(--font-mono, monospace)",
}}
>
{error.digest}
</code>
) : null}
<button
type="button"
onClick={reset}
style={{
marginTop: "0.5rem",
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.5rem 1.25rem",
borderRadius: "var(--radius-md, 10px)",
border: "1px solid var(--color-border-default, rgba(159,178,199,0.16))",
backgroundColor: "var(--color-bg-raised, #111A2E)",
color: "var(--color-accent-primary, #4DA3FF)",
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
<RefreshCw size={14} />
</button>
</div>
</body>
</html>
);
}
+3 -4
View File
@@ -50,10 +50,9 @@
--shadow-glow-secondary: 0 0 20px rgba(111, 183, 255, 0.22);
/* ── Typography ── */
--font-data:
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
--font-data: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-display: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: var(--font-jetbrains-mono), "Fira Code", "SF Mono", monospace;
/* ── Spacing (4px grid) ── */
--space-1: 4px;
+21 -11
View File
@@ -1,6 +1,22 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import { RegisterSW } from "@/components/dashboard/RegisterSW";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
weight: ["300", "400", "500", "600", "700", "800"],
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-jetbrains-mono",
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "PolyWeather | Weather Intelligence",
description:
@@ -23,25 +39,19 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN" className="dark">
<html
lang="zh-CN"
className={`${inter.variable} ${jetbrainsMono.variable} dark`}
>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body className="min-h-screen font-sans antialiased">
<a href="#main-content" className="skip-to-content">
Skip to content
</a>
<main id="main-content">{children}</main>
<RegisterSW />
</body>
</html>
);