Next.js 16 全棧實戰(二):引入 Shadcn UI 與構建 Dashboard 佈局
整理版優先睇
用 Shadcn UI 同 Vibe Coding 快速整好 Dashboard 佈局,跟住用 AI 微調 UI 細節。
呢篇文章係 Next.js 16 全棧實戰系列第二篇,作者承接上一篇用 Trae 初始化嘅基礎,逐步教讀者點樣引入 Shadcn UI 呢個 React 組件庫,同埋規劃 App Router 嘅目錄結構。作者想解決嘅問題係:好多開發者慣用 Ant Design 呢類黑盒庫,想改樣式或者行為時好麻煩;而 Shadcn UI 係一個代碼生成器,可以畀你完全掌控組件源碼。整體結論係成功建立咗一個經典嘅左側固定側邊欄加右側內容自適應嘅 Dashboard 佈局,而且示範咗用 Trae 嘅 Vibe Coding 功能,透過自然語言提示詞快速調整 UI,例如加選中效果同調整 Logo 高度,提升開發效率。
實戰部分由安裝 Shadcn UI 開始,揀咗 Zinc 中性灰做主色,配合 Lucide React 圖標庫。然後規劃目錄:app/ui 放組件、app/lib 放工具函數、app/dashboard 放業務頁面。跟住手寫代碼,首先整 SideNav 組件,用 Link 實現無刷新跳轉;再透過 layout.tsx 共享佈局,令側邊欄喺頁面切換時保持狀態;最後建立工作台頁面,顯示模擬數據卡片。成個佈局用 Tailwind 嘅 md: 前綴實現響應式,移動端側邊欄會變頂部導航。
最後作者用 Trae 嘅「選中元素」功能,直接對 UI 元素落提示詞,例如「工作台增加一個選中效果」,AI 就自動改好代碼,再經代碼審查確認後接受…
- Shadcn UI 嘅核心賣點係代碼生成器,唔係黑盒庫,開發者有 100% 控制權,可以隨意改組件原始碼。
- 用 npx shadcn@latest init 初始化,揀 Zinc 中性灰色主題,然後裝 lucide-react 做圖標庫。
- 目錄結構要分明:app/ui 放 UI 組件、app/lib 放工具函數、app/dashboard 放業務頁面,方便後期擴展。
- 實戰用 Link 組件同 Tailwind 嘅 md: 前綴,整出左側固定、右邊自適應嘅 Dashboard 佈局,移動端會自動轉頂部導航。
- 透過 Trae 嘅「選中元素」功能,直接用自然語言提示(例如「工作台增加選中效果」),AI 會自動改好代碼,經審查後接受修改。
點解揀 Shadcn UI?
好多初學者習慣用 Ant Design 或者 Material UI,但呢啲庫係 npm 安裝嘅黑盒,想改樣式或者行為時要繞好多彎路。Shadcn UI 唔同,佢係一個代碼生成器,你執行命令之後,佢會將組件嘅源碼直接「複製」入你個項目度,咁你就可以隨意修改,擁有 100% 嘅控制權。
Shadcn UI 唔係 npm 套件,而係一組可以直接改原始碼嘅組件
目錄結構規劃
喺 Next.js 16 App Router 入面,檔案放邊度好講究。為咗令項目後期唔會亂,最好一開始就起好「房間」:app/ui 專門放 UI 組件(側邊欄、卡片等),app/lib 放數據定義同工具函數,app/dashboard 放核心業務頁面——呢個文件夾底下嘅 page.tsx 只有登錄後先可以訪問。
用 app/ui、app/lib、app/dashboard 劃分職責,日後加功能就唔會亂
實戰搭建 Dashboard 佈局
目標係整一個經典嘅「左側固定側邊欄 + 右側內容自適應」佈局。首先建立 app/ui/dashboard/sidenav.tsx,用 Next.js 嘅 <Link> 組件實現無刷新跳轉,再用 Tailwind 寫樣式。
import Link from 'next/link';
import { LayoutDashboard, Building2, Users, ShieldCheck, Menu, GraduationCap, LogOut } from 'lucide-react';
const links = [
{ name: '工作台', href: '/dashboard', icon: LayoutDashboard },
{ name: '機構管理', href: '/dashboard/dept', icon: Building2 },
{ name: '人員管理', href: '/dashboard/user', icon: Users },
{ name: '角色管理', href: '/dashboard/role', icon: ShieldCheck },
{ name: '菜單配置', href: '/dashboard/menu', icon: Menu },
];
export default function SideNav() {
return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
<Link className="mb-2 flex h-20 items-end justify-start rounded-md bg-zinc-900 p-4 md:h-40" href="/">
<div className="w-32 text-white md:w-40 flex items-center gap-2">
<GraduationCap className="h-8 w-8" />
<span className="text-lg font-bold">教培管家</span>
</div>
</Link>
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link key={link.name} href={link.href}
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<form>
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<LogOut className="w-6" />
<div className="hidden md:block">退出登錄</div>
</button>
</form>
</div>
</div>
);
}
跟住建立 app/dashboard/layout.tsx,用 Next.js 嘅 layout 特性共享側邊欄,切換頁面時側邊欄唔會重新渲染。
import SideNav from '@/app/ui/dashboard/sidenav';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64 bg-white border-r">
<SideNav />
</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12 bg-gray-50">
{children}
</div>
</div>
);
}
用 layout.tsx 包住側邊欄,頁面切換時側邊欄保持狀態,唔使重新加載
最後整一個簡單嘅工作台頁面 app/dashboard/page.tsx,放一啲模擬數據卡片,用嚟驗證佈局效果。
export default function Page() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">工作台 Dashboard</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div className="h-32 rounded-xl bg-white p-4 shadow-sm border border-gray-100">
<p className="text-sm text-gray-500">總學員數</p>
<p className="text-2xl font-bold">1,203</p>
</div>
<div className="h-32 rounded-xl bg-white p-4 shadow-sm border border-gray-100">
<p className="text-sm text-gray-500">本月營收</p>
<p className="text-2xl font-bold">¥ 45,231</p>
</div>
</div>
</div>
);
}
Vibe Coding 調整 UI
默認生成嘅 UI 未必合心水,可以用 Trae 嘅「選中元素」功能來 vibe coding。喺 Trae 打開頁面,用選中元素點擊左側工作台菜單,右邊對話框會見到對應嘅 a 標籤,輸入提示詞「工作台增加一個選中效果」,AI 就會自動幫你修改代碼。
直接對 UI 元素講「工作台增加一個選中效果」,AI 就識幫你改好
改完之後點擊代碼審查,確認冇問題就接受修改。同樣方法,如果覺得 Logo 太高,只要講「調整到合適嘅高度」,AI 又會幫你搞掂。作者話:大模型多模態能力配合選中元素功能,只要你表達清楚意圖,就可以又好又快咁實現功能。
回顧之前嘅內容
1. 點解揀 Shadcn UI?
1.1 初始化 Shadcn
npx shadcn@latest init
1.2 安裝基礎圖標庫
npminstall lucide-react
2. 目錄結構規劃
3. 實戰:搭建 Dashboard 佈局
3.1 第一步:編寫側邊欄組件 (SideNav)
import Link from 'next/link';
import {
LayoutDashboard,
Building2,
Users,
ShieldCheck,
Menu,
GraduationCap,
LogOut
} from 'lucide-react';
// 定義菜單數據(暫時寫死,後續會從數據庫讀取)
const links = [
{ name: '工作台', href: '/dashboard', icon: LayoutDashboard },
{ name: '機構管理', href: '/dashboard/dept', icon: Building2 },
{ name: '人員管理', href: '/dashboard/user', icon: Users },
{ name: '角色管理', href: '/dashboard/role', icon: ShieldCheck },
{ name: '菜單配置', href: '/dashboard/menu', icon: Menu },
];
export default function SideNav() {
return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
{/* 1. Logo 區域 */}
<Link
className="mb-2 flex h-20 items-end justify-start rounded-md bg-zinc-900 p-4 md:h-40"
href="/"
>
<div className="w-32 text-white md:w-40 flex items-center gap-2">
<GraduationCap className="h-8 w-8" />
<span className="text-lg font-bold">教培管家</span>
</div>
</Link>
{/* 2. 導航連結區域 */}
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
{/* 佔位符,把登出按鈕頂到底部 */}
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
{/* 3. 登出按鈕 */}
<form>
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<LogOut className="w-6" />
<div className="hidden md:block">退出登錄</div>
</button>
</form>
</div>
</div>
);
}
3.2 第二步:創建佈局文件 (Layout)
import SideNav from '@/app/ui/dashboard/sidenav';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
{/* 側邊欄區域:在移動端是頂部導航,在桌面端是左側固定 */}
<div className="w-full flex-none md:w-64 bg-white border-r">
<SideNav />
</div>
{/* 主內容區域:可滾動 */}
<div className="flex-grow p-6 md:overflow-y-auto md:p-12 bg-gray-50">
{children}
</div>
</div>
);
}
3.3 第三步:創建第一個頁面
export default function Page() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">工作台 Dashboard</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{/* 這裏以後放統計卡片 */}
<div className="h-32 rounded-xl bg-white p-4 shadow-sm border border-gray-100">
<p className="text-sm text-gray-500">總學員數</p>
<p className="text-2xl font-bold">1,203</p>
</div>
<div className="h-32 rounded-xl bg-white p-4 shadow-sm border border-gray-100">
<p className="text-sm text-gray-500">本月營收</p>
<p className="text-2xl font-bold">¥ 45,231</p>
</div>
</div>
</div>
);
}
4. 見證奇蹟
npm run dev
5 vibe coding
工作台增加一個選中的效果
下一步預告
前情回顧
1. 為什麼選擇 Shadcn UI?
1.1 初始化 Shadcn
npx shadcn@latest init
1.2 安裝基礎圖標庫
npminstall lucide-react
2. 目錄結構規劃
3. 實戰:搭建 Dashboard 佈局
3.1 第一步:編寫側邊欄組件 (SideNav)
import Link from 'next/link';
import {
LayoutDashboard,
Building2,
Users,
ShieldCheck,
Menu,
GraduationCap,
LogOut
} from 'lucide-react';
// 定義菜單數據(暫時寫死,後續會從數據庫讀取)
const links = [
{ name: '工作台', href: '/dashboard', icon: LayoutDashboard },
{ name: '機構管理', href: '/dashboard/dept', icon: Building2 },
{ name: '人員管理', href: '/dashboard/user', icon: Users },
{ name: '角色管理', href: '/dashboard/role', icon: ShieldCheck },
{ name: '菜單配置', href: '/dashboard/menu', icon: Menu },
];
export default function SideNav() {
return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
{/* 1. Logo 區域 */}
<Link
className="mb-2 flex h-20 items-end justify-start rounded-md bg-zinc-900 p-4 md:h-40"
href="/"
>
<div className="w-32 text-white md:w-40 flex items-center gap-2">
<GraduationCap className="h-8 w-8" />
<span className="text-lg font-bold">教培管家</span>
</div>
</Link>
{/* 2. 導航連結區域 */}
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
{/* 佔位符,把登出按鈕頂到底部 */}
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
{/* 3. 登出按鈕 */}
<form>
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<LogOut className="w-6" />
<div className="hidden md:block">退出登錄</div>
</button>
</form>
</div>
</div>
);
}
3.2 第二步:創建佈局文件 (Layout)
import SideNav from '@/app/ui/dashboard/sidenav';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
{/* 側邊欄區域:在移動端是頂部導航,在桌面端是左側固定 */}
<div className="w-full flex-none md:w-64 bg-white border-r">
<SideNav />
</div>
{/* 主內容區域:可滾動 */}
<div className="flex-grow p-6 md:overflow-y-auto md:p-12 bg-gray-50">
{children}
</div>
</div>
);
}
3.3 第三步:創建第一個頁面
export default function Page() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">工作台 Dashboard</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{/* 這裏以後放統計卡片 */}
<div className="h-32 rounded-xl bg-white p-4 shadow-sm border border-gray-100">
<p className="text-sm text-gray-500">總學員數</p>
<p className="text-2xl font-bold">1,203</p>
</div>
<div className="h-32 rounded-xl bg-white p-4 shadow-sm border border-gray-100">
<p className="text-sm text-gray-500">本月營收</p>
<p className="text-2xl font-bold">¥ 45,231</p>
</div>
</div>
</div>
);
}
4. 見證奇蹟
npm run dev
5 vibe coding
工作台增加一個選中的效果

















