Next.js 实战 (二):搭建 Layouts 基础排版布局

简介: 本文介绍了作者在Next.js v15.x版本发布后,对一个旧项目的重构过程。文章详细说明了项目开发规范配置、UI组件库选择(最终选择了Ant-Design)、以及使用Ant Design的Layout组件实现中后台布局的方法。文末展示了布局的初步效果,并提供了GitHub仓库链接供读者参考学习。

前言

等了许久,Next.js 终于迎来了 v15.x 版本,刚好 Github 上面的旧项目重构完,终于可以放心大胆地去研究 Next.js了。

搭建最新项目可以参考官方文档:Installation

最新的 Next.js 版本,使用的是 React19.x 内测版本,可能存在与其他库不兼容的情况

项目开发规范配置

这块内容我都懒得写了,具体的可以参考我之前写的文章,配置大同小异:

  1. Nuxt3 实战 (二):配置 Eslint、Prettierrc、Husky等项目提交规范
  2. Nuxt3 实战 (三):使用 release-it 自动管理版本号和生成 CHANGELOG

UI 组件库的选择

  1. NextUI:我个人是比较喜欢 NextUI 的,这个库的 UI 设计比较符合我的审美,而且我之前的项目 今日热榜 中用的就是这个,感觉还不错,但我仔细看了下,它缺少了一个很重要的组件:Form表单,这个会给后面频繁的 CURD 表单操作带来麻烦,所以放弃了
  2. Ant-DesignAnt-Design 是我再熟悉不过的组件库了,公司的业务用的就是这个,但这个库还是有点偏业务风格,而且目前和 Next.js 的兼容性还存在点问题,自己也有点审美疲劳了,也放弃了。
  3. shadcn/ui:最终选择了这个,这个库是基于 tailwindcss 的,而且目前在市场上很受欢迎,Github 也保持不错的增长,而且是你想用什么组件,就把组件源码直接放到应用程序中的,值得推荐。

layout 排版布局

我们先搞定最常规的布局,shadcn/ui构建块 中有一些常规的布局,我一下就看重这个:
7u4ntu5iwt68cvxhyvh9ptrb5jsvv36c.png

  1. 左侧是 slibar,菜单顶部可以放 Logo 和标题
  2. 右侧顶部放用户头像和一些操作按钮,比如:面包屑暗黑模式全屏消息通知
  3. 中间就是业务模块,底部放版权信息

业务代码

1、 新增 src/components/AppSideBar/index.tsx 文件:

'use client';

import Image from 'next/image';
import * as React from 'react';

import NavMain from '@/components/NavMain';
import NavUser from '@/components/NavUser';
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
} from '@/components/ui/sidebar';

const data = {
  user: {
    name: '谢明伟',
    email: 'baiwumm@foxmail.com',
    avatar: 'logo.svg',
  },
};

export default function AppSideBar({ ...props }: React.ComponentProps<typeof Sidebar>) {
  return (
    <Sidebar collapsible="icon" {...props}>
      <SidebarHeader>
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton size="lg" asChild>
              <div className="flex items-center gap-2 cursor-pointer">
                <Image src="/logo.svg" width={40} height={40} alt="logo" />
                <span className="truncate font-semibold">{process.env.NEXT_PUBLIC_PROJECT_NAME}</span>
              </div>
            </SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarHeader>
      <SidebarContent>
        <NavMain />
      </SidebarContent>
      <SidebarFooter>
        <NavUser user={data.user} />
      </SidebarFooter>
    </Sidebar>
  );
}

2、 新增 src/components/NavMain/index.tsx 文件:

'use client';

import { map } from 'lodash-es';
import { ChevronRight } from 'lucide-react';
import { usePathname, useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useState } from 'react';

import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import {
  SidebarGroup,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
} from '@/components/ui/sidebar';
import MenuList from '@/constants/MenuList';

export default function NavMain() {
  const t = useTranslations('Route');
  // 路由跳转
  const router = useRouter();
  // 当前激活的菜单
  const pathname = usePathname();
  const [activeKey, setActiveKey] = useState(pathname);

  // 点击菜单回调
  const handleMenuClick = (path: string, redirect = '') => {
    if (redirect) {
      return;
    }
    router.push(path);
    setActiveKey(path);
  };
  return (
    <SidebarGroup>
      <SidebarMenu>
        {map(MenuList, ({ path, icon, name, redirect, children = [] }) => (
          <Collapsible key={path} asChild defaultOpen={activeKey === path} className="group/collapsible">
            <SidebarMenuItem>
              <CollapsibleTrigger asChild>
                <SidebarMenuButton
                  tooltip={t(name)}
                  isActive={activeKey === path}
                  onClick={() => handleMenuClick(path, redirect)}
                >
                  {icon}
                  <span>{t(name)}</span>
                  {children?.length ? (
                    <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
                  ) : null}
                </SidebarMenuButton>
              </CollapsibleTrigger>
              <CollapsibleContent>
                <SidebarMenuSub>
                  {map(children, (subItem) => (
                    <SidebarMenuSubItem key={subItem.path}>
                      <SidebarMenuSubButton asChild onClick={() => handleMenuClick(subItem.path, subItem.redirect)}>
                        <a onClick={() => handleMenuClick(path, redirect)} className="cursor-pointer">
                          {subItem.icon}
                          <span>{t(subItem.name)}</span>
                        </a>
                      </SidebarMenuSubButton>
                    </SidebarMenuSubItem>
                  ))}
                </SidebarMenuSub>
              </CollapsibleContent>
            </SidebarMenuItem>
          </Collapsible>
        ))}
      </SidebarMenu>
    </SidebarGroup>
  );
}

3、新增 src/components/NavUser/index.tsx 文件:

'use client';

import { ChevronsUpDown, IdCard, LogOut } from 'lucide-react';

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar';

export default function NavUser({
  user,
}: {
  user: {
    name: string;
    email: string;
    avatar: string;
  };
}) {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <SidebarMenuButton
              size="lg"
              className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
            >
              <Avatar className="h-8 w-8 rounded-lg">
                <AvatarImage src={`/${user.avatar}`} alt={user.name} />
                <AvatarFallback className="rounded-lg">CN</AvatarFallback>
              </Avatar>
              <div className="grid flex-1 text-left text-sm leading-tight">
                <span className="truncate font-semibold">{user.name}</span>
                <span className="truncate text-xs">{user.email}</span>
              </div>
              <ChevronsUpDown className="ml-auto size-4" />
            </SidebarMenuButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
            side={isMobile ? 'bottom' : 'right'}
            align="end"
            sideOffset={4}
          >
            <DropdownMenuLabel className="p-0 font-normal">
              <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
                <Avatar className="h-8 w-8 rounded-lg">
                  <AvatarImage src={`/${user.avatar}`} alt={user.name} />
                  <AvatarFallback className="rounded-lg">CN</AvatarFallback>
                </Avatar>
                <div className="grid flex-1 text-left text-sm leading-tight">
                  <span className="truncate font-semibold">{user.name}</span>
                  <span className="truncate text-xs">{user.email}</span>
                </div>
              </div>
            </DropdownMenuLabel>
            <DropdownMenuSeparator />
            <DropdownMenuGroup>
              <DropdownMenuItem>
                <IdCard />
                个人中心
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <LogOut />
              退出登录
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

4、新增 src/components/GlobalHeader/index.ts 文件:

'use client';

import { compact, map } from 'lodash-es';
import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Fragment } from 'react';

import LangSwitch from '@/components/LangSwitch';
import ThemeModeButton from '@/components/ThemeModeButton';
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Separator } from '@/components/ui/separator';
import { SidebarTrigger } from '@/components/ui/sidebar';

export default function GlobalHeader() {
  const t = useTranslations('Route');
  const pathname = usePathname();
  const splitPath = compact(pathname.split('/'));
  return (
    <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 border-b justify-between px-4 sticky top-0">
      <div className="flex items-center gap-2">
        <SidebarTrigger className="-ml-1" />
        <Separator orientation="vertical" className="mr-2 h-4" />
        <Breadcrumb>
          <BreadcrumbList>
            {map(splitPath, (path, index) => (
              <Fragment key={path}>
                <BreadcrumbItem>
                  <BreadcrumbPage>{t(path)}</BreadcrumbPage>
                </BreadcrumbItem>
                {index < splitPath.length - 1 ? <BreadcrumbSeparator className="hidden md:block" /> : null}
              </Fragment>
            ))}
          </BreadcrumbList>
        </Breadcrumb>
      </div>
      <div className="flex gap-2">
        <ThemeModeButton />
        <LangSwitch />
      </div>
    </header>
  );
}

5、App/layout.tsx 文件:

import AppSideBar from '@/components/AppSideBar';
import GlobalHeader from '@/components/GlobalHeader';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';

export default async function RootLayout({
children,
}: Readonly<{
    
children: React.ReactNode;
}>) {
return (
  <html suppressHydrationWarning>
    <body>
      <SidebarProvider>
        <AppSideBar />
        <SidebarInset>
          {/* 头部布局 */}
          <GlobalHeader />
          <main className="p-4">{children}</main>
        </SidebarInset>
      </SidebarProvider>
    </body>
  </html>
);
}

最终效果

kkgw92k99fd9kpickuw5kmclr650yq7n.png

万事开头难,后续我们就可以在此基础上新增功能、主题配置等,比如:侧边栏宽度主题色头部是否固定

Github 仓库next-admin

如果你也正在学习 Next.js ,关注我,我也刚起步,与我在互联网中共同进步!

相关文章
|
7天前
|
设计模式 数据安全/隐私保护
Next.js 实战 (七):浅谈 Layout 布局的嵌套设计模式
这篇文章介绍了在Next.js框架下,如何处理中后台管理系统中特殊页面(如登录页)不包裹根布局(RootLayout)的问题。作者指出Next.js的设计理念是通过布局的嵌套来创建复杂的页面结构,这虽然保持了代码的整洁和可维护性,但对于特殊页面来说,却造成了不必要的布局包裹。文章提出了一个解决方案,即通过判断页面的skipGlobalLayout属性来决定是否包含RootLayout,从而实现特殊页面不包裹根布局的目标。
52 33
|
14天前
|
前端开发 API 开发者
Next.js 实战 (五):添加路由 Transition 过渡效果和 Loading 动画
这篇文章介绍了Framer Motion,一个为React设计的动画库,提供了声明式API处理动画和页面转换,适合创建响应式用户界面。文章包括首屏加载动画、路由加载Loading、路由进场和退场动画等主题,并提供了使用Framer Motion和next.js实现这些动画的示例代码。最后,文章总结了这些效果,并邀请读者探讨更好的实现方案。
|
12天前
|
JavaScript 前端开发 API
Next.js 实战 (六):如何实现文件本地上传
这篇文章介绍了在Next.js中如何实现文件上传到本地的方法。文章首先提到Next.js官方文档中没有提供文件上传的实例代码,因此开发者需要自行实现,通常有两种思路:使用Node.js原生上传或使用第三方插件如multer。接着,文章选择了使用Node.js原生上传的方式来讲解实现过程,包括如何通过哈希值命名文件、上传到指定目录以及如何分类文件夹。然后,文章展示了具体的实现步骤,包括编写代码来处理文件上传,并给出了代码示例。最后,文章通过一个效果演示说明了如何通过postman模拟上传文件,并展示了上传后的文件夹结构。
|
1月前
|
存储 网络架构
Next.js 实战 (四):i18n 国际化的最优方案实践
这篇文章介绍了Next.js国际化方案,作者对比了网上常见的方案并提出了自己的需求:不破坏应用程序的目录结构和路由。文章推荐使用next-intl库来实现国际化,并提供了详细的安装步骤和代码示例。作者实现了国际化切换时不改变路由,并把当前语言的key存储到浏览器cookie中,使得刷新浏览器后语言不会失效。最后,文章总结了这种国际化方案的优势,并提供Github仓库链接供读者参考。
|
1月前
Next.js 实战 (三):优雅的实现暗黑主题模式
这篇文章介绍了在Next.js中实现暗黑模式的具体步骤。首先,需要安装next-themes库。然后,在/components/ThemeProvider/index.tsx文件中新增ThemeProvider组件,并在/app/layout.tsx文件中注入该组件。如果想要加入过渡动画,可以修改代码实现主题切换时的动画效果。最后,需要在需要的位置引入ThemeModeButton组件,实现暗黑模式的切换。
|
2月前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
46 2
|
2月前
|
前端开发 JavaScript
JavaScript新纪元:ES6+特性深度解析与实战应用
【10月更文挑战第29天】本文深入解析ES6+的核心特性,包括箭头函数、模板字符串、解构赋值、Promise、模块化和类等,结合实战应用,展示如何利用这些新特性编写更加高效和优雅的代码。
56 0
|
2月前
|
JavaScript 前端开发
JavaScript中的原型 保姆级文章一文搞懂
本文详细解析了JavaScript中的原型概念,从构造函数、原型对象、`__proto__`属性、`constructor`属性到原型链,层层递进地解释了JavaScript如何通过原型实现继承机制。适合初学者深入理解JS面向对象编程的核心原理。
38 1
JavaScript中的原型 保姆级文章一文搞懂
|
6月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
123 2
|
2月前
JS+CSS3文章内容背景黑白切换源码
JS+CSS3文章内容背景黑白切换源码是一款基于JS+CSS3制作的简单网页文章文字内容背景颜色黑白切换效果。
25 0