Daisy's Garden
⭐ 精选你正在看的这个站。Nuxt 3 + Tailwind + @nuxt/content + VueUse + QWeather。博客、项目集、3D 子路径、实时天气特效、三档暗色模式、Hello Kitty 指针。
为什么自己写
市面上成熟的博客框架(VitePress、Astro、Hugo)都能快速上线,但它们的视觉风格都偏技术/简洁。我想要的是水彩 + anime + 粉紫色调 —— 这种审美需要从底层自己搭。
结构
blog/
├── app.vue · NuxtLayout 外壳
├── error.vue · 统一 404/500 水彩错误页
├── layouts/
│ └── default.vue · Header(Logo + Nav + 天气 + 主题切换)+ Footer + 自定义指针
├── pages/ · /, /posts, /posts/[slug], /projects, /about
├── components/
│ ├── PostCard.vue · 文章卡片
│ ├── FloatingPetals.vue · Hero 飘落花瓣 + 水彩斑点
│ ├── RevealOnScroll.vue · 滚动进入视口淡入动画
│ ├── BackToTop.vue · 回到顶部按钮
│ ├── CustomCursor.vue · Hello Kitty 风自定义指针
│ ├── ColorModeToggle.vue· 三档主题切换(auto / light / dark)
│ ├── WeatherWidget.vue · 头部实时天气 emoji + 温度 + hover 详情卡
│ ├── LocationPicker.vue · 城市搜索 + 浏览器定位 + 重置
│ ├── WeatherEffects.vue · 全屏粒子特效层(按天气切换)
│ └── weather/ · 7 个子特效
│ ├── Rain.vue · 垂直落雨(阵雨 40 条 / 雷雨 70 条)
│ ├── Snow.vue · 飘落 ❄️ + 横向漂移 + 自旋
│ ├── Clouds.vue · 手绘 SVG 云朵匀速漂移
│ ├── SunGlow.vue · 晴天对角金色光晕 pulse
│ ├── Stars.vue · 晴朗夜空 60 颗闪烁星
│ ├── Fog.vue · 双层雾气 breathe
│ └── Lightning.vue · 雷雨偶发白光双闪
├── composables/
│ └── useWeather.ts · 全局天气状态 + 30min 自动刷新 + localStorage 记位置
├── server/api/
│ ├── weather.get.ts · QWeather 代理 + 30min 内存缓存 + mock 降级
│ └── geo.get.ts · QWeather GeoAPI 城市搜索代理
├── types/
│ └── weather.ts · Condition 枚举(9 类)+ WeatherData 接口
└── content/
├── posts/ · *.md 文章
└── projects/ · *.md 项目(你正在读的就是一篇)
设计决策
调色 & 字体
- ANIME_PALETTE:13 个颜色的调色板,和
/play/3D 场景完全同步,跳转时视觉不断裂 - 霞鹜文楷 Lite:中文字体,通过本地 npm 依赖加载(非 CDN),国内 ECS 部署首屏字体不走外网
- Inter + Caveat:英文正文 + 手写感点缀,
@fontsource子集化 - prose-daisy:自定义
@tailwindcss/typography变体,文章正文自动用站点配色 - 无 Google Fonts:国内加载快且不需要翻墙
暗色模式 · 三档自由切换
darkMode: 'class'策略,由html.dark类控制- 中性墨色走 CSS 变量(
--ink、--ink-deep等),html.dark下整体翻转 → 所有text-ink-*utility 自动跟随,无需模板重复dark:前缀 - 头部 🌓 / ☀️ / 🌙 三档按钮,
useStorage持久化 +usePreferredDark监听系统,watchEffect精准 toggle html 的 dark class - 暗色阴影避开
dark:shadow-foo-bar的 Tailwind JIT 歧义,用html.dark + theme('boxShadow.xxx')查值 - 防御性 CSS:
html.dark .bg-cream-gradient强制覆盖 layout 根 bg-image,防止 dark 变体偶发失效造成"文字浅 + 底色浅 = 白屏"
Hello Kitty 风自定义指针
- 社区 PNG 接管 + 内嵌 SVG 回落(PNG 404 自动切 SVG,永不缺指针)
- 悬停
a / button / input时scale(1.3) rotate(-5deg);点击时scale(0.88)+ 樱桃粉涟漪 600ms 自动消失 - 只在
(pointer: fine)设备启用,触屏保持原生指针 - 尊重
prefers-reduced-motion→ 关闭拖尾 + 涟漪
实时天气 + 对应粒子特效
- 数据层:和风天气 QWeather API(专属 Host + API KEY),服务端
server/api/weather.get.ts代理 + 30min 内存缓存 + 三层降级(key 未配 / fetch 失败 / 返回异常都 mock) - Condition 归一化:QWeather 60+ icon code 归到自定义 9 类(sunny / clear-night / cloudy / cloudy-night / overcast / rain / storm / snow / fog)
- 头部 Widget:emoji + 温度 + hover 详情卡(体感 / 湿度 / 风力 / 能见度 / 数据源 / 更新时间)
- 城市切换:搜索框(防抖 350ms → QWeather GeoAPI → 前 8 条候选)+ 📍 浏览器定位 + ↺ 重置,
localStorage记住下次 - 全屏特效:
<Teleport to="body">挂 7 个子特效组件,根据 condition 动态渲染,每个都有暗色模式分支 +prefers-reduced-motion降级
细节工程
<ClientOnly>包所有浏览器 API 依赖(cursor / 天气 / 主题 toggle),避免 hydration mismatch- 智能 mouseleave:检测
relatedTarget === null(中文 IME 弹候选框的典型信号)+ 卡片内 focus 检测,避免用户打拼音时卡片被误关 - 鼠标桥 padding 替代 margin:hover 卡片与按钮之间的间隙用
pt-2包装层而非mt-2,让间隙在 hit-test 命中区内,鼠标跨过不触发 mouseleave - VueUse 用到了:
useStorage、usePreferredDark、useIntervalFn—— 省掉很多样板代码
部署
阿里云 ECS + sidecar nginx(端口 8000)+ ICP 备案(进行中)。/play/ 子路径是静态构建产物(npm run build:play),非 Nuxt 路由,Nitro 在 nuxt.config.ts 里显式 ignore 掉让它走纯静态服务。
踩坑实录 · 一次诡异的整页白屏
症状:暗色模式开关一点,整页变纯白。光看页面看不出任何东西,DevTools console 也没红字报错。Vue 没崩。
症状的特殊性:
- 浅色模式下完全正常 ✅
- 暗色模式下其它页面(不渲染
WeatherEffects的)也正常 ✅ - 但只要 layout 引入
<WeatherEffects />+ 切深色 → 白屏
用了一小时的二分排查:
- 禁掉 3 个新组件(ColorModeToggle / WeatherWidget / WeatherEffects)→ 页面恢复
- 逐个加回:ColorModeToggle ✅、WeatherWidget ✅、WeatherEffects ❌
- WeatherEffects 内部禁渲染但保留 imports → 仍白屏 → 不是渲染问题,是模块加载就坏了
- 进一步禁所有 sub-effect imports → 不白屏 → 凶手在 7 个 sub-effect 里
- 二分到 4 个(SunGlow + Stars + Clouds + Rain)→ 白
- 二分到 2 个(SunGlow + Clouds)→ 白
- 单挑 SunGlow → 白 → 凶手定位
- 删 SunGlow 整个
<style scoped>块 → 不白 → 是 CSS 问题 - 只留最可疑的一条
:global(html.dark) .sun-ray { mix-blend-mode: screen; opacity: 0.35; }→ 还是白
真相 · 看 Vite 编译输出:
curl http://localhost:3000/_nuxt/components/weather/SunGlow.vue?vue&type=style&...
返回的编译后 CSS 是:
html.dark {
mix-blend-mode: screen;
opacity: 0.35;
}
.sun-ray 后代选择器被 Vue 编译器吃掉了! 规则直接命中 <html> 元素,整页 35% 透明 → 透出浏览器默认白底 = 白屏。
根因
Vue 3 + Vite 的 <style scoped> 处理 :global(A) .B(部分 global)有 bug::global() 后面的后代会被错误丢弃。
看下面对比:
/* 🚫 写法 1:部分 global —— Vue 编译器会丢 .sun-ray,规则跑到 html 上 */
:global(html.dark) .sun-ray {
mix-blend-mode: screen;
}
/* ✅ 写法 2:整体 global —— 完整 selector 都在 :global() 里,Vue 不动它 */
:global(html.dark .sun-ray) {
mix-blend-mode: screen;
}
教训
- Scoped style + 后代选择器到全局元素 → 永远整体包
:global(...) - CSS 不显示就去看 Vite 编译输出(
?vue&type=style那个 URL),别只看源文件 - 二分法定位有 9 个组件的 bug,<1 小时:disable all → enable half → narrow → disable rules → narrow
修一次坑,全项目 8 个文件(7 子特效 + CustomCursor)一起换上正确写法,未来不再踩。
迭代打磨 · 这一版后又改了什么
白屏 bug 修完后,整个天气/暗色/代码块系统又跑了一轮细节打磨:
🌙 真月相计算 + 3D 球体渲染
clear-night 的星空里加了一个月亮 SVG,按当前真实日期计算月相:
// 以 2000-01-06 18:14 UTC 已知新月为锚,朔望月 29.530588853 天
function calculateMoonPhase(date = new Date()): number {
const REF = new Date('2000-01-06T18:14:00Z').getTime()
const SYNODIC = 29.530588853 * 86400000
return (((date.getTime() - REF) % SYNODIC) / SYNODIC + 1) % 1
}
0 = 新月,0.25 = 上弦,0.5 = 满月,0.75 = 下弦。对应农历初一 / 初七初八 / 十五 / 廿二廿三。
SVG 实现难点:动态切「阴晴圆缺」。用 mask 里一个移动的黑色圆来遮住亮面:
<mask>
<rect fill="white" />
<circle :cx="shadowCx" cy="50" r="45" fill="black" filter="url(#mask-feather)" />
</mask>
shadowCx 按 phase 插值(浅色在右 waxing → shadow 在左,暗色在右 waning → shadow 在右)。
立体感来自 5 层叠加:外层柔光 → 暗面渐变(左上亮右下暗)→ 亮面球体渐变 → 内阴影(右下暗化)→ 左上白色高光弧。再给 mask 的黑圆加 feGaussianBlur stdDeviation=2.5 让 terminator(明暗交界线)软化,不再是硬边。
不同步的多层动画 = 有呼吸:月亮椭圆轨道飘(18s)+ 自身呼吸缩放(7s)+ 外光晕独立 pulse(5.5s)+ 3 个 sparkle 精灵偶发闪出(6-8s 各异)。4 个周期数互不整除 → 永远不会「同步复位」,视觉上持续有微妙变化。
路由感知的月亮透明度
月亮 160px 放在右上,会叠在正文上。main 加 relative z-10 让正文永远在特效层之上,但深色模式下浅色正文叠在亮月亮上依然糊。解法:
const route = useRoute()
const moonOpacity = computed(() => route.path === '/' ? 0.85 : 0.55)
首页没大段正文,月亮可以亮饱满;文章页月亮半透明,背景透出来,文字清晰。transition: opacity 0.6s 让路由切换时月亮平滑变亮/变暗。
Fog 雾天 · 一个关于 backdrop-filter 的取舍
最初用 backdrop-filter: blur(5px) 做雾效:真的能让背景模糊起来,视觉上很"雾"。但文字也跟着模糊了,根本读不清。
改方案:
- 去掉
backdrop-filter - 保留色彩层:
rgba(175, 170, 200, 0.22)冷灰紫 wash 覆盖全屏 - 加 vignette:顶部 + 底部深色渐变 → 营造"远处雾更浓"的纵深错觉
- 远近双层雾块:3 个远景大色块(
blur(90-100px),慢漂)+ 4 个近景浓色块(blur(35-50px),快漂),错位重叠模拟立体雾 - 每块独立 animation,orbit + opacity + scale 三轴联动
文字依然清晰可读,但视觉上有「在雾里」的氛围。
SunGlow 晴天 · 加了可见太阳
最初只有 2 个 mix-blend-mode: multiply 的大色块 —— 太淡,看着像没晴。v2:
- 140px 可见太阳圆盘:
radial-gradient从中心白 → 暖黄 → 橙,带实体感 - 4 层 box-shadow 叠套辐射(近→远每层更大更淡)
- sun-core 内核:中心一抹更亮的白,4s shimmer 闪烁
- 3 个 sun-blob 大色块:暖金(右上)+ 桃粉(左下)+ 淡黄(中部)各自不同节奏 pulse
- 放弃
mix-blend-mode: multiply—— 直接实色叠加视觉更明确
Shiki 双主题 · 代码块跟模式走
原本 theme: 'github-light' 单主题 —— 暗色页面上 github-light 的深色 token 在自己配色的深紫底上读不出。改双主题:
highlight: {
theme: {
default: 'github-light',
dark: 'github-dark-dimmed' // 不刺眼的柔和暗主题
}
}
Shiki 把两份颜色同时 inline 到每个 token <span> 里,通过 CSS 变量切换:
html.dark .prose-daisy pre code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
注意:tailwind.config.js 里 prose 变体的改动不会被 HMR 热更新,必须 kill + restart dev server 才能生效。
Prose code 可见度打磨 · 三轮
| 轮次 | 问题 | 调整 |
|---|---|---|
| v1 | inline code 在浅色 cream 底上跟页面 bg 太接近,看不出边界 | bg 改 cream.300(深一档),加 1px solid primary.DEFAULT 淡紫边,加 fontWeight: 600,加微阴影 |
| v2 | pre 代码块在浅色模式也是深紫底(--tw-prose-pre-bg: night.50 写错了),shiki light token 深色字在深底上糊 |
--tw-prose-pre-bg 改 cream.100,暗色在 tailwind.css 里 html.dark .prose-daisy pre override |
| v3 | 暗色 pre 里 shiki 还是 light 主题颜色 | 配合上面 Shiki 双主题 + var 切换 |
这一版 commit 到 gitee 后,站点已经稳定可用。剩下的工作:3D 世界细节、项目集扩到 9 个、about 页拼贴装饰、ICP 备案完成。慢工细活。