
HY
折腾进行时
Hello, I'm HY.








刷参考站的时候看到他右栏挂着一张天气预报卡 —— 地点、大号温度、一屏指标,底下还有个「收起」按钮,看着挺舒服。
琢磨了一下,这东西对博客来说几乎是白捡的:不用服务器、不用注册 key、不用维护,访客点进来还能顺手瞄一眼自己那儿的天气。于是照着他的样子整了一个,全站都有。

一张标准的侧栏卡片:标题「天气预报」左边一根主题色竖条,下面第一行是地点图标 + 「广州市 番禺区」,右上角一枚 3rem 的大天气图标;再往下是大号温度 29° 配一个「晴」字,接着一行「高温 32°C / 低温 25°C」—— 高温走红、低温走蓝;然后一条虚线分界,两列指标网格:风力、能见度、体感温度、湿度、气压、空气质量、云量、紫外线、降水量、降水概率、日出、日落,共 12 项,空气质量那格还带个「良」的等级标签;网格下面是全宽的「污染物浓度」,摊开 PM2.5 / PM10 / O₃ / NO₂ / SO₂ / CO 六个数值;卡片底部一行小字「9 分钟前发布」,最后是「收起」按钮。
我的版本结构上没怎么动 —— 参考站已经把信息密度调得挺合适了,我主要是换成了自己的注入方式,另外把展开/收起做成了默认展开。

这类侧栏组件想插进上游主题,得改 SideBar.astro 的 componentMap(它写死了支持的组件类型),那是主题源文件,不能碰。所以还是走本站用惯了的页脚注入:
| 文件 | 状态 | 干什么 |
|---|---|---|
src/config/FooterConfig.html | 改(追加) | 全部样式 + PAGE_SCRIPTS 全站加载条目 |
public/assets/js/weather-card.js | 新增 | 拼卡片 DOM、拉接口、渲染、挂载/搬运、折叠 |
跟「最近评论」那次不太一样的是,这个组件的 DOM 我没放在页脚 —— 卡片要拼几十个节点,几乎每个都得按接口字段回填,摆在页脚那堆 HTML 里反而更难维护,索性全交给脚本拼。页脚只留样式和一个加载条目:
1{2 // 「天气预报」:全站加载 —— 桌面挂右栏、移动端挂底部栏,挂载逻辑在 js 里3 path: /.*/,4 id: "ff-weather-js",5 src: "/assets/js/weather-card.js?v=1",6},git diff 对 FooterConfig.html 是 470 行纯新增、0 删除,主题里一个文件都没改。
数据:参考站用的是 uapis.cn 的天气接口,前端直连一次拿全:
1GET https://uapis.cn/api/v1/misc/weather?extended=true&forecast=true2→ 省市 / 区县 / adcode / 天气 / weather_icon / 温度 / 风向风力 / 湿度3 / 高温低温 / 7 天预报 / 体感 / 能见度 / 气压 / 紫外线 / 降水量 / 云量4 / AQI + 等级 + 主要污染物 / PM2.5 PM10 O₃ NO₂ SO₂ CO / 发布时间免费、不用注册 key,响应头带 Access-Control-Allow-Origin,浏览器直连不需要代理。中文描述现成(「晴」「多云」「东南风」都是接口直接给的),省得自己拿代码去翻。
按访客 IP 定位是这个接口最舒服的地方:不传参数时,它按请求方 IP 反查位置。也就是说每个访客看到的是自己所在地的天气,不是我这儿的。等于白捡一个「个性化」。想固定成站长常驻地也行,?city=广州 或 ?adcode=440105,脚本顶部留了个 FIXED_CITY 常量。
挂载点按视口分两套,跟主题自己的布局规则走:≥768px 进右栏 top 组件容器的末尾(站点信息之后),<768px 主题会把左右侧栏整块隐藏、改用页面底部的 #mobile-bottom-sidebar,组件就挂进里面的 #bottom-sidebar。两类容器都在 swup 替换范围之外,搬一次就永久有效:
1function findMountPoint() {2 if (isMobileNow()) {3 var bar = document.getElementById("bottom-sidebar");4 if (bar) return { parent: bar, before: null };5 }6 var sidebar = document.getElementById("right-sidebar");7 if (!sidebar) return null;8 var wrapper = sidebar.firstElementChild; // top 组件容器9 if (!wrapper) return null;10 return { parent: wrapper, before: null };11}再挂一个 matchMedia("(max-width: 767px)") 的 change 监听,跨断点时把节点搬到另一侧。实测 swup 无刷新切 /archive/ → /about/ → 回首页三跳之后,节点还是同一个、折叠状态也保持,而且接口一次都没重新请求。
折叠没去用上游 WidgetLayout.astro 那套 <widget-layout>。那个组件的展开逻辑写在自定义元素的 constructor 里,靠 querySelector 取自己的子节点 —— 我这边是脚本动态插进去的,构造时机不好控,赌浏览器在子节点就位前先把 constructor 跑掉就白搭。自己写反而简单,一个 class 切换加两行文案:
1function applyCollapsed(next) {2 collapsed = next;3 root.classList.toggle("is-collapsed", collapsed);4 label.textContent = collapsed ? "展开" : "收起";5 sessionStorage.setItem(VIEW_KEY, collapsed ? "1" : "0");6}默认展开(参考站是收起的,只露温度,我觉得天气卡的信息量就是它的卖点,没必要先藏起来),点一下压到 9.5rem —— 正好露出地点、温度和那条高低温;状态记在 sessionStorage 里,同一次会话内切页不会弹回默认。

缓存给到 30 分钟,原因不是性能,是配额。这接口的响应头里有 Ratelimit-Policy:visitor-quota 1500 credits / 31 天、ip-daily 2000 / 天,每次请求扣 1 credit。个人博客的流量按 30 分钟的本地缓存算,一个访客半小时内翻多少页都只请求一次,撑得住;真哪天被跑爆了,就得在 CF Worker 上再加一层共享缓存。
一、图标不能照抄 <use>。 参考站那些图标是 Astro 用 astro-icon 在构建期按需渲染出来的 <symbol>,符号表只包含当页真正用到的几枚。我在脚本里动态插入节点,既不在构建期的渲染树里、页面上的符号表也没有这些 id,写 <use href="#ai:material-symbols:air"> 只会渲染成一片空白。
解法是把 path 内联进来。从 iconify 的 material-symbols 集合把用到的 34 枚图标的 path 全拉下来,塞进脚本当常量:
1var ICONS = {2 "air": '<path fill="currentColor" d="M11.5 20q-1.25 0-2.125-.875T8.5 17h2q..."/>',3 // ... 33 枚4};5
6function ico(name, cls) {7 return '<svg class="ff-w-ico ' + cls + '" viewBox="0 0 24 24">' + ICONS[name] + "</svg>";8}代价是脚本从 18.7 KB 长到 31.4 KB(图标常量独占 12.7 KB)—— 换来的是一份完全自包含、不依赖任何运行环境的图标表,值。顺便把天气代码到图标的映射也补全了:接口给的是三位数代码(100 晴 / 101 多云 / 104 阴 / 3xx 雨 / 4xx 雪 / 5xx 雾霾),先查精确码、落空再按百位兜底,以后接口新增代码也不会显示成空白。
二、auto-fit 在窄栏只排得出一列。 指标网格一开始写的是自适应的老配方:
1/* 想「宽度够就两列,不够就一列」 */2grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));8.5rem 是 136px,当时觉得 280px 的右栏怎么也能塞两列。结果算下来要 136×2 + 8(gap) = 280px,而卡片内边距还要吃掉 32px,可用宽度只有 248px —— 一格都放不下第二个,12 项全竖着排,整张卡片被顶到 925px 高。
改成写死两列就完事,每列约 124px:
1grid-template-columns: repeat(2, minmax(0, 1fr));卡片高度从 925px 落到 613px,差不多砍掉三分之一。教训是「自适应」在窄容器里经常不如硬编码 —— 右栏就这么宽,没什么好自适应的。
weather-card.js 顶部的 API_URL(字段名要跟着 render() 一起改)。FIXED_CITY 填成城市名或 adcode,留空就是按访客 IP 定位。DEFAULT_COLLAPSED 改成 true。FooterConfig.html 末尾的 .ff-w-* 段,类名统一带前缀,不会跟别的组件撞。?v=:vercel.json 给 /assets/* 配了 30 天强缓存,不加版本号老访客拿到的还是旧文件。验证走的是老一套:astro dev 起服务,headless 浏览器分别看亮 / 暗主题、1440 与 1280 两种右栏宽度、390px 移动端挂载点,再叠一次 swup 往返(三跳之后节点还是同一个、接口一次都没重新请求)。错误路径也单独试了:把接口请求掐断再重载,卡片会切到错误态并给一个「重试」按钮;重试时接口还挂着就继续停在错误态,不会白屏也不会卡在「加载中」;接口恢复后再点一下才回到内容。
暗色主题下颜色都是跟着主题变量走的,文字和图标不用单独调:

移动端挂在页面底部的组件栏里,宽度拉满,指标网格还是两列,污染物那行三列:

卡片在右栏用的是和其它侧栏组件同一套 card-base,圆角、描边和暗色适配都跟着主题走,不用单独调。
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
部分内容可能已过时
分享你的想法,与大家交流讨论