起始页(StartPage) - 极简玻璃风导航页作未登录首页:时段问候、Bing 默认可切换搜索、10 个快捷位、右下齿轮进登录 - 昼夜背景自动切换(light/dark),按天气图标码 + 本地时间判断 - 天气动态效果(WeatherFx canvas):雨/雪/雷,克制风格,尊重 reduced-motion 天气与定位(前后端) - 后端 /api/web/weather:高德 IP 定位(城市+坐标) + 和风实时天气 - 和风用 JWT(EdDSA/Ed25519)认证,密钥走环境变量、仅后端持有 - 每日限流(高德 300、和风 900,北京 0 点归零) + 10 分钟按 IP 缓存 - 前端 useWeather + WeatherIcon(7 类极简 SVG) 导航与登录 - 浏览器返回键支持(History API);返回即登出 - 记住我:勾选存 localStorage、不勾存 sessionStorage,默认不勾 其他 - .gitignore 忽略 *.pem/*.env 等密钥 - 新增文档 docs/startpage-weather-location.md 及部署 spec/plan Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.3 KiB
TypeScript
75 lines
2.3 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import type { WeatherState } from '../../hooks/useWeather'
|
||
import { WeatherIcon } from './WeatherIcon'
|
||
|
||
// 按本地小时给出 早/中/晚/夜 问候(默认英文,改中文只需替换下面的文案)。
|
||
function greetingFor(hour: number): string {
|
||
if (hour >= 5 && hour < 12) return 'Good morning'
|
||
if (hour >= 12 && hour < 18) return 'Good afternoon'
|
||
if (hour >= 18 && hour < 23) return 'Good evening'
|
||
return 'Good night'
|
||
}
|
||
|
||
// 天气行:根据后端返回的业务态分别渲染。
|
||
function WeatherLine({ wx }: { wx: WeatherState }) {
|
||
let content = null
|
||
|
||
if (!wx.loading && wx.data) {
|
||
const d = wx.data
|
||
if (d.ok) {
|
||
content = (
|
||
<>
|
||
<WeatherIcon code={d.icon} size={18} />
|
||
<span>
|
||
{d.city} · {d.text} · {d.temp}°
|
||
</span>
|
||
</>
|
||
)
|
||
} else if (d.reason === 'quota_exceeded') {
|
||
// “表个态”:当日额度用完。
|
||
content = <span className="text-white/55">天气请求次数已用完,明日 0 点恢复</span>
|
||
}
|
||
// not_configured / failed:静默(不打扰页面)
|
||
}
|
||
|
||
// 固定最小高度,避免天气加载完成后页面跳动。
|
||
return (
|
||
<div className="mt-3 flex min-h-[1.5rem] items-center justify-center gap-2 font-sans text-sm text-white/70">
|
||
{content}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function Greeting({ wx }: { wx: WeatherState }) {
|
||
const [now, setNow] = useState(() => new Date())
|
||
|
||
useEffect(() => {
|
||
// 每 30s 刷新,足够让分钟与跨时段问候保持准确。
|
||
const id = setInterval(() => setNow(new Date()), 30_000)
|
||
return () => clearInterval(id)
|
||
}, [])
|
||
|
||
const greeting = greetingFor(now.getHours())
|
||
const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||
const date = now.toLocaleDateString([], {
|
||
weekday: 'long',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
})
|
||
|
||
return (
|
||
<div className="text-center text-white [text-shadow:0_1px_16px_rgba(0,0,0,0.45)]">
|
||
<p className="font-sans text-xs uppercase tracking-[0.4em] text-white/65">
|
||
{time} · {date}
|
||
</p>
|
||
|
||
<h1 className="mt-4 font-display text-5xl font-light italic leading-tight sm:text-6xl">
|
||
{greeting}
|
||
<span className="text-white/45">.</span>
|
||
</h1>
|
||
|
||
<WeatherLine wx={wx} />
|
||
</div>
|
||
)
|
||
}
|