网站LOGO
白雾茫茫丶
页面加载中
12月22日
网站LOGO 白雾茫茫丶
记录学习、生活和有趣的事
菜单
  • 白雾茫茫丶
    记录学习、生活和有趣的事
    用户的头像
    首次访问
    上次留言
    累计留言
    我的等级
    我的角色
    打赏二维码
    打赏博主
    Next.js 实战 (二):搭建 Layouts 基础排版布局
    点击复制本页信息
    微信扫一扫
    文章二维码
    文章图片 文章标题
    创建时间
  • 一 言
    确认删除此评论么? 确认
  • 本弹窗介绍内容来自,本网站不对其中内容负责。
    • 复制图片
    • 复制图片地址
    • 百度识图
    按住ctrl可打开默认菜单

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

    谢明伟 · 原创 ·
    前端开发Next 实战 · ReactNext
    共 11348 字 · 约 2 分钟 · 141

    前言

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

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

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

    项目开发规范配置

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

    Nuxt3 实战 (二):配置 Eslint、Prettierrc、Husky等项目提交规范

    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构建块中有一些常规的布局,我一下就看重这个:

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

    业务代码

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

      html 代码:
      '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文件:

      html 代码:
      '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文件:

      html 代码:
      '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.tsx文件:

      html 代码:
      '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文件:

      html 代码:
      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>
      );
      }

      最终效果

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

    Github 仓库next-admin

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

    声明:本文由 谢明伟(博主)原创,依据 CC-BY-NC-SA 4.0 许可协议 授权,转载请注明出处。

    还没有人喜爱这篇文章呢

    我要发表评论 我要发表评论
    博客logo 白雾茫茫丶 记录学习、生活和有趣的事 51统计 百度统计
    MOEICP 萌ICP备20236860号 ICP 粤ICP备2023007649号 ICP 粤公网安备44030402006402号

    💻️ 谢明伟 一天前 在线

    🕛

    本站已运行 2 年 356 天 1 小时 52 分

    🌳

    自豪地使用 Typecho 建站,并搭配 MyLife 主题
    白雾茫茫丶. © 2022 ~ 2024.
    网站logo

    白雾茫茫丶 记录学习、生活和有趣的事