[Nuxt 4 实战] 细节决定成败:深色模式动画、骨架屏与 404 页面的艺术

15 阅读3分钟

前言

在开发 SonicToolLab 时,我发现用户对工具站的耐受度其实很低。

  • 如果点击转换按钮后界面卡死,用户会以为网页坏了。
  • 如果在晚上打开网页被亮瞎眼,用户会直接关闭。
  • 如果输错网址看到原生丑陋的 404,用户会觉得这个站很“水”。

今天这一篇,我们不聊复杂的算法,只聊体验。如何利用 Nuxt UI 提供的组件,花 20% 的时间提升 80% 的质感。

🌗 1. 丝滑的深色模式 (Dark Mode)

Nuxt UI 基于 Tailwind CSS,天生支持深色模式。但要做到“好用”,有几个关键步骤。

步骤一:配置 Color Mode

虽然 Nuxt UI 默认集成了 @nuxtjs/color-mode,但我建议在 nuxt.config.ts 中显式配置一下,确保它使用 class 策略(给 html 标签加 dark 类名)。

// nuxt.config.ts
export default defineNuxtConfig({
  colorMode: {
    classSuffix: '', // 配合 Tailwind,去掉了默认的 -mode 后缀
    preference: 'system', // 默认跟随系统
    fallback: 'light' // 兜底策略
  }
})

步骤二:封装切换组件

不要把切换逻辑散落在各处。我封装了一个通用的 <ColorModeButton />

难点:Hydration Mismatch(水合不匹配)

服务端渲染时不知道用户的系统是黑还是白,可能会导致图标闪烁(服务端渲染了月亮,客户端变成了太阳)。

解法: 使用 <ClientOnly> 包裹按钮,或者使用 v-if 等待挂载。

Code snippet

<script setup>
const colorMode = useColorMode()

// 计算属性:判断当前是否为暗色
const isDark = computed({
  get () {
    return colorMode.value === 'dark'
  },
  set () {
    // 切换模式
    colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
  }
})
</script>

<template>
  <ClientOnly>
    <UButton
      :icon="isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'"
      color="gray"
      variant="ghost"
      aria-label="切换主题"
      @click="isDark = !isDark"
    />
    
    <template #fallback>
      <div class="w-8 h-8" /> 
    </template>
  </ClientOnly>
</template>

⏳ 2. 拒绝白屏:Loading 状态与骨架屏

工具站经常需要请求 API(比如汇率、Whois 信息)。在数据回来之前,千万不要留白。

全局加载条

Nuxt 内置了 <NuxtLoadingIndicator />。在 app.vue 顶部加上它:

Code snippet

<template>
  <NuxtLoadingIndicator color="#34d399" />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

这样,路由切换时顶部会有进度条,给用户“由于我在加载,请稍等”的反馈。

局部骨架屏 (Skeleton)

Nuxt UI 提供了 USkeleton 组件。配合 useFetchstatus 非常好用。

Code snippet

<script setup>
const { data, status } = await useFetch('/api/tools/list', { lazy: true })
</script>

<template>
  <div class="grid grid-cols-3 gap-4">
    <template v-if="status === 'pending'">
      <UCard v-for="i in 6" :key="i">
        <div class="flex items-center gap-4">
          <USkeleton class="h-12 w-12 rounded-full" />
          <div class="space-y-2">
            <USkeleton class="h-4 w-[200px]" />
            <USkeleton class="h-4 w-[150px]" />
          </div>
        </div>
      </UCard>
    </template>

    <template v-else>
      <ToolCard v-for="tool in data" :tool="tool" />
    </template>
  </div>
</template>

这种体验比一个旋转的 Spinner 要高级得多,因为它提前勾勒出了页面的布局。

🚧 3. 自定义 404 页面

Nuxt 默认的错误页面是黑底白字的调试风,上线后如果用户输错网址看到这个,会非常出戏。

在项目根目录创建 error.vue(注意不是在 pages 里,是根目录)。

Code snippet

<script setup lang="ts">
const props = defineProps({
  error: Object
})

const handleError = () => {
  // 清除错误并跳回首页
  clearError({ redirect: '/' })
}
</script>

<template>
  <div class="h-screen flex flex-col items-center justify-center text-center p-4">
    <h1 class="text-9xl font-bold text-primary-500">404</h1>
    
    <p class="text-xl mt-4 text-gray-500 dark:text-gray-400">
      糟糕,你仿佛来到了知识的荒原...
    </p>

    <p class="text-sm mt-2 mb-8 text-gray-400">
      错误信息:{{ error?.message }}
    </p>

    <UButton 
      size="xl" 
      icon="i-heroicons-home"
      @click="handleError"
    >
      带我回首页
    </UButton>
  </div>
</template>

这样,即使用户迷路了,也能通过一个友好的按钮找回方向,大大降低了跳出率。

总结

UI/UX 的打磨是没有尽头的,但在 SonicToolLab 的开发中,以上三点是性价比最高的投入:

  1. Dark Mode 照顾了开发者的眼睛(这是你的核心用户群)。
  2. Skeleton 缓解了等待的焦虑。
  3. Custom 404 挽留了迷路的用户。

把这些细节做好,你的网站就从“一个简单的 Demo”进化成了“一个成熟的产品”。

👉 SonicToolLab 在线体验