从0开始搭建后台管理系统-02(Layout布局)(上篇)

本文详细介绍了如何在Vue3项目中使用Layout组件,通过router-view实现页面统一布局,包括headerBar的收缩按钮、面包屑导航、全屏功能、主题切换、用户头像和tagsBar的设计与实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 介绍

后台管理系统通常都是统一的布局结构,通常有:sideBar、headerBar、main,其中:headerBar包括navBar等,sideBar分为左侧和右侧等。简单结构如下图:

如何让所有的页面都会通过Layout结构,展现在mian中呢?
答:我们会在Layout中使用router-view组件。并且在路由中,每个父路由都是指向的Layout组件,再通过重定向来跳转至具体页面,这样可以实现每个路由都必须通过Layout结构。

2. 详解

注:Layout组件的路径为 src/layout

layout

  • components
    • header
      • breadcrumb
      • tagBar
      • themeSwitch
      • userDropdown
  • sideBar
  • index.vue

2.1 headerBar介绍

本框架中haderBar的功能有:

  1. 控制sideBar收缩按钮,v-model绑定isCollapsed控制侧边栏。
  2. breadBrumb面包屑,用于记录当前页面的位置
  3. screenfull全屏按钮,将管理系统全屏展示
  4. themeSwitch主题切换按钮,用于切换封装的主题风格,主题后面单独文章介绍
  5. 用户头像,包含外链的跳转和退出登录等功能
  6. tagsBar用于记录打开过的页面,并实现来回切换、并实现部分页面保活功能。

2.1.1 收缩按钮

在Layout的入口文件index.vue中将两个组件绑定isCollapsed属性来控制sideBar进行收缩。
在header入口中定义一个icon按钮去控制isCollapsed的切换。

// layout>index.vue
<my-side :is-collapse="isCollapsed"/>
<my-header v-model:isCollapsed="isCollapsed"></my-header>


 // header>index.vue
 <template>
  <div class="header-top">
    <el-icon @click="()=>emit('update:isCollapsed',!isCollapsed)" class="header-icon" :size="20">
      <component :is="isCollapsed ?  Expand:Fold"/> // 切换图标
    </el-icon>
    ...
  </div>
</template>

<script setup lang="ts">
  import {Fold, Expand} from '@element-plus/icons-vue'
  defineProps({
  isCollapsed: {
    type: Boolean,
  }
})
const emit = defineEmits(['update:isCollapsed'])
</script>

2.1.2 breadCrumb面包屑

面包屑的功能就是一个路由展示。如:当前在首页的仪表盘页面,那么breadCrumb就是 首页 > 仪表盘
通过elementPlus中的el-breadcrumb,和route中的route.matched的路由记录来实现,如下:

// > header>components>breadcrumb>index.vue
<template>
    <el-breadcrumb :separator-icon="ArrowRight">
      <el-breadcrumb-item class="breadcrumb-item" v-for="item in breadcrumbItems" :to="{ path: item.path }">{{item.meta.title}}</el-breadcrumb-item>
    </el-breadcrumb>
</template>

<script setup lang="ts">
import {ref, watch} from 'vue'
import {useRoute,type RouteLocationMatched} from 'vue-router'
import {ArrowRight} from '@element-plus/icons-vue'

const route = useRoute()

const breadcrumbItems = ref<RouteLocationMatched[]>([])

const refreshBreadcrumbItems = ()=>{
  breadcrumbItems.value = route.matched.filter(r=>r.meta.title)
}

// 监听路由改变
watch(
    ()=>route.path,
    (newPath)=>{
      refreshBreadcrumbItems()
    },{
      immediate:true
    }
)

</script>
<style lang="less" scoped>
.breadcrumb-item{
  :deep(.is-link){
    color: inherit;
  }
}
</style>

2.1.3 screenfull全屏按钮

这个功能是使用了 screenfull 包实现的,我们把他封装在全局组件中使用。
添加一个el-tooltip,这个是elemenPlus中用于提示的一个组件,提高用户体验。

// > src>components>screenfull>index.vue
<template>
  <el-icon @click="trigger" >
    <el-tooltip :content="isFull? '退出全屏' : '全屏'" placement="bottom" effect="dark">
      <svg-icon :name=" isFull? 'closeScreenfull': 'openScreenfull'"/>
    </el-tooltip>
  </el-icon>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {ElMessage } from 'element-plus'
import screenfull from 'screenfull'

const props = defineProps({
  el:{
    type:HTMLElement,
    default:document.documentElement
  },
})

const isFull = ref<boolean>(false)

const trigger = ()=>{
  if(screenfull.isEnabled){ // 判断是否支持全屏
    screenfull.toggle(props.el)
    screenfull.isFullscreen
        ? isFull.value = false
        : isFull.value = true
  }else{
    ElMessage.error('此设备不支持全屏操作!')
  }
}
</script>

2.1.4 theme-switch 主题切换

在一般文档页面、后台管理页面、官网等用户长时间使用的页面中,通常会加上主题切换。可以避免视觉疲劳。主题中最少有:两种风格。
本文章也封装了两套:明亮、黑暗,由于主题也是一大块,这里不做细解。具体实现请看本系列文章(自定义主题详解)

// header > themeSwitch > index.vue
<template>
    <el-dropdown trigger="click" @command="setTheme">
      <el-icon>
        <el-tooltip effect="dark" content="主题" placement="bottom">
          <svg-icon name="palette"/>
        </el-tooltip>
      </el-icon>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item
              v-for="theme in themeList"
              :command="theme"
          >{{theme.title}}</el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
</template>

<script setup lang="ts">
import {useTheme} from '@/hooks/useTheme'
const {initTheme,themeList,setTheme} = useTheme()
initTheme()
</script>

<style lang="less" scoped>
.el-icon{
  height: 100%;
  svg{
    cursor: pointer;
  }
}
.el-dropdown{
  color:inherit;
}
</style>

2.1.5 头像

头像使用的是elementPlus组件-el-dropdown,同时伴有el-dropdown-menu做了一个下拉菜单,用来做登出操作。

// header > userDropdown > index.vue
<template>
    <el-dropdown >
    <span>
      <el-avatar :size="30" :fit="'cover'" :src="circleUrl" />
    </span>
      <template #dropdown>
        <el-dropdown-menu>
          <span @click="toAbout">
          <el-dropdown-item>关于</el-dropdown-item>
            </span>
          <el-dropdown-item>GitHub</el-dropdown-item>
          <span @click="logout">
          <el-dropdown-item >退出登录</el-dropdown-item>
            </span>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {useUserStore} from '@/store/user'
const userStore = useUserStore()

const circleUrl = ref<string>('https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png')
const {logout} = userStore // 删除用户缓存信息,跳转登录页面
</script>

// store > user.ts
...
actions:{
  logout(){
            this.$reset()
            cache.removeCookie(ACCESS_TOKEN_KEY)
            cache.clear()
            resetRoutes()
            router.push({name:'login'})
        }
}
...

2.1.6 tagsBar

tagsBar中功能相对较多。我们需要实现:获取、添加、删除、选择并跳转、固定等功能。
思路:首页的tagBar不能关闭,这样实现会更加简单些,毕竟不可能一个页面都不展示,所以默认是展示首页。其他页面添加进来都会进行一个保存,保存其路由信息。当前的标签还要进行样式区别,增加体验感。
函数解释:
computeRoutes:,正常路由都是父路由嵌套子路由,我们只需要显示具体的子路由。所以需要将路由进行一个扁平化操作,获取全部的子路由。这里使用了递归的思想。

<template>
  <section class="tag-wrapper">
    <el-tag
        v-for="tag in displayTags"
        class="tag"
        :class="{isActive:tag.name===currentTag?.name}"
        effect="plain"
        :closable="tag.name !== defaultTagName"
        @close="closeTag(tag)"
        @click='selectTag(tag)'
    >
      {{ tag?.meta?.title }}
    </el-tag>
  </section>
</template>

<script setup lang="ts">
import {onMounted, ref, watch} from 'vue'
import {RouteRecordRaw, useRoute, useRouter} from "vue-router";
import {useUserStore} from '@/store/user'

const route = useRoute()
const router = useRouter()
const {routes} = useUserStore()

// 默认tag,不能关闭
const defaultTagName = 'dashboard'
// 当前tag
const currentTag = ref<RouteRecordRaw>()
// tagBar中全部tag
const displayTags = ref<RouteRecordRaw[]>([])

/** 获取当前全部可用路由信息 */
const computeRoutes = (targetList: RouteRecordRaw[]) => {
  let result: RouteRecordRaw[] = []
  targetList.forEach(item => {
    if (item.children) {
      const childResult = computeRoutes(item.children) as RouteRecordRaw[]
      if (childResult.length > 0) {
        result = result.concat(childResult)
      }
    }
    result.push(item)
  })
  return result
}
const usableRoutes = computeRoutes(routes)

/** 获取标签 */
const getDisplayTag = (tagName)=>{
  return displayTags.value.find(route=>route.name === tagName)
}
const getUsableTag = (tagName)=>{
  return usableRoutes.find(route=>route.name === tagName)
}


/** 选择标签 */
const selectTag = (tag) => {
  router.push({path:tag.path,replace:true})
}

/** 关闭标签 */
const closeTag = (tag) => {
  const index = displayTags.value.findIndex(item => item.name == tag.name)
  if (index !== -1) {
    displayTags.value.splice(index, 1)
    if (tag.name === currentTag?.value?.name) {
      const chooseIndex = index - 1 >= 0 ? index - 1 : 0
      selectTag(displayTags.value[chooseIndex])
    }
  }
}

/** 初始化tag,保证有个默认的tag存在 */
onMounted(()=>{
  const hasDefault = getDisplayTag(defaultTagName)
  if(!hasDefault){
    const defaultTag = getUsableTag(defaultTagName)
    if(defaultTag){
      displayTags.value.splice(0,0,defaultTag)
    }
  }
})

/** 检测路由 */
watch(
    () => route.name,
    (newVal) => {
      const currentPathName = newVal
      const chosenTag = getDisplayTag(currentPathName)
      if (chosenTag) {
        currentTag.value = chosenTag
      } else {
        const route = getUsableTag(currentPathName)
        if (route && route?.meta?.title) {
          displayTags.value.push(route)
          currentTag.value = route
        }
      }
    }, {
      immediate: true
    }
)


</script>

<style lang="less" scoped>
.tag {
  margin: 5px;
  background-color: @theme-color;
  border: 1px solid @theme-line-color;
  color: @theme-font-color;
  cursor: pointer;

  :deep(.el-icon) {
    color: @theme-font-color;
  }
}

.isActive {
  background-color: @theme-label-active-color;
  color: @theme-label-active-font-color;

  :deep(.el-icon) {
    color: @theme-label-active-font-color;
  }
}
</style>

本篇内容过多,拆分为两篇,下篇讲解sideBar和整体Layout。

3. 本框架其他文章链接

GitHub开源链接:GitHub - grxynl/vue3-admin-template: vue3+TypeScript+pinia 后台管理系统模板

其他文章
从0开始搭建后台管理系统(首篇)
从0开始搭建后台管理系统-01(Login登录)

从0开始搭建后台管理系统-02(Layout布局)(下篇)

4. 结束语

本框架完全免费,框架尚有不足,供前端爱好者一起讨论,一起学习。
源码和文档都制作不易,如果觉得您还可以的话,求一个stars,这是对我最大的支持,也是本框架前进的最大动力。

<think>好的,我现在需要帮助用户解决如何使用Next.js、TypeScript和Ant Design搭建后台管理系统的问题。用户提到了包含Layout布局和测试页面,所以我需要一步步梳理搭建过程。 首先,我应该确认用户的需求:他们希望有一个基于Next.js和TypeScript的后台管理系统,使用Ant Design作为UI库,并且包含布局和测试页面。可能需要从项目初始化开始,逐步指导安装依赖、配置Ant Design,然后创建Layout组件和测试页面。 接下来,我需要回忆Next.js的基本配置步骤。记得Next.js项目可以通过create-next-app快速初始化,但用户可能已经知道这点,所以需要明确说明。同时,TypeScript的集成可能需要安装相应的类型定义文件,比如@types/react和@types/node。 然后是Ant Design的引入。Ant Design在Next.js中使用需要处理样式导入,可能需要配置next.config.js来支持CSS模块或者使用babel插件。另外,用户提到需要安装图标组件包@ant-design/icons-vue,但这里用户可能混淆了Vue和React的版本,因为Next.js通常使用React,所以正确的包应该是@ant-design/icons,需要提醒用户注意这点。 接下来是Layout布局的实现。Ant Design的Layout组件包括Header、Sider、Content、Footer等部分。需要指导用户如何引入这些组件,并设置导航菜单。可能还需要处理路由,确保点击菜单项能跳转到对应的页面,比如测试页面。这里可能需要使用Next.js的路由机制,结合Link组件。 测试页面的创建相对简单,可能只需要一个简单的组件,放置在pages目录下的对应路径中。需要说明如何导出该页面组件,并确保路由正确。 另外,用户提供的引用中有关于安装依赖的命令,比如npm install和npm run dev,这些需要整合到步骤中,确保用户正确安装所有依赖并启动项目。 可能还需要处理Ant Design的按需加载和主题定制,但用户的问题中没有明确提到,所以暂时可以省略,或者作为可选步骤提及。同时,样式文件的全局引入可能需要修改_app.tsx文件,导入Ant Design的CSS文件。 最后,生成相关问题,需要考虑用户可能遇到的后续问题,比如如何添加权限控制、优化性能、处理路由或主题定制等。这些问题应该与搭建后台系统相关,并且基于当前回答内容自然延伸。 现在需要将这些思路整理成结构化的步骤,确保每个部分清晰,代码示例正确,并且符合用户的要求,比如使用中文,正确的LaTeX格式(虽然在这个问题中可能用不上数学公式,但需要检查是否有需要的地方)。同时,引用用户提供的参考引用,但要注意是否正确适用,比如引用[1]和[2]是关于安装依赖和项目启动的,需要正确插入到相关步骤中。</think>### Next.js + TypeScript + Ant Design 后台管理系统搭建指南 #### 一、环境准备 1. 创建项目 ```bash npx create-next-app@latest admin-system --typescript cd admin-system ``` 2. 安装 Ant Design 及图标库(注意使用React版本) ```bash npm install antd @ant-design/icons @ant-design/pro-layout --save npm install --save-dev @types/antd # 安装类型定义[^2] ``` #### 二、配置Ant Design 1. 修改`next.config.js`添加CSS支持 ```javascript /** @type {import('next').NextConfig} */ const nextConfig = { compiler: { styledComponents: true } } module.exports = nextConfig ``` 2. 创建`src/styles/globals.css`导入Ant Design样式 ```css @import '~antd/dist/antd.css'; ``` #### 三、创建基础布局 1. 新建`src/layouts/BasicLayout.tsx` ```tsx import { Layout, Menu } from 'antd'; import { PieChartOutlined, FileOutlined } from '@ant-design/icons'; import Link from 'next/link'; const { Header, Content, Sider } = Layout; export default function BasicLayout({ children }) { return ( <Layout style={{ minHeight: '100vh' }}> <Sider collapsible> <div className="logo" /> <Menu theme="dark" defaultSelectedKeys={['1']} mode="inline"> <Menu.Item key="1" icon={<PieChartOutlined />}> <Link href="/dashboard">仪表盘</Link> </Menu.Item> <Menu.Item key="2" icon={<FileOutlined />}> <Link href="/test">测试页面</Link> </Menu.Item> </Menu> </Sider> <Layout className="site-layout"> <Header style={{ padding: 0, background: '#fff' }} /> <Content style={{ margin: '16px' }}> <div className="site-layout-background" style={{ padding: 24, minHeight: 360 }}> {children} </div> </Content> </Layout> </Layout> ); } ``` #### 四、创建页面 1. 仪表盘页面`pages/dashboard.tsx` ```tsx import BasicLayout from '@/layouts/BasicLayout'; export default function Dashboard() { return ( <BasicLayout> <h1>系统仪表盘</h1> </BasicLayout> ); } ``` 2. 测试页面`pages/test.tsx` ```tsx import { Button } from 'antd'; import BasicLayout from '@/layouts/BasicLayout'; export default function TestPage() { return ( <BasicLayout> <h2>功能测试页面</h2> <Button type="primary">测试按钮</Button> </BasicLayout> ); } ``` #### 五、启动项目 ```bash npm run dev ``` 访问 http://localhost:3000/dashboard 查看效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值