PersonalWebApplication/docs/startpage-weather-location.md
cc a185e56c22 feat: 导航起始页 + 天气/定位 + 天气动态效果
起始页(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>
2026-06-18 20:13:18 +08:00

134 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 起始页:天气 + 定位(已实现)
导航起始页问候语下方展示「天气图标 · 城市 · 天气 · 气温」。
**城市来自高德 IP 定位,天气来自和风实时天气。** 密钥只在后端,前端不可见。
## 架构
```
浏览器 → GET /api/web/weather (后端)
├─ 高德 /v3/ip?ip=<访问者IP> → 城市名 + 城市中心坐标
└─ 和风 /v7/weather/now?location= → 天气文字 / 气温 / 图标码
```
- 后端:`Server/src/weather.rs`(服务 + 限流 + 缓存)、路由 `Server/src/routes/web.rs``/api/web/weather`
- 前端:
- `Client/src/api/weather.ts`、`Client/src/hooks/useWeather.ts`拉取30 分钟刷新)
- `Client/src/components/StartPage.tsx`:统一持有天气状态,派生**昼夜背景**与**天气动效**,并把天气传给 `Greeting`
- `Client/src/components/start/Greeting.tsx`:展示「天气图标 · 城市 · 天气 · 气温」
- `Client/src/components/start/WeatherIcon.tsx`:天气文字旁的静态图标
- `Client/src/components/start/WeatherFx.tsx`:雨/雪 canvas 动态层
## 限流与缓存(后端内置)
- **高德 300 次/天、和风 900 次/天**,各自独立计数,**北京时间 0 点归零**。
- 任一服务商额度用尽 → 接口返回 `{ ok:false, reason:"quota_exceeded", provider }`
前端显示「天气请求次数已用完,明日 0 点恢复」。
- **按访问者 IP 缓存 10 分钟**:前端每 30 分钟刷新一次,正常用量远低于额度。
- 计数只在“真的调用了上游”时增加(命中缓存不计数)。
## 环境变量
后端启动时读取(缺高德 Key 或和风 JWT 任一项时接口返回 `not_configured`,前端静默不展示):
| 变量 | 必填 | 说明 |
|------|------|------|
| `AMAP_KEY` | 是 | 高德 **Web服务(REST)** Key |
| `QWEATHER_HOST` | 是 | 和风**专属 API Host**(控制台给的,如 `https://xxxx.qweatherapi.com` |
| `QWEATHER_PROJECT_ID` | 是 | 和风**项目 ID** → JWT 的 `sub` |
| `QWEATHER_KEY_ID` | 是 | 和风**凭据 ID** → JWT 头的 `kid` |
| `QWEATHER_PRIVATE_KEY_PATH` | 二选一 | Ed25519 私钥(PKCS#8 PEM)**文件路径**(推荐,多行 PEM 用文件最省事) |
| `QWEATHER_PRIVATE_KEY` | 二选一 | 直接给私钥 PEM 内容(可用 `\n` 转义换行) |
| `AMAP_DAILY_LIMIT` | 否 | 默认 `300` |
| `QWEATHER_DAILY_LIMIT` | 否 | 默认 `900` |
| `WEATHER_CACHE_SECS` | 否 | 缓存秒数,默认 `600` |
| `WEATHER_DEFAULT_LOCATION` | 否 | 内网/本地 IP 定位失败时的兜底坐标 `经度,纬度`(如 `116.40,39.90` |
| `WEATHER_DEFAULT_CITY` | 否 | 兜底坐标对应的城市名 |
> 和风认证用 **JWT(EdDSA/Ed25519)**:后端用私钥签发 15 分钟有效的 token
> 以 `Authorization: Bearer` 调用。私钥**只放后端**,绝不进前端/仓库。
> 本地开发时访问者 IP 是 `127.0.0.1`,高德无法定位 → 建议设 `WEATHER_DEFAULT_LOCATION` +
> `WEATHER_DEFAULT_CITY` 才能在本地看到天气;部署后用访问者真实公网 IP自动定位。
## 本地运行
后端(在 `Server/`WSL 或 Windows 均可):
```bash
AMAP_KEY=xxx \
QWEATHER_HOST=https://xxxx.qweatherapi.com \
QWEATHER_PROJECT_ID=项目ID QWEATHER_KEY_ID=凭据ID \
QWEATHER_PRIVATE_KEY_PATH=./qweather-ed25519.pem \
WEATHER_DEFAULT_LOCATION=116.40,39.90 WEATHER_DEFAULT_CITY=北京 \
PORT=8080 cargo run
```
前端 `cd Client && npm run dev`,打开起始页即可看到天气行。
## 部署:改了后端时(重新编译)
改了 `Server/` 下代码时上线需要**重新编译 Rust 二进制**
1. WSL 里 `cd /mnt/f/Code/InternetProject/Server && cargo build --release`
2. scp 覆盖服务器 `/opt/internet-project/RustServer` + 前端 `dist/*`
3. 私钥与密钥建议写进服务器上的一个文件再统一加载,避免出现在命令历史里。
例如服务器放 `/opt/internet-project/qweather.pem``/opt/internet-project/weather.env`
```bash
# weather.env 内容(不进仓库)
AMAP_KEY=xxx
QWEATHER_HOST=https://xxxx.qweatherapi.com
QWEATHER_PROJECT_ID=项目ID
QWEATHER_KEY_ID=凭据ID
QWEATHER_PRIVATE_KEY_PATH=/opt/internet-project/qweather.pem
PORT=8548
```
启动:
```bash
ssh root@8.130.143.54 "pkill RustServer; cd /opt/internet-project && \
set -a && . ./weather.env && set +a && nohup ./RustServer > server.log 2>&1 &"
```
## 天气图标方案
未引入图标库/字体,而是把和风 `now.icon` 码归成 7 类
(晴日/晴夜/多云阴/雨/雷/雪/雾霾),各画一个极简线性 SVG`WeatherIcon.tsx`
`stroke=currentColor` 跟随文字颜色,矢量可缩放、离线可用,和玻璃风统一。
要更精细可换成和风官方图标集,但当前方案零依赖、足够清爽。
## 昼夜背景切换
背景图按昼夜在 `light.*` / `dark.*` 之间自动切换(图片放
`Client/src/assets/startpage/`,详见该目录 README
- **昼夜判断**`StartPage.tsx`):优先用 `now.icon`——`150/151/152/153/350/351/456/457`
判为夜、`100/101/102/103/300/301/400/401` 判为昼;其余无昼夜之分的天气码用
**本地时间兜底**18:0006:00 为夜)。每分钟一拍,跨时段会自动翻。
- **回退**:选中的图缺失 → 另一张 → 目录里排序第一张 → 中性渐变。
## 天气动态效果
`WeatherFx.tsx` 用一个轻量 canvas 粒子层按天气渲染(**克制**风格、低透明度、
尊重系统「减少动态效果」、`kind='none'` 不挂载):
| 天气(`now.icon` | 效果 |
|------|------|
| 雨类 `300399` | 斜向落雨;其中雷阵雨 `302/303/304` 叠**偶发闪电**一闪 |
| 雪类 `400499` 及冷 `901` | 缓慢飘落雪花 |
| 其余 | 无(静态) |
层级:背景图(-z-20) → 渐变 scrim + 半透明黑遮罩(-z-10) → **天气动效(z-0)** → 内容(z-10) → 齿轮(z-30)。
## 本地预览开关(仅 `npm run dev`
不开后端 / 不等真实天气也能预览昼夜背景与动效,加 URL 参数即可:
- `?bg=light` / `?bg=dark` —— 强制白天/夜晚背景
- `?fx=rain` / `?fx=snow` / `?fx=thunder` / `?fx=none` —— 强制动效
例:`http://localhost:5173/?bg=dark&fx=snow`。这些参数 `import.meta.env.DEV` 为真才生效,**线上无效**,线上完全由真实天气驱动。
## 再次部署的两种情况
- **只改前端**(动效、背景、样式等):前端单独重发即可——`npm run build` →
清空服务器 `static/` → 上传 `dist/*`。后端进程不用动(实时读磁盘静态文件)。
- **改了后端**`Server/` 下任何代码):需在 WSL `cargo build --release`
覆盖 `/opt/internet-project/RustServer` → 带环境变量重启(见上「部署:改了后端时」)。