目录
- 前言
- [1. 设计"大脑":定义 TypeScript 类型](#1. 设计“大脑”:定义 TypeScript 类型)
- [2. 注入灵魂:编写数据库种子脚本 (Seed)](#2. 注入灵魂:编写数据库种子脚本 (Seed))
-
- [2.1 准备假数据](#2.1 准备假数据)
- [2.2 编写执行脚本](#2.2 编写执行脚本)
- [2.3 执行初始化](#2.3 执行初始化)
- [3. 核心:Server Components 数据获取](#3. 核心:Server Components 数据获取)
- [4. 联调:让 Sidebar 动起来](#4. 联调:让 Sidebar 动起来)
- [5. 验证成果](#5. 验证成果)
- 下一步预告
前言
前情回顾:
在第二篇中,我们引入了 Shadcn UI,并搭建了 Dashboard 的静态布局。
本篇目标:
- 数据建模:设计支持 RBAC(基于角色的权限控制)的数据库表结构。
- 数据填充:编写 Seed 脚本,一键初始化数据库。
- 服务端数据获取:利用 Next.js Server Components 的特性,直接从数据库读取菜单,让侧边栏"活"起来。
1. 设计"大脑":定义 TypeScript 类型
在全栈开发中,类型安全是我们的护城河。我们先定义数据长什么样。
在 app/lib 目录下,新建文件 definitions.ts。我们将定义核心的"菜单"和"角色"类型。
typescript
// app/lib/definitions.ts
// 菜单类型定义
export type Menu = {
menu_id: string; // UUID
parent_id: string; // 父菜单ID
menu_name: string; // 菜单显示名称
menu_type: 'M' | 'C' | 'F'; // M:目录, C:菜单, F:按钮
path: string; // 路由路径
perms: string; // 权限标识 (如 system:user:list)
icon?: string; // 图标字符串
};
// 角色类型定义
export type Role = {
role_id: string;
role_name: string;
role_key: string; // 如 admin, editor
data_scope: string; // 数据权限范围
};
AI 编程技巧:
你可以直接把这两个类型复制给 Trae 的 AI 助手,告诉它:"记住这些类型定义,接下来的数据库查询代码都要符合这个结构。"

2. 注入灵魂:编写数据库种子脚本 (Seed)
数据库现在是空的,我们需要一些初始数据(比如"系统管理"菜单、"超级管理员"用户)。
2.1 准备假数据
新建 app/lib/placeholder-data.ts。这里我们模拟一些侧边栏菜单数据:
typescript
// app/lib/placeholder-data.ts
// 定义一些初始菜单
export const menus = [
{ id: '550e8400-e29b-41d4-a716-446655440001', parent_id: '0', name: '工作台', type: 'C', path: '/dashboard', perms: '', icon: 'LayoutDashboard' },
{ id: '550e8400-e29b-41d4-a716-446655440002', parent_id: '0', name: '系统管理', type: 'M', path: '/system', perms: '', icon: 'Settings' },
{ id: '550e8400-e29b-41d4-a716-446655440003', parent_id: '550e8400-e29b-41d4-a716-446655440002', name: '人员管理', type: 'C', path: '/dashboard/user', perms: 'system:user:list', icon: 'Users' },
{ id: '550e8400-e29b-41d4-a716-446655440004', parent_id: '550e8400-e29b-41d4-a716-446655440002', name: '角色管理', type: 'C', path: '/dashboard/role', perms: 'system:role:list', icon: 'ShieldCheck' },
];

2.2 编写执行脚本
为了方便初学者,我们使用 Next.js 的 Route Handler 来触发数据库初始化。
新建 app/seed/route.ts:
typescript
// @ts-ignore
import postgres from 'postgres';
import { menus } from '../lib/placeholder-data';
const sql = postgres(process.env.POSTGRES_URL!, { ssl: false });
async function seedMenus() {
// 1. 建表
await sql`
CREATE TABLE IF NOT EXISTS sys_menu (
menu_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
parent_id VARCHAR(255) NOT NULL,
menu_name VARCHAR(255) NOT NULL,
menu_type CHAR(1) NOT NULL,
path VARCHAR(255),
perms VARCHAR(100),
icon VARCHAR(50)
);
`;
// 2. 插入数据
// 注意:真实项目中 parent_id 也应该是 UUID,这里为了演示简化处理
const insertedMenus = await Promise.all(
menus.map((menu) => sql`
INSERT INTO sys_menu (menu_id, parent_id, menu_name, menu_type, path, perms, icon)
VALUES (${menu.id}, ${menu.parent_id}, ${menu.name}, ${menu.type}, ${menu.path}, ${menu.perms}, ${menu.icon})
ON CONFLICT (menu_id) DO NOTHING;
`)
);
return insertedMenus;
}
export async function GET() {
try {
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`; // 启用 UUID 插件
await sql.begin(async (sql: postgres.TransactionSql) => {
await seedMenus();
});
return Response.json({ message: 'Database seeded successfully' });
} catch (error) {
return Response.json({ error }, { status: 500 });
}
}
2.3 执行初始化
- 确保你的
.env文件里配置了正确的POSTGRES_URL。 - 启动项目:
npm run dev。 - 在浏览器访问:
http://localhost:3000/seed。 - 如果你看到
{"message": "Database seeded successfully"},恭喜你,数据库已经就绪!

运行后,提示报错,我们可以将错误贴给kimi让大模型修复一下


安装好依赖的包后再次运行我们的地址就可以看到表创建成功了

3. 核心:Server Components 数据获取
这是 Next.js 最强大的地方:我们不需要写 API 接口(/api/menus),也不需要前端 useEffect 去 fetch。组件直接连数据库!
新建 app/lib/data.ts,专门用于存放 SQL 查询逻辑:
typescript
import postgres from 'postgres';
import { Menu } from './definitions';
const sql = postgres(process.env.POSTGRES_URL!, { ssl: false });
export async function fetchMenus() {
try {
// 模拟延时,让你感受 loading 状态
// await new Promise((resolve) => setTimeout(resolve, 3000));
console.log('Fetching menus...');
// 查询所有目录(M)和菜单(C),排除按钮(F)
const data = await sql<Menu[]>`
SELECT * FROM sys_menu
WHERE menu_type IN ('M', 'C')
ORDER BY menu_id ASC
`;
return data;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch menus.');
}
}

4. 联调:让 Sidebar 动起来
回到 app/ui/dashboard/sidenav.tsx。我们需要做两件事:
- 调用
fetchMenus获取数据。 - 把数据库里的图标字符串(如 "Users")映射回 React 组件。
修改代码如下:
tsx
import Link from 'next/link';
// 1. 引入图标映射工具
import * as Icons from 'lucide-react';
import { fetchMenus } from '@/app/lib/data';
import { GraduationCap, LogOut } from 'lucide-react';
export default async function SideNav() {
// 2. 直接在组件中获取数据 (Server Component 的魔法)
const menuItems = await fetchMenus();
return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
<Link className="..." href="/">
{/* Logo 部分保持不变 */}
<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">
{/* 3. 动态渲染菜单 */}
{menuItems.map((item) => {
// 动态获取图标组件,如果没有则使用默认图标
// @ts-ignore
const IconComponent = Icons[item.icon] || Icons.Circle;
return (
<Link
key={item.menu_id}
href={item.path}
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"
>
<IconComponent className="w-6" />
<p className="hidden md:block">{item.menu_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>
);
}
5. 验证成果
保存所有文件,刷新 http://localhost:3000/dashboard。

你会发现侧边栏的内容已经不再是硬编码的了!我们使用自带的pgAdmin工具,修改一下菜单的名称,刷新页面,变化会立刻反映出来。


你刚刚完成了一个重要的里程碑:
- 你建立了一个真实的数据库连接。
- 你理解了 Next.js 如何在服务端"直接"拿数据。
- 你实现了前端 UI 与后端数据的打通。
下一步预告
现在的页面跳转虽然快,但如果在网速慢的时候,用户点击菜单可能会感觉"卡顿"(因为正在等待数据库查询)。
在 下一篇 中,我们将优化用户体验:
- 使用 Next.js 的 Streaming (流式渲染) 和 Suspense。
- 制作一个漂亮的 Skeleton (骨架屏),在数据加载完成前展示占位动画。
- 顺便,我们将实现 URL 路径高亮(Active State),让用户知道自己当前在哪里。
关注我,一起构建更丝滑的全栈应用!