📦 项目教程仓库:https://github.com/ZIQI-a/AI_Agent_study 🚀 成品项目地址:https://github.com/ZIQI-a/huamiao_Agent
本章目标
要做的事:安装 shadcn/ui,搭建话喵的整体布局(侧边栏 + 主内容区),设计配色方案
学到的知识:
- Tailwind CSS 的工作原理(原子化 CSS)
- shadcn/ui 的独特理念(不是 npm 包,是 copy 组件)
- 主题系统和自定义配色
- 响应式设计
3.1 Tailwind CSS 回顾
项目创建时已经自动集成了 Tailwind。快速回顾核心用法:
html
<!-- 传统 CSS -->
<button class="btn-primary">按钮</button>
<!-- 需要去 .css 文件里找 .btn-primary 的定义 -->
<!-- Tailwind CSS -->
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
按钮
</button>
<!-- 样式直接写在 className 里,所见即所得 -->
核心理念:一个类名 = 一个 CSS 属性
bash
bg-blue-500 → background-color: #3b82f6
text-white → color: white
px-4 → padding-left: 1rem; padding-right: 1rem
py-2 → padding-top: 0.5rem; padding-bottom: 0.5rem
rounded → border-radius: 0.25rem
hover:bg-blue-600 → 鼠标悬停时背景变深
flex → display: flex
items-center → align-items: center
gap-4 → gap: 1rem
不用写 CSS 文件,不用起类名,不用担心样式冲突。
3.2 安装 shadcn/ui
shadcn/ui 是目前最流行的 React 组件库之一。它的理念很独特:
bash
传统组件库(如 Ant Design、Material UI):
pnpm add antd → 安装为 node_modules 里的包
import { Button } from "antd" → 引用包里的组件
shadcn/ui:
npx shadcn@latest add button → 把组件代码复制到你的项目里
import { Button } from "@/components/ui/button" → 引用你项目里的组件
代码是你的,可以随意修改
不会因为版本更新而破坏你的定制
初始化 shadcn/ui
bash
# 在项目根目录执行
pnpm dlx shadcn@latest init
会问你几个问题:
bash
✔ Would you like to use CSS variables for theming? → Yes
✔ Which color would you like to use as the base color? → Neutral
✔ Would you like to use CSS variables for colors? → Yes
✔ Are you using a custom tailwind prefix? → (直接回车,不使用)
✔ Where is your global CSS file? → src/app/globals.css
✔ Do you want to use React Server Components? → Yes
✔ Configure the import alias for components? → @/components
✔ Configure the import alias for utils? → @/lib/utils
初始化完成后,你会看到 src/components/ui/ 目录被创建了。
添加常用组件
bash
# 添加按钮组件
pnpm dlx shadcn@latest add button
# 添加输入框
pnpm dlx shadcn@latest add input
# 添加卡片
pnpm dlx shadcn@latest add card
# 添加下拉选择
pnpm dlx shadcn@latest add select
# 添加对话框
pnpm dlx shadcn@latest add dialog
# 添加标签页
pnpm dlx shadcn@latest add tabs
# 添加 Toast 提示
pnpm dlx shadcn@latest add toast
# 添加骨架屏(加载占位)
pnpm dlx shadcn@latest add skeleton
# 一次性添加多个
pnpm dlx shadcn@latest add button input card select tabs toast skeleton
每个命令执行后,组件代码会被复制到 src/components/ui/ 目录。
3.3 自定义主题配色
我们为话喵设计一个温暖的猫咪主题配色。
打开 src/app/globals.css,修改 CSS 变量:
css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 40 33% 98%; /* 温暖的米白色 */
--foreground: 20 14% 12%; /* 深棕色文字 */
--card: 40 33% 96%; /* 卡片背景 */
--card-foreground: 20 14% 12%;
--popover: 40 33% 96%;
--popover-foreground: 20 14% 12%;
--primary: 25 85% 57%; /* 主色:温暖的橘色 */
--primary-foreground: 40 33% 98%;
--secondary: 30 20% 90%; /* 辅助色:浅驼色 */
--secondary-foreground: 20 14% 20%;
--muted: 30 15% 92%;
--muted-foreground: 20 8% 45%;
--accent: 45 80% 60%; /* 强调色:琥珀色 */
--accent-foreground: 20 14% 12%;
--destructive: 0 84% 60%;
--destructive-foreground: 40 33% 98%;
--border: 30 15% 87%;
--input: 30 15% 87%;
--ring: 25 85% 57%;
--radius: 0.75rem;
}
.dark {
--background: 20 14% 10%;
--foreground: 40 20% 92%;
--card: 20 14% 12%;
--card-foreground: 40 20% 92%;
--popover: 20 14% 12%;
--popover-foreground: 40 20% 92%;
--primary: 25 85% 57%;
--primary-foreground: 40 33% 98%;
--secondary: 20 10% 18%;
--secondary-foreground: 40 20% 90%;
--muted: 20 10% 16%;
--muted-foreground: 30 10% 55%;
--accent: 45 70% 50%;
--accent-foreground: 40 33% 98%;
--destructive: 0 63% 45%;
--destructive-foreground: 40 33% 98%;
--border: 20 10% 20%;
--input: 20 10% 20%;
--ring: 25 85% 57%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
知识点:HSL 颜色值 shadcn/ui 用 HSL(色相、饱和度、亮度)而不是十六进制来定义颜色。
25 85% 57%= 色相 25(橙色范围)、饱和度 85%、亮度 57% 这样调整颜色更直观:改色相换色调,改饱和度调整鲜艳程度。
3.4 搭建整体布局
我们要做一个经典的左侧导航 + 右侧内容的布局。
创建导航组件
创建 src/components/layout/sidebar.tsx:
typescript
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
// 导航菜单项
const navItems = [
{
label: "首页",
href: "/",
icon: "🏠",
},
{
label: "文章创作",
href: "/articles/create",
icon: "✍️",
},
{
label: "古诗词",
href: "/poems",
icon: "📜",
},
{
label: "风格文库",
href: "/styles",
icon: "📚",
},
{
label: "历史记录",
href: "/history",
icon: "📋",
},
];
export function Sidebar() {
const pathname = usePathname();
return (
<aside className="w-64 border-r bg-card flex flex-col">
{/* Logo */}
<div className="p-6 border-b">
<Link href="/" className="flex items-center gap-2">
<span className="text-3xl">🐱</span>
<div>
<h1 className="text-xl font-bold text-primary">话喵</h1>
<p className="text-xs text-muted-foreground">AI 智能创作平台</p>
</div>
</Link>
</div>
{/* 导航菜单 */}
<nav className="flex-1 p-4 space-y-1">
{navItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<span className="text-lg">{item.icon}</span>
{item.label}
</Link>
);
})}
</nav>
{/* 底部信息 */}
<div className="p-4 border-t">
<div className="text-xs text-muted-foreground">
<p>Powered by DeepSeek</p>
<p className="mt-1">v0.1.0</p>
</div>
</div>
</aside>
);
}
知识点:
cn()函数是什么? 这是 shadcn/ui 初始化时创建的工具函数(在src/lib/utils.ts里)。 它的作用是合并 Tailwind 类名,避免冲突:
typescriptimport { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
cn("text-red-500", "text-blue-500")→"text-blue-500"(后面的覆盖前面的)
创建主布局
修改 src/app/layout.tsx:
typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Sidebar } from "@/components/layout/sidebar";
import { Toaster } from "@/components/ui/toaster";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "话喵 - AI 智能创作平台",
description: "AI 驱动的文章创作、古诗词生成、风格仿写平台",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<div className="flex h-screen">
{/* 左侧导航 */}
<Sidebar />
{/* 右侧主内容 */}
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
<Toaster />
</body>
</html>
);
}
3.5 更新首页
修改 src/app/page.tsx:
typescript
import Link from "next/link";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
const features = [
{
title: "文章创作",
description: "输入标题,AI 帮你写文章。支持选择字数、风格和详细程度。",
icon: "✍️",
href: "/articles/create",
},
{
title: "古诗词生成",
description: "输入一个名词,AI 为你创作古诗词,附带注释和赏析。",
icon: "📜",
href: "/poems",
},
{
title: "风格文库",
description: "导入你喜欢的文章,AI 学习风格后进行仿写创作。",
icon: "📚",
href: "/styles",
},
];
export default function Home() {
return (
<div className="container mx-auto py-12 px-6">
{/* 头部 */}
<div className="text-center mb-12">
<h1 className="text-5xl font-bold mb-4">
<span className="text-primary">话喵</span>
<span className="text-3xl ml-2">🐱</span>
</h1>
<p className="text-xl text-muted-foreground">
AI 智能创作平台 --- 让文字有温度
</p>
</div>
{/* 功能卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
{features.map((feature) => (
<Link key={feature.href} href={feature.href}>
<Card className="h-full hover:shadow-lg transition-shadow cursor-pointer">
<CardHeader>
<div className="text-4xl mb-2">{feature.icon}</div>
<CardTitle>{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
</div>
);
}
3.6 创建页面容器组件
每个功能页面都需要类似的布局结构(标题 + 内容区),我们提取一个通用容器。
创建 src/components/layout/page-container.tsx:
typescript
interface PageContainerProps {
title: string;
description?: string;
children: React.ReactNode;
}
export function PageContainer({ title, description, children }: PageContainerProps) {
return (
<div className="container mx-auto py-8 px-6 max-w-4xl">
<div className="mb-8">
<h1 className="text-3xl font-bold">{title}</h1>
{description && (
<p className="text-muted-foreground mt-2">{description}</p>
)}
</div>
{children}
</div>
);
}
更新文章创作页面 src/app/articles/create/page.tsx:
typescript
import { PageContainer } from "@/components/layout/page-container";
export default function CreateArticle() {
return (
<PageContainer
title="文章创作"
description="输入标题,AI 帮你写文章"
>
<p className="text-muted-foreground">文章创作功能即将上线...</p>
</PageContainer>
);
}
更新古诗词页面 src/app/poems/page.tsx:
typescript
import { PageContainer } from "@/components/layout/page-container";
export default function Poems() {
return (
<PageContainer
title="古诗词生成"
description="输入名词,AI 创作古诗词"
>
<p className="text-muted-foreground">古诗词生成功能即将上线...</p>
</PageContainer>
);
}
3.7 响应式设计
当前布局在手机上侧边栏会挤占太多空间。我们加上响应式处理。
修改 src/components/layout/sidebar.tsx,在顶部加上移动端菜单:
typescript
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { useState } from "react";
const navItems = [
{ label: "首页", href: "/", icon: "🏠" },
{ label: "文章创作", href: "/articles/create", icon: "✍️" },
{ label: "古诗词", href: "/poems", icon: "📜" },
{ label: "风格文库", href: "/styles", icon: "📚" },
{ label: "历史记录", href: "/history", icon: "📋" },
];
export function Sidebar() {
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
return (
<>
{/* 移动端顶部栏 */}
<div className="md:hidden fixed top-0 left-0 right-0 z-50 bg-card border-b p-4 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<span className="text-2xl">🐱</span>
<span className="font-bold text-primary">话喵</span>
</Link>
<button
onClick={() => setIsOpen(!isOpen)}
className="p-2 hover:bg-muted rounded-lg"
>
{isOpen ? "✕" : "☰"}
</button>
</div>
{/* 遮罩层 */}
{isOpen && (
<div
className="md:hidden fixed inset-0 z-40 bg-black/50"
onClick={() => setIsOpen(false)}
/>
)}
{/* 侧边栏 */}
<aside
className={cn(
"fixed md:static inset-y-0 left-0 z-50 w-64 border-r bg-card flex flex-col transition-transform md:translate-x-0",
isOpen ? "translate-x-0" : "-translate-x-full"
)}
>
{/* Logo */}
<div className="p-6 border-b">
<Link
href="/"
className="flex items-center gap-2"
onClick={() => setIsOpen(false)}
>
<span className="text-3xl">🐱</span>
<div>
<h1 className="text-xl font-bold text-primary">话喵</h1>
<p className="text-xs text-muted-foreground">AI 智能创作平台</p>
</div>
</Link>
</div>
{/* 导航菜单 */}
<nav className="flex-1 p-4 space-y-1">
{navItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
onClick={() => setIsOpen(false)}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<span className="text-lg">{item.icon}</span>
{item.label}
</Link>
);
})}
</nav>
{/* 底部信息 */}
<div className="p-4 border-t">
<div className="text-xs text-muted-foreground">
<p>Powered by DeepSeek</p>
<p className="mt-1">v0.1.0</p>
</div>
</div>
</aside>
</>
);
}
同时修改 src/app/layout.tsx,给主内容区加上移动端的顶部间距:
typescript
// 修改 body 内的布局
<body className={inter.className}>
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-y-auto pt-16 md:pt-0">
{children}
</main>
</div>
<Toaster />
</body>
本章小结
| 概念 | 说明 |
|---|---|
| Tailwind CSS | 原子化 CSS,一个类名对应一个 CSS 属性 |
| shadcn/ui | 组件代码复制到你的项目,完全可定制 |
| CSS 变量主题 | 用 HSL 值定义颜色,支持暗色模式 |
cn() 函数 |
合并 Tailwind 类名,避免冲突 |
| 响应式设计 | md: 前缀表示在中等屏幕以上生效 |
动手验证
- 启动
pnpm dev,看到话喵的布局(左侧导航 + 右侧内容) - 点击导航栏切换页面,观察高亮状态
- 缩小浏览器窗口,侧边栏自动隐藏,出现汉堡菜单
- 点击汉堡菜单,侧边栏从左侧滑出
- 首页显示三个功能卡片,点击可跳转
下一章预告
我们将第一次调用 AI API ------ 用 DeepSeek 的大模型生成一段文字。你将理解 LLM API 的调用方式、Token 的概念、以及如何处理 API 错误。
如果这个教程对你有帮助,欢迎 ⭐ Star 支持一下!