07. 样式方案实战

Next.js 支持多种样式方案,包括 CSS Modules、Tailwind CSS、Styled Components 等。本章将重点讲解最常用的 Tailwind CSS 和 CSS Modules。


1. Tailwind CSS(推荐)

Tailwind CSS 是一个实用优先的 CSS 框架,通过类名快速构建界面。

为什么选择 Tailwind?

  • 快速开发:不需要写自定义 CSS
  • 响应式设计:内置断点系统
  • 按需打包:只打包使用的样式
  • 一致性:设计系统统一

基础用法

// src/app/page.tsx
export default function Home() {
  return (
    <div className="min-h-screen bg-gray-100">
      <div className="max-w-4xl mx-auto px-4 py-20">
        <h1 className="text-4xl font-bold text-gray-900 mb-4">
          欢迎使用 Tailwind CSS
        </h1>
        <p className="text-lg text-gray-600 mb-8">
          这是一个使用 Tailwind CSS 构建的页面
        </p>
        <button className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors">
          点击我
        </button>
      </div>
    </div>
  )
}

常用工具类

布局

<div className="flex items-center justify-between">
  <div>左侧</div>
  <div>右侧</div>
</div>
 
<div className="grid grid-cols-3 gap-4">
  <div>项目 1</div>
  <div>项目 2</div>
  <div>项目 3</div>
</div>

间距

<div className="p-4 m-2">内边距 4,外边距 2</div>
<div className="px-6 py-4">水平 6,垂直 4</div>
<div className="mt-8 mb-4">上边距 8,下边距 4</div>

颜色

<div className="bg-blue-500 text-white">蓝色背景,白色文字</div>
<div className="text-gray-600 border-gray-300">灰色文字和边框</div>

响应式设计

<div className="text-sm md:text-base lg:text-lg xl:text-xl">
  响应式文字大小
</div>
 
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  响应式网格
</div>

2. 实战案例:卡片组件

基础卡片

// src/components/Card.tsx
interface CardProps {
  title: string
  description: string
  image?: string
}
 
export default function Card({ title, description, image }: CardProps) {
  return (
    <div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
      {image && (
        <img
          src={image}
          alt={title}
          className="w-full h-48 object-cover"
        />
      )}
      <div className="p-6">
        <h3 className="text-xl font-semibold mb-2">{title}</h3>
        <p className="text-gray-600">{description}</p>
      </div>
    </div>
  )
}

使用示例

// src/app/products/page.tsx
import Card from '@/components/Card'
 
export default function ProductsPage() {
  return (
    <div className="max-w-6xl mx-auto px-4 py-20">
      <h1 className="text-4xl font-bold mb-8">商品列表</h1>
      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        <Card
          title="商品 A"
          description="这是商品 A 的描述"
          image="/images/product-a.jpg"
        />
        <Card
          title="商品 B"
          description="这是商品 B 的描述"
          image="/images/product-b.jpg"
        />
        <Card
          title="商品 C"
          description="这是商品 C 的描述"
          image="/images/product-c.jpg"
        />
      </div>
    </div>
  )
}

3. CSS Modules

CSS Modules 允许你编写局部作用域的 CSS,避免样式冲突。

创建 CSS Module

/* src/components/Button.module.css */
.button {
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  font-weight: 500;
  transition: all 0.2s;
}
 
.primary {
  background-color: #2563eb;
  color: white;
}
 
.primary:hover {
  background-color: #1d4ed8;
}
 
.secondary {
  background-color: #e5e7eb;
  color: #374151;
}
 
.secondary:hover {
  background-color: #d1d5db;
}

在组件中使用

// src/components/Button.tsx
import styles from './Button.module.css'
 
interface ButtonProps {
  children: React.ReactNode
  variant?: 'primary' | 'secondary'
  onClick?: () => void
}
 
export default function Button({
  children,
  variant = 'primary',
  onClick,
}: ButtonProps) {
  return (
    <button
      className={`${styles.button} ${styles[variant]}`}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

组合类名

使用 clsxclassnames 库组合类名:

npm install clsx
// src/components/Button.tsx
import clsx from 'clsx'
import styles from './Button.module.css'
 
export default function Button({
  children,
  variant = 'primary',
  disabled = false,
  onClick,
}: ButtonProps) {
  return (
    <button
      className={clsx(styles.button, styles[variant], {
        [styles.disabled]: disabled,
      })}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  )
}

4. 全局样式

使用 globals.css

/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
 
/* 自定义全局样式 */
:root {
  --primary-color: #2563eb;
  --secondary-color: #64748b;
}
 
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
 
/* 自定义工具类 */
@layer components {
  .btn-primary {
    @apply bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700;
  }
  
  .card {
    @apply bg-white rounded-lg shadow-md p-6;
  }
}

在组件中使用自定义工具类

// src/app/page.tsx
export default function Home() {
  return (
    <div>
      <button className="btn-primary">主要按钮</button>
      <div className="card">卡片内容</div>
    </div>
  )
}

5. 实战案例:响应式导航栏

使用 Tailwind CSS 实现

// src/components/Navbar.tsx
'use client'
 
import { useState } from 'react'
import Link from 'next/link'
 
export default function Navbar() {
  const [isOpen, setIsOpen] = useState(false)
 
  return (
    <nav className="bg-white shadow-md">
      <div className="max-w-6xl mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          {/* Logo */}
          <Link href="/" className="text-2xl font-bold text-blue-600">
            我的网站
          </Link>
 
          {/* 桌面导航 */}
          <div className="hidden md:flex gap-6">
            <Link href="/" className="text-gray-700 hover:text-blue-600">
              首页
            </Link>
            <Link href="/about" className="text-gray-700 hover:text-blue-600">
              关于
            </Link>
            <Link href="/blog" className="text-gray-700 hover:text-blue-600">
              博客
            </Link>
            <Link href="/contact" className="text-gray-700 hover:text-blue-600">
              联系
            </Link>
          </div>
 
          {/* 移动端菜单按钮 */}
          <button
            className="md:hidden text-gray-700"
            onClick={() => setIsOpen(!isOpen)}
          >
            <svg
              className="w-6 h-6"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              {isOpen ? (
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M6 18L18 6M6 6l12 12"
                />
              ) : (
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M4 6h16M4 12h16M4 18h16"
                />
              )}
            </svg>
          </button>
        </div>
 
        {/* 移动端菜单 */}
        {isOpen && (
          <div className="md:hidden py-4 space-y-2">
            <Link
              href="/"
              className="block px-4 py-2 text-gray-700 hover:bg-gray-100"
              onClick={() => setIsOpen(false)}
            >
              首页
            </Link>
            <Link
              href="/about"
              className="block px-4 py-2 text-gray-700 hover:bg-gray-100"
              onClick={() => setIsOpen(false)}
            >
              关于
            </Link>
            <Link
              href="/blog"
              className="block px-4 py-2 text-gray-700 hover:bg-gray-100"
              onClick={() => setIsOpen(false)}
            >
              博客
            </Link>
            <Link
              href="/contact"
              className="block px-4 py-2 text-gray-700 hover:bg-gray-100"
              onClick={() => setIsOpen(false)}
            >
              联系
            </Link>
          </div>
        )}
      </div>
    </nav>
  )
}

6. 实战案例:表单样式

使用 Tailwind CSS

// src/components/ContactForm.tsx
'use client'
 
import { useState } from 'react'
 
export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  })
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    // 处理提交
    console.log(formData)
  }
 
  return (
    <form onSubmit={handleSubmit} className="max-w-2xl mx-auto space-y-6">
      <div>
        <label
          htmlFor="name"
          className="block text-sm font-medium text-gray-700 mb-2"
        >
          姓名
        </label>
        <input
          type="text"
          id="name"
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          required
        />
      </div>
 
      <div>
        <label
          htmlFor="email"
          className="block text-sm font-medium text-gray-700 mb-2"
        >
          邮箱
        </label>
        <input
          type="email"
          id="email"
          value={formData.email}
          onChange={(e) =>
            setFormData({ ...formData, email: e.target.value })
          }
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          required
        />
      </div>
 
      <div>
        <label
          htmlFor="message"
          className="block text-sm font-medium text-gray-700 mb-2"
        >
          留言
        </label>
        <textarea
          id="message"
          value={formData.message}
          onChange={(e) =>
            setFormData({ ...formData, message: e.target.value })
          }
          rows={6}
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          required
        />
      </div>
 
      <button
        type="submit"
        className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors font-medium"
      >
        发送消息
      </button>
    </form>
  )
}

7. 自定义 Tailwind 配置

修改 tailwind.config.js

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          // ... 更多颜色
          900: '#1e3a8a',
        },
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
    },
  },
  plugins: [],
}

使用自定义颜色

<div className="bg-primary-500 text-primary-50">
  使用自定义颜色
</div>

8. 样式方案选择指南

场景推荐方案原因
快速原型开发Tailwind CSS开发速度快
组件库开发CSS Modules样式隔离,易于维护
大型项目Tailwind + CSS Modules结合两者优势
需要主题切换CSS Variables + Tailwind动态主题支持

9. 实战练习:构建响应式卡片网格

需求

创建一个响应式卡片网格,要求:

  • 移动端:1 列
  • 平板:2 列
  • 桌面:3 列
  • 卡片有悬停效果

实现

// src/app/gallery/page.tsx
const items = [
  { id: 1, title: '项目 1', description: '描述 1' },
  { id: 2, title: '项目 2', description: '描述 2' },
  { id: 3, title: '项目 3', description: '描述 3' },
  { id: 4, title: '项目 4', description: '描述 4' },
  { id: 5, title: '项目 5', description: '描述 5' },
  { id: 6, title: '项目 6', description: '描述 6' },
]
 
export default function GalleryPage() {
  return (
    <div className="min-h-screen bg-gray-100 py-20">
      <div className="max-w-7xl mx-auto px-4">
        <h1 className="text-4xl font-bold mb-12 text-center">作品展示</h1>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {items.map((item) => (
            <div
              key={item.id}
              className="bg-white rounded-lg shadow-md p-6 hover:shadow-xl transition-shadow transform hover:-translate-y-1"
            >
              <h3 className="text-xl font-semibold mb-2">{item.title}</h3>
              <p className="text-gray-600">{item.description}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

10. 总结

本章我们学习了:

  1. ✅ Tailwind CSS 的基础用法和常用工具类
  2. ✅ CSS Modules 的使用方法
  3. ✅ 全局样式的配置
  4. ✅ 响应式设计的实现
  5. ✅ 构建了导航栏、表单等实战案例

关键要点

  • 🎨 Tailwind CSS 适合快速开发
  • 🎨 CSS Modules 适合组件样式隔离
  • 🎨 响应式设计使用 Tailwind 的断点系统
  • 🎨 可以结合使用多种样式方案

下一步:在下一章,我们将学习静态资源管理,包括图片、字体等的使用。