所有项目

Daisy's Garden

⭐ 精选

你正在看的这个站。Nuxt 3 + Tailwind + @nuxt/content + VueUse + QWeather。博客、项目集、3D 子路径、实时天气特效、三档暗色模式、Hello Kitty 指针。

📅 2026Nuxt 3TypeScriptTailwind@nuxt/contentPiniaVueUseQWeather API

为什么自己写

市面上成熟的博客框架(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 / inputscale(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 用到了useStorageusePreferredDarkuseIntervalFn —— 省掉很多样板代码

部署

阿里云 ECS + sidecar nginx(端口 8000)+ ICP 备案(进行中)。/play/ 子路径是静态构建产物(npm run build:play),非 Nuxt 路由,Nitro 在 nuxt.config.ts 里显式 ignore 掉让它走纯静态服务。

踩坑实录 · 一次诡异的整页白屏

症状:暗色模式开关一点,整页变纯白。光看页面看不出任何东西,DevTools console 也没红字报错。Vue 没崩。

症状的特殊性

  • 浅色模式下完全正常 ✅
  • 暗色模式下其它页面(不渲染 WeatherEffects 的)也正常 ✅
  • 但只要 layout 引入 <WeatherEffects /> + 切深色 → 白屏

用了一小时的二分排查

  1. 禁掉 3 个新组件(ColorModeToggle / WeatherWidget / WeatherEffects)→ 页面恢复
  2. 逐个加回:ColorModeToggle ✅、WeatherWidget ✅、WeatherEffects ❌
  3. WeatherEffects 内部禁渲染但保留 imports → 仍白屏 → 不是渲染问题,是模块加载就坏了
  4. 进一步禁所有 sub-effect imports → 不白屏 → 凶手在 7 个 sub-effect 里
  5. 二分到 4 个(SunGlow + Stars + Clouds + Rain)→ 白
  6. 二分到 2 个(SunGlow + Clouds)→ 白
  7. 单挑 SunGlow → 白 → 凶手定位
  8. 删 SunGlow 整个 <style scoped> → 不白 → 是 CSS 问题
  9. 只留最可疑的一条 :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 放在右上,会叠在正文上。mainrelative 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-bgcream.100,暗色在 tailwind.css 里 html.dark .prose-daisy pre override
v3 暗色 pre 里 shiki 还是 light 主题颜色 配合上面 Shiki 双主题 + var 切换

这一版 commit 到 gitee 后,站点已经稳定可用。剩下的工作:3D 世界细节、项目集扩到 9 个、about 页拼贴装饰、ICP 备案完成。慢工细活。