vue3实现通用后台管理(傻瓜式一步一步记录代码实现过程)

项目的技术栈展示 以及项目的核心重点部分

在这里插入图片描述

项目效果如下

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

vite的搭建

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

开始做项目

首先我们要安装一些依赖
其中的vue-router和vuex安装最新版的就行,因为项目是vue3
element-plus和less,less-loader最好按照我这个版本来下载

element-plus是一个vue常用的ui组件库 @element-plus/icons-vue是element-plus中的icons组件化的库

yarn add vue-router -D
yarn add vuex -D
yarn add element-plus@2.2.8  -D
yarn add    @element-plus/icons-vue@2.0.6 -D
yarn add less@4.1.3   less-loader@11.1.0   -D

创建路由

在src下创建router文件夹,其中创建index.js

//这个createRouter是用来创建router的,createWebHashHistory则是创建hash模式,如果使用hash模式则会在地址栏带有一个#号
import {createRouter,createWebHashHistory} from "vue-router"
//配置路由
const routes=[
		//这个路由时用于匹配所有不存在的路由,并重新定位到login路由
    {
        path:"/:catchAll(.*)",
        redirect:"/login"
    },
    //首先我们要写登录页面,所以先定义登录的路由
    {
        path:"/login",
        name:'login',
        //这里组件我们使用懒加载的方式引入,组件等会创建
        component:()=>import("../views/login/index.vue")
    }
]
//使用createRouter创建路由器,并返回出去
export default createRouter({
     //history用于设置路由模式
    history:createWebHashHistory(),
     //routes则是路由信息
    routes
})

应用一下路由和其他的依赖

在main.js中,其中有一个css的默认样式,把这个默认引入的样式删除掉!!!!!

//其中有一个css的默认样式,把这个默认引入的样式删除掉!!!!!
import './style.css'

然后大概是下面这个样子

//从vue中引出createApp创建vue实例
import { createApp } from 'vue'
import App from './App.vue'
//引入路由
import router from "./router/index.js"
//引入这个less文件,这个文件在项目的资源中获取,按照路径放置好
import "./assets/less/index.less"
//引入ElementPlusIconsVue 中所有的组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

let app= createApp(App)

//for循环,注册ElementPlusIconsVue 的组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }

  //使用一下路由
  app.use(router)
 //挂载节点
  app.mount('#app')

还有一个ElementPlus,这个依赖我们按照官网自动化导入一下
在这里插入图片描述
先下载两个依赖

yarn add  unplugin-vue-components unplugin-auto-import   -D

在项目目录下的vite.config.js文件中

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
//从依赖中引出这三个
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
//关闭语法校验
  lintOnSave:false,
  //plugins中使用刚下的依赖
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ]
})

编写登录页面

/views/login/index.vue
在src下的views下创建login文件夹,并在login文件夹中创建index.vue

然后在app.vue中,把默认的东西都删除掉,改写成下面这样

<template>
        <!--放置路由出口-->
    <router-view></router-view>
</template>
   
<style>
#app{
  height:100%
}
</style>

打开登录页面
先编写html部分

<template>
      <!--使用elementui中的el-form组件,model绑定的是表单数据对象-->
    <el-form :model="loginForm" class="login-container">
      <h3>系统登录</h3>
      <el-form-item>
        <!--我们用两个输入框,并用 v-model双向绑定,就绑定到loginForm的属性上-->
        <el-input
          type="input"
          placeholder="请输入账号"
          v-model="loginForm.username"
        >
        </el-input>
      </el-form-item>
      <el-form-item>
        <el-input
          type="password"
          placeholder="请输入密码"
          v-model="loginForm.password"
        >
        </el-input>
      </el-form-item>
      <el-form-item>
             <!--点击这个按钮定义触发登录方法-->
        <el-button type="primary" @click="login"> 登录 </el-button>
      </el-form-item>
    </el-form>
  </template>

js部分

  <script setup>
  //getCurrentInstance 用于获取组件实例对象
  import { reactive,getCurrentInstance } from "vue";
  //useRouter 获取路由器对象的方法
  import { useRouter } from "vue-router";
  //loginForm 表单数据对象,使用reactive包裹就可以变成响应式数据
  const loginForm = reactive({
     username: "admin",
     password: "admin",
   });
   //获取路由器对象
  const router = useRouter();
  //获取组件实例对象
  const { proxy } = getCurrentInstance();
  //定义登录方法
  const login = async () => {
      //这里会触发一个请求,并把账号和密码传入进去,这个请求在写完登录页面后定义
      //至于为什么在组件实例上调用,后面我们会把请求挂载在vue的全局对象上,方便调用
    const res = await proxy.$api.getMenu(loginForm);
    
     //然后跳转到home页面中
    router.push({
      path: "/home",
    });
  };


  </script>

css样式

  <style lang="less" scoped>
  .login-container {
    width: 350px;
    background-color: #fff;
    border: 1px solid #eaeaea;
    border-radius: 15px;
    padding: 35px 35px 15px 35px;
    box-shadow: 0 0 25px #cacaca;
    margin: 180px auto;
    h3 {
      text-align: center;
      margin-bottom: 20px;
      color: #505450;
    }
    :deep(.el-form-item__content) {
      justify-content: center;
    }
  }
  </style>

二次封装axios请求,封装请求方法

Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js 环境中发送 HTTP 请求。它是一个流行的第三方库,被广泛用于前端开发中

下面我们先安装一下这个依赖

yarn  add  axios@1.4.0  -D

在src下创建api文件夹,在api中创建request.js

import axios from "axios"
//从element-plus中引入一个提醒
import {ElMessage} from "element-plus"
//定义一个默认的报错信息
const NETWORK_ERROR="网络错误"
//axios.create,创建一个axios实例,可以在里面配置默认信息
let service=axios.create({
	//baseURL前缀,也就是说请求的接口前面都会再加一个api
	//比如请求/user,就会变为/api/user
    baseURL: "/api"
})

//请求前拦截器,请求发送前可以做一些操作,这里我们暂时没有
service.interceptors.request.use((req)=>{

    //需要把请求返回出去
    return req
})


//请求完成后拦截器
service.interceptors.response.use((res)=>{
      //从请求返回的数据中解构出 code(状态码)和msg(后端返回的一些信息)和data数据
    let {code,message,data} =res
     //如果状态码是200,或者请求的状态码是200则把数据返回
    if(code=="200"||res.status==200){
        return data
    }else{
        //如果失败我们使用ElMessage.error发送一个失败的提醒
        ElMessage.error(message||NETWORK_ERROR)
        //并且返回一个失败的promise
        return Promise.reject(message||NETWORK_ERROR)
    }
    
})

//二次封装请求 ,会接收到请求信息
function request(options){
    //如果没有设置默认请求方式为get
    options.method=options.method||"get"
    //如果请求方式为get
    if(options.method.toLowerCase()=="get"){
       //则要为请求信息添加一个params,因为axios中get请求的参数需要用params携带
        options.params=options.query||options.data
    }
    
    //函数的返回值就是,service(也就是axios实例)的返回值,需要把请求信息传进去
    return service(options)
}
//把二次封装的请求方法暴露出去
export default request

在api文件夹下创建api.js,用于封装请求方法

//引入二次封装的请求方法
import request from "./request"

//默认暴露出一个对象,因为我们不止一个请求方法,所以要写在一个对象中
export default{
  //定义登录要发送的请求
  getMenu(params) {
    return request({
      url: '/permission/getMenu',
      method: 'post',
      data: params
    })
  }

}

在main.js中

import api from './api/api'
//我们把暴露的请求方法对象,设置为app.config.globalProperties的一个属性$api(这个可以自己取名),app.config.globalProperties身上设置属性,可以在组件实例上访问,如果不了解在vue官网查阅
 app.config.globalProperties.$api=api

请求我们也写好了,那么后面就是接口的问题了,我们向谁发送这个请求获取数据呢,继续向下看吧

mock模拟后端

mock 是指通过模拟后端接口的数据返回来进行前端开发和测试的技术

我这里就用mock来模拟后端,返回数据了

1.在api文件夹下创建mock.js文件 和一个mockData文件夹(保存mock的数据)

2.定义登录请求的数据,mockData下创建permission.js
打开permission.js

//引入mock,mock不仅可以拦截请求还可以模拟数据
import Mock from 'mockjs'

//返回一个对象,其中的方法会作为mock拦截成功要调用的方法,并把方法返回值作为请求返回值
export default {
  getMenu: config => {
    //方法会接收到请求的参数,从中取出username, password 
    const { username, password } = JSON.parse(config.body)
    
    // 判断账号和密码是否对应
    //这里我们可以通过多个if判断,来添加多个用户,我们这里用两个if判断代表两个用户,每个用户返回的数据都不同,因为后面我们要做一个权限校验,不同用户渲染不同的菜单
    if (username === 'admin' && password === 'admin') {
         //返回一个对象其中有code,data和token(我们使用Mock.Random.guid()来模拟随机的全局唯一标识符),message: '获取成功'
      return {
        code: 200,
        data: {
          menu: [
            {
              path: 'home',
              name: 'home',
              label: '首页',
              icon: 'house',
              url: 'home/index.vue'
            },
            {
              path: 'user',
              name: 'user',
              label: '用户管理',
              icon: 'user',
              url: 'user/index.vue'
            },
            {
              label: '其他',
              icon: 'location',
              children: [
                {
                  path: 'page1',
                  name: 'page1',
                  label: '页面1',
                  icon: 'setting',
                  url: 'page1/index.vue'
                },
                {
                  path: 'page2',
                  name: 'page2',
                  label: '页面2',
                  icon: 'setting',
                  url: 'page2/index.vue'
                }
              ]
            }
          ],
          token: Mock.Random.guid(),
          message: '获取成功'
        }
      }
    } else if (username === 'xiaoxiao' && password === 'xiaoxiao') {
      return {
        code: 200,
        data: {
          menu: [
            {
              path: 'home',
              name: 'home',
              label: '首页',
              icon: 's-home',
              url: 'home/index.vue'
            }
          ],
          token: Mock.Random.guid(),
          message: '获取成功'
        }
      }
    } else {
      return {
        code: -999,
        data: {
        },
       message: '密码错误'
      }
    }

  }
}

3.使用mock拦截请求
打开mock.js文件

import Mock from "mockjs"
//引入获取数据的对象
import permissionApi from "./mockData/permission"

//拦截指定接口,返回一个回调函数的返回值
//第一个参数使用正则的方式匹配拦截请求,第二个是请求方式,第三个是拦截后调用的方法
Mock.mock(/permission\/getMenu/,"post",permissionApi.getMenu)

4.在main.js中引入一下mock

//引入mock,让其生效
import "./api/mock.js"

5.然后我们就可以测试一下登录请求会被拦截

编写main布局页

一般后台管理的布局就是左侧菜单栏,然后右侧的上面有一个导航栏,右侧的下面就是要展示的页面,进入到不同的页面,左侧的菜单和头部导航栏都是不变的,所以我们用一个main页面来做布局

1.首先创建main和首页的路由
在router下的index.js中

const routes=[
  		//在原有的基础上添加这个路由
    {
        path:"/",
        component: ()=>import("../views/main.vue"),
        name:'main',
        redirect:"/home",
        children: [
			{
			        path:"home",
			        component: ()=>import("../views/home/index.vue"),
			        name:'home'
			    }
		]
    }
]

2.创建main和home
src/views/main.vue
src/views/home/index.vue
按照上方的路径创建组件

3.编写main页面

<template>
  <div class="common-layout">
  <!--使用elementui中的el-container布局-->
    <el-container>
      <!--我们会把菜单封装为一个组件comon-aside-->
    <comon-aside></comon-aside>
    
     <el-container>
   <!--头部导航页也是封装一下,注意这个组件需要被el-header包裹-->
    <el-header>
        <comon-header></comon-header>
    </el-header>
    
    <el-main>
    <!--el-main包裹的就是内容区,也就是路由出口-->
        <router-view></router-view>
    </el-main>
  </el-container>
</el-container>

  </div>


</template>

<script setup>
//引入组件
import ComonHeader from "../components/ComonHeader/index.vue"
import ComonAside from "../components/ComonAside/index.vue"


</script>

<style lang="less" scoped>
.el-header{
  padding: 0;
}
.common-layout{
  height: 100%;
  & > .el-container{
  height: 100%;
  & > .el-aside{
  background-color: #545c64;
}
}
}

</style>

4.创建ComonHeader和ComonAside组件
src/components/ComonHeader/index.vue
src/components/ComonAside/index.vue
按照上面的路径创建

封装菜单页

下面我们要封装菜单了,但是我们需要考虑菜单展示的数据从哪里来,

我们之前登录的时候,是不是返回的数据中就包含了menu菜单数据

那这个数据是在登录页面获取的,我们在菜单组件中怎么使用呢

我们会使用vuex来管理菜单数据,这样我们在菜单组件中就可以使用了

1.首先在src下创建store,在其中创建index.js

import {createStore} from "vuex"
//createStore创建store实例
export default createStore({
	  //state则是保存一些数据的
		state:{
		menu:[]
		},
		//mutations则是一些修改state的方法,这里我们定义了一个修改menu的方法,在登录后调用,设置menu的值
		mutations:{
  			setMenu(state, val) {
                state.menu = val
               
              },
        }
		
})

2.在main.js中挂载store

import store from "./store/index.js"
  app.use(store)

3.在login登录页中,引入store,修改login登录方法

  import { useStore } from "vuex";
  const store = useStore();


const login = async () => {
        const res = await proxy.$api.getMenu(loginForm);
        //请求成功后,调用store的setmenu,修改menu的值
        store.commit("setMenu", res.data.menu);
     
        router.push({
          path: "/home",
        });
      };

4.编写菜单页ComonAside
html

<template>
   <!--我们使用el-aside的菜单组件-->
   
   <!--这里我们使用一个store的属性(等会定义)来控制菜单的宽度,因为菜单在导航栏有一个按钮,点击按钮可以折叠或展开菜单-->
  <el-aside :width="$store.state.isCollapse?'64px':'180px'">
   <!--el-menu的collapse表示是否折叠菜单,也是和isCollapse绑定-->
    <el-menu
      class="el-menu-vertical-demo"
      background-color="#545c64"
      :collapse="$store.state.isCollapse"
    >
      <!--如果不折叠则展示后台管理,折叠的话只展示后台-->
      <h3 v-show="!$store.state.isCollapse">后台管理</h3>
      <h3 v-show="$store.state.isCollapse">后台</h3>
	 <!--菜单数据中会有两种情况,一种是有children,一种是没有的,这两种情况需要做不同的展示-->
	  <!--noChildren方法会返回没有children的菜单,cilckmenu表示点击后跳转的方法,等一下定义
-->
      <el-menu-item
        :index="item.name"
        v-for="item in noChildren()"
        :key="item.path"
        @click="cilckmenu(item)"
      >
       <!--使用component展示菜单对应的icon-->
        <component
          class="icons"
          :is="item.icon"
        ></component>
        <!--label 菜单的名称-->
        <span>{{item.label}}</span>
      </el-menu-item>
      
      <!--hasChildren方法会返回有children的菜单,如果没有用过el-menu的el-sub-menu可以到官网了解详细介绍-->
      <el-sub-menu
        v-for="item,index in hasChildren()"
        :index="item.label"
        :key="index"
      >
        <template #title>
          <el-icon>
            <component
            class="icons"
            :is="item.icon"
          ></component>
          </el-icon>
          
          <span>{{item.label}}</span>
        </template>

        <el-menu-item-group>
          <el-menu-item
            :index="subItem.name"
            v-for="subItem,subIndex  in item.children"
            :key="subIndex"
        @click="cilckmenu(subItem)"
          >
            <component
              class="icons"
              :is="subItem.icon"
            ></component> <span>{{subItem.name}}</span>
          </el-menu-item>
        </el-menu-item-group>

      </el-sub-menu>

    </el-menu>
  </el-aside>
</template>

js

<script setup>
import { ref, computed, reactive, watch } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
let router=useRouter()
let store=useStore()

//获取到vuex中保存的menu
let asyncList=store.state.menu

//noChildren 筛选出没有子菜单的菜单
const noChildren = () => {
  return asyncList.filter((item) => !item.children);
};
//hasChildren 筛选出有子菜单的菜单
const hasChildren = () => {
  return asyncList.filter((item) => item.children);
};

//点击菜单触发的方法
function cilckmenu(item){
//点击菜单触发的方法,跳转到菜单对应的路由页面
    router.push({
      path:item.path
    })


}


</script>

css

<style lang='less' scoped>
.icons {
  width: 20px;
  height: 20px;
  margin-right: 5px;

}
.el-menu {
  h3 {
    text-align: center;
    color: white;
    line-height: 36px;
  }
}
.el-menu-vertical-demo {
  border-right: 0;
}
.el-menu-item,.el-sub-menu__title *{
  color: white;
}
</style>

store中

  //分别在state和mutations中添加
 state:{
   //isCollapse默认值是false表示不折叠
  isCollapse:false
},
mutations:{
    //updateIsCollapse修改isCollapse的方法 
		updateIsCollapse(state,value){
		                state.isCollapse=value
		            }
}

封装头部导航栏ComonHeader

html中

<template>
<el-header>
        <!--l-context 是左侧导航栏的内容 -->
  <div class="l-context">
   <!--这个按钮,单击会触发handleriscoll,这个方法中我们会调用store的updateIsCollapse改变菜单的折叠情况 -->
    <el-button @click="handleriscoll">
        <el-icon ><Menu /></el-icon>
        </el-button>
   <!--我们要使用el-breadcrumb  做一个面包屑的效果 separator就是面包屑之间的分割符-->
    <el-breadcrumb separator="/" >
     <!--第一个面包屑默认就是首页,点击后会触发store中的selectMenu方法,这个方法我们等会再说用处-->
    <el-breadcrumb-item :to="{ path: '/home' }" @click=" store.commit('selectMenu',{path: '/home'})">首页</el-breadcrumb-item>
    <!--第二个面包屑就是当前所在的路由,current就是当前所在的路由信息,等会定义,需要用v-if判断如果不存在就不展示-->
    <el-breadcrumb-item v-if="current.label" :to="current.path">{{current.label}}</el-breadcrumb-item>


  </el-breadcrumb>
  
  </div>
 <!--r-context 是右侧导航栏的内容 -->
  <div class="r-context">
     <!--el-dropdown是elementui的下拉框组件 -->
    <el-dropdown>
      <!--这个是正常展示的内容-->
    <span class="el-dropdown-link user">
       <!--getUserImage返回一个图片的路径,传入图片的名字-->
      <img :src="getUserImage('user')" alt="">
    </span>
    
    <!--template #dropdown 定义下拉框的内容-->
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item>个人中心</el-dropdown-item>
        <!--点击退出登录后,触发一个方法-->
        <el-dropdown-item @click="hanlego">退出登录</el-dropdown-item>
       
      </el-dropdown-menu>
    </template>
  </el-dropdown>
  </div>
</el-header>
</template>

js

<script setup>
import {useStore} from 'vuex'
import {computed} from 'vue'
import { useRouter } from "vue-router";
let router=useRouter()
let store=useStore()
//这个属性就是当前路由页面的信息,从store的currentMenu上获取,等会定义
let current=computed(()=>{
 return  store.state.currentMenu||{}
})

//getUserImage返回一个图片的信息,这个图片是保存在src下的assets中,在资源中获取
let getUserImage=( user)=>{
  return new URL(`../../assets/img/${user}.png`,import.meta.url).href
}
//handleriscoll 就是触发store的updateIsCollapse方法,传入取反的isCollapse,改变菜单的折叠状态
function handleriscoll(){
  store.commit("updateIsCollapse",!store.state.isCollapse)
}

function hanlego(){
 //退出登录的时候,跳转到login页面
  router.push({
    path:"/login"
  })

}
</script>

css

<style lang="less" scoped>

header{
  display: flex;
  justify-content: space-between;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    background-color: #333;
    .el-breadcrumb{
      /deep/ span {
        color: #fff !important;
    }
    
  }
}
.l-context{
  margin-left: 20px;
  display: flex;
  align-items: center;
  .el-button{
    margin-right: 10px;
  }
  h3{
    color: white;
  }
}
.r-context{
 .user img{
  width: 50px;
  height: 50px;
  border-radius: 50%;

 }
}
</style>

store中

   state:{
    //定义这个当前路由信息
	currentMenu:null
	}
  mutations:{
   //selectMenu改变selectMenu方法,传入一个路由信息
  selectMenu(state,value){
              //如果传入的路由信息是到home页面,则把currentMenu置为空,不是的话就把传入的路由信息赋值为currentMenu
                if(value.path=="/home"||value.path=="home"){
                    state.currentMenu=null
                }else{
                    state.currentMenu=value
                    
                }
            }
 }

编写首页

之前在main下定义路由了
打开views下home中的index.vue
html

<template>
     <!--页面采用el-row和el-col布局-->
  <el-row
    class="home"
    :gutter="20"
  >
  <!--这一个el-col是第一列的内容-->
    <el-col
      :span="6"
      style="margin-top:20px"
    >
      <!--第一列有两个卡片,这个是第一个-->
      <el-card shadow="hover">
        <div class="user">
         <!--img的图片在资源中-->
          <img
            src="../../assets/img/user.png"
            alt=""
          >
          <div class="userinfo">
            <p class="name">Admin</p>
            <p class="role">超级管理员</p>
          </div>
        </div>

        <div class="login-info">
          <p>上次登录时间:<span>2022-7-11</span></p>
          <p>上次登录地点:<span>北京</span> </p>
        </div>
      </el-card>
			<!--第二个卡片中有一个表格-->
      <el-card
        style="margin-top:20px"
        shadow="hover"
        height="500px"
      >
         <!--表格的数据和列的数据,等下定义-->
        <el-table :data="tableData">
          <el-table-column
            v-for="item,key in tableLabel"
            :key="item"
            :prop="key"
            :label="item"
          >
          </el-table-column>
        </el-table>
      </el-card>
    </el-col>

     <!--第二列-->
    <el-col
      :span="18"
      style="margin-top: 20px"
      class="main"
    >
         <!--订单销售情况-->
      <div class="num">
       <!--会有多个el-card,countData等下定义-->
        <el-card
          :body-style="{display:'flex',padding:0}"
          v-for="item in countData"
          :key="item.name"
        >
          <component
            class="icons"
            :is="item.icon"
            :style="{'background-color':item.color}"
          ></component>
          <div class="details">
            <p class="num">¥{{item.value }}</p>
            <p class="txt">{{item.name }}</p>
          </div>
        </el-card>
      </div>
	 <!--下面是图表数据,每一个el-card都代表一个图表-->
      <el-card style="height:280px">
        <div
          ref="echart"
          style="height: 280px;;"
        >
        </div>
      </el-card>
      
      <div class="graph">
          <el-card style="height: 260px">
            <div
              ref="userechart"
              style="height: 240px"
            ></div>
          </el-card>
          <el-card style="height: 260px">
            <div
              ref="videoechart"
              style="height: 240px"
            ></div>
          </el-card>
        </div>
    </el-col>
  </el-row>

</template>

1.其中的数据有三个是需要请求的,这里我们先写一下mock接口数据
在api下的mockData中,创建home.js
返回的对象中有三个方法,分别是保存的table,销售情况,和图表信息的数据

export default {
    getTableData:()=>{
       return {
        tableData: [
            {
              name: "oppo",
              todayBuy: 500,
              monthBuy: 3500,
              totalBuy: 22000,
            },
            {
              name: "vivo",
              todayBuy: 300,
              monthBuy: 2200,
              totalBuy: 24000,
            },
            {
              name: "苹果",
              todayBuy: 800,
              monthBuy: 4500,
              totalBuy: 65000,
            },
            {
              name: "小米",
              todayBuy: 1200,
              monthBuy: 6500,
              totalBuy: 45000,
            },
            {
              name: "三星",
              todayBuy: 300,
              monthBuy: 2000,
              totalBuy: 34000,
            },
            {
              name: "魅族",
              todayBuy: 350,
              monthBuy: 3000,
              totalBuy: 22000,
            },
          ]
       
    
       } 
    },
    getCountData:()=>{
      return {
       countData: [
        {
          "name": "今日支付订单",
          "value": 1234,
          "icon": "SuccessFilled",
          "color": "#2ec7c9"
        },
        {
          "name": "今日收藏订单",
          "value": 210,
          "icon": "StarFilled",
          "color": "#ffb980"
        },
        {
          "name": "今日未支付订单",
          "value": 1234,
          "icon": "GoodsFilled",
          "color": "#5ab1ef"
        },
        {
          "name": "本月支付订单",
          "value": 1234,
          "icon": "SuccessFilled",
          "color": "#2ec7c9"
        },
        {
          "name": "本月收藏订单",
          "value": 210,
          "icon": "StarFilled",
          "color": "#ffb980"
        },
        {
          "name": "本月未支付订单",
          "value": 1234,
          "icon": "GoodsFilled",
          "color": "#5ab1ef"
        }
      ]
      
   
      } 
   },
   getEchartsData:()=>{
    return {
      "orderData": {
        "date": [
          "20191001",
          "20191002",
          "20191003",
          "20191004",
          "20191005",
          "20191006",
          "20191007"
        ],
        "data": [
          {
            "苹果": 2112,
            "小米": 1809,
            "华为": 2110,
            "oppo": 1129,
            "vivo": 3233,
            "一加": 3871
          },
          {
            "苹果": 1969,
            "小米": 3035,
            "华为": 4204,
            "oppo": 3779,
            "vivo": 3282,
            "一加": 4800
          },
          {
            "苹果": 1649,
            "小米": 3300,
            "华为": 2176,
            "oppo": 4141,
            "vivo": 1699,
            "一加": 3579
          },
          {
            "苹果": 4966,
            "小米": 2862,
            "华为": 4963,
            "oppo": 4897,
            "vivo": 1102,
            "一加": 3671
          },
          {
            "苹果": 2598,
            "小米": 3852,
            "华为": 2320,
            "oppo": 2413,
            "vivo": 3673,
            "一加": 4100
          },
          {
            "苹果": 1581,
            "小米": 3975,
            "华为": 4405,
            "oppo": 3379,
            "vivo": 1843,
            "一加": 4288
          },
          {
            "苹果": 3581,
            "小米": 4725,
            "华为": 2224,
            "oppo": 4463,
            "vivo": 4339,
            "一加": 1640
          }
        ]
      },
      "videoData": [
        {
          "name": "小米",
          "value": 2999
        },
        {
          "name": "苹果",
          "value": 5999
        },
        {
          "name": "vivo",
          "value": 1500
        },
        {
          "name": "oppo",
          "value": 1999
        },
        {
          "name": "魅族",
          "value": 2200
        },
        {
          "name": "三星",
          "value": 4500
        }
      ],
      "userData": [
        {
          "date": "周一",
          "new": 5,
          "active": 200
        },
        {
          "date": "周二",
          "new": 10,
          "active": 500
        },
        {
          "date": "周三",
          "new": 12,
          "active": 550
        },
        {
          "date": "周四",
          "new": 60,
          "active": 800
        },
        {
          "date": "周五",
          "new": 65,
          "active": 550
        },
        {
          "date": "周六",
          "new": 53,
          "active": 770
        },
        {
          "date": "周日",
          "new": 33,
          "active": 170
        }
      ]
    } 
 }

}

2.在api下的mock.js中

import homeApi from "./mockData/home"
//拦截指定接口,返回一个回调函数的返回值
Mock.mock(/home\/getTableData/,homeApi.getTableData)
Mock.mock(/home\/getCountData/,homeApi.getCountData)
Mock.mock(/home\/getEchartsData/,homeApi.getEchartsData)

4.定义请求方法
在api下的api.js中

//在原来暴露出的对象中添加
 getTableData(params){
    return request({
        url:"/home/getTableData",
        method:"get",
        data:params,
    })
   },
   getCountData(params){
    return request({
        url:"/home/getCountData",
        method:"get",
        data:params,
    })
   },
   getEchartsData(params){
    return request({
        url:"/home/getEchartsData",
        method:"get",
        data:params,
    })
   } ,

3.下载echarts
echarts是基于JavaScript的数据可视化库,用于创建丰富、交互式的图表和数据展示。它支持包括折线图、柱状图、饼图、散点图、地图等多种常见图表类型,并提供了丰富的配置项和交互功能,使得用户可以轻松地定制各种样式的图表。
我们用echarts实现图表

yarn add echarts@5.4.2

4.编写home的js部分

<script setup>
import {
  ref,
  computed,
  reactive,
  watch,
  getCurrentInstance,
  onMounted,
} from "vue";
//引入echarts
import * as echarts from "echarts";
//获取组件实例对象
let { proxy } = getCurrentInstance();
//表格列的数据
const tableLabel = {
  name: "课程",
  todayBuy: "今日购买",
  monthBuy: "本月购买",
  totalBuy: "总购买",
};
//表格的数据和销售的数据,等会请求接口在设置实际数据
let tableData = reactive([]);
let countData = reactive([]);

//下面都是图表的一些配置
let xOptions = reactive({
  // 图例文字颜色
  textStyle: {
    color: "#333",
  },
  grid: {
    left: "20%",
  },
  // 提示框
  tooltip: {
    trigger: "axis",
  },
  xAxis: {
    type: "category", // 类目轴
    data: [],
    axisLine: {
      lineStyle: {
        color: "#17b3a3",
      },
    },
    axisLabel: {
      interval: 0,
      color: "#333",
    },
  },
  yAxis: [
    {
      type: "value",
      axisLine: {
        lineStyle: {
          color: "#17b3a3",
        },
      },
    },
  ],
  color: ["#2ec7c9", "#b6a2de", "#5ab1ef", "#ffb980", "#d87a80", "#8d98b3"],
  series: [],
});
let pieOptions = reactive({
      tooltip: {
        trigger: "item",
      },
      color: [
        "#0f78f4",
        "#dd536b",
        "#9462e5",
        "#a6a6a6",
        "#e1bb22",
        "#39c362",
        "#3ed1cf",
      ],
      series: [],
    });
let orderData = reactive({
  xData: [],
  series: [],
});
let userData = reactive({
  xData: [],
  series: [],
});
let videoData = reactive({
  series: [],
});

//请求getTableData 接口并赋值
const getTableData = async () => {
  let res = await proxy.$api.getTableData();
  tableData.push(...res.tableData);
};
//请求getCountData  接口并赋值
const getCountData = async () => {
  let res = await proxy.$api.getCountData();
  countData.push(...res.countData);
};
//请求getEchartsData  接口并渲染图表
const getEchartsData = async () => {
  let result = await proxy.$api.getEchartsData();
  let res = result.orderData;
  let userRes = result.userData;
  let videoRes = result.videoData;
  orderData.xData = res.date;
  const keyArray = Object.keys(res.data[0]);
  const series = [];
  keyArray.forEach((key) => {
    series.push({
      name: key,
      data: res.data.map((item) => item[key]),
      type: "line",
    });
  });
  orderData.series = series;
  xOptions.xAxis.data = orderData.xData;
  xOptions.series = orderData.series;
  // userData进行渲染
  let hEcharts = echarts.init(proxy.$refs["echart"]);
  hEcharts.setOption(xOptions);

  // 柱状图进行渲染的过程
  userData.xData = userRes.map((item) => item.date);
  userData.series = [
    {
      name: "新增用户",
      data: userRes.map((item) => item.new),
      type: "bar",
    },
    {
      name: "活跃用户",
      data: userRes.map((item) => item.active),
      type: "bar",
    },
  ];

  xOptions.xAxis.data = userData.xData;
  xOptions.series = userData.series;
  let uEcharts = echarts.init(proxy.$refs["userechart"]);
  uEcharts.setOption(xOptions);
  videoData.series = [
    {
      data: videoRes,
      type: "pie",
    },
  ];
  pieOptions.series = videoData.series;
  let vEcharts = echarts.init(proxy.$refs["videoechart"]);
  vEcharts.setOption(pieOptions);
};
//在mounted中执行这三个方法
onMounted(() => {
  getTableData();
  getCountData();
  getEchartsData();
});
</script>

css

<style lang='less' scoped>
.home {
  .user {
    display: flex;
    align-items: center;
    padding-bottom: 20px;
    border-bottom: 1px solid #ccc;
    img {
      width: 150px;
      height: 150px;
      border-radius: 50%;
      margin-right: 40px;
    }
    .userinfo {
      line-height: 30px;
    }
  }
  .login-info {
    margin-top: 10px;
    line-height: 30px;
    font-size: 14px;
    color: #999;
    span {
      color: #666;
      margin-left: 70px;
    }
  }
}

.main{
    .num {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  .el-card {
    width: 32%;
    margin-bottom: 20px;
    border-radius: 5px;
  }
  .details {
    margin-left: 10px;
    .num {
      font-size: 30px;
      line-height: 50px;
    }
    .txt {
      font-size: 12px;
      color: #999;
    }
  }
 
  .icons {
    width: 80px;
    height: 80px;
    color: #fff;
    text-align: center;
  }
}
.graph{
    margin-top: 20px;
    display: flex;
    justify-content: space-between;
    .el-card{
        width: 48%;
    }
    
  }
}
</style>

编写用户管理页

1.创建路由,他和home一样都是main的子页面

const routes=[
  		
    {
        path:"/",
        component: ()=>import("../views/main.vue"),
        name:'main',
        redirect:"/home",
        children: [
			{
			        path:"home",
			        component: ()=>import("../views/home/index.vue"),
			        name:'home'
			    },
		    //在原有的基础上添加这个路由
		    {
			        path:"user",
			        component: ()=>import("../views/user/index.vue"),
			        name:'user'
			    }
		]
    }
]

2.根据路由创建文件
src/views/user/index.vue
3.编写页面
html

<template>
   <!--整体分为三个部分-->
   <!--user-header  头部的搜索框-->
  <div class="user-header">
      <!--handleEa方法是新增数据,传入add表示新增-->
    <el-button
      type="primary"
      @click="handleEa('add')"
    >+新增</el-button>
    <!--form表单的model保存搜索的信息-->
    <el-form
      :inline="true"
      :model="formInline"
    >
      <el-form-item label="请输入">
        <el-input
          v-model="formInline.keyword"
          placeholder="请输入用户名"
        />
      </el-form-item>
      <el-form-item>
       <!--handleSerch搜索方法-->
        <el-button
          type="primary"
          @click="handleSerch"
        >搜索</el-button>
      </el-form-item>
    </el-form>
  </div>
   <!--表格部分内容-->
  <div class="table">
   <!--table的 data和列的信息都是等会定义-->
    <el-table
      :data="list"
      style="width: 100%"
      height="500px"
    >
      <el-table-column
        v-for="item in tableLabel"
        :key="item.label"
        :prop="item.prop"
        :label="item.label"
        :width="item.width?item.width:125"
      >
      </el-table-column>

      <el-table-column
        label="操作"
        fixed="right"
        min-width="180"
      >
       <!--这一列我们使用插槽自定义数据,还可以获取到行和列的信息-->
        <template #default="scope">
         <!--其中定义编辑和删除按钮,传入行和列的信息,给对应的方法进行处理-->
         <!--编辑方法的第一个参数需要是"edit",因为他和新增公用一个方法,需要参数来区分-->
          <el-button size="small"
      @click="handleEa('edit',scope)"
          >编辑</el-button>
          <el-button
            size="small"
            type="danger"
      @click="deleteUser(scope)"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>
      <!--分页器,total表示总条数,默认一页10条,@current-change是页数发生改变时触发-->
    <el-pagination
      background
      small
      layout="prev, pager, next"
      :total="config.total"
      @current-change="changePage"
    />
  </div>

<!--el-dialog是一个弹出窗,在里面我们可以新增或编辑用户-->
<!--v-model是否显示,handleClose关闭触发的方法,title显示的标题-->
  <el-dialog
    v-model="dialogVisible"
    :before-close="handleClose"
    :title="action=='add'?'新增用户':'编辑用户'"
    width="50%"
  >
  <!--ref是获取组件实例的关键-->
    <el-form
      :model="formUser"
      label-width="60px"
      ref="userFrom"
    >
      <el-row>
        <el-col :span="12">
              <!--el-form-item上必须要有一个prop其中的值就是内部表单的v-model绑定的属性,比如formUser.name,那这个prop的值就是name-->
          <el-form-item label="姓名" prop="name" :rules="[{ required: true, message: '姓名是必填项' }]">
            <el-input
              placeholder="请输入姓名"
              v-model="formUser.name"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="年龄" prop="age"  :rules="[{ required: true, message: '年龄是必填项' },
        { type:'number', message: '请输入数字' }]">
            <el-input
              placeholder="请输入年龄"
              v-model.number="formUser.age"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="性别" prop="sex" :rules="[{ required: true, message: '性别是必选项' }]">
            <el-select 
              v-model="formUser.sex"
              class="m-2"
              placeholder="请选择"
              size="large"
            >
              <el-option 
                label=""
                value="1"
              />
              <el-option
                label=""
                value="0"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="时间" prop="birth" :rules="[{ required: true, message: '时间是必填项' }]">
            <el-date-picker
              v-model="formUser.birth"
              type="date"
              label="Pick a date"
              placeholder="请选择"
              style="width: 100%"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-form-item label="地址" prop="addr" :rules="[{ required: true, message: '地址是必填项' }]">
        <el-input
              placeholder="请输入地址"
              v-model="formUser.addr"
            />
          </el-form-item>
      </el-row>
      <el-row style="justify-content: right;">
        <el-form-item >
         <!--submitForm点击提交触发的方法-->
        <el-button type="primary"  @click="submitForm(formUser)">
        提交
      </el-button>
    </el-form-item> 
    <el-form-item >
    	<!--handleclose点击取消触发的方法-->
        <el-button type="primary"  @click="handleclose">
        取消
      </el-button>
    </el-form-item>
      </el-row>
    
      
    </el-form>
    
  </el-dialog>

</template>

4.创建接口
在api下的mockData中创建 user.js

import Mock from 'mockjs'

//param2Obj处理请求的参数,传入参数的列表, get请求从config.url获取参数,post从config.body中获取参数
function param2Obj(url) {
  const search = url.split('?')[1]
  if (!search) {
    return {}
  }
  return JSON.parse(
    '{"' +
    decodeURIComponent(search)
      .replace(/"/g, '\\"')
      .replace(/&/g, '","')
      .replace(/=/g, '":"') +
    '"}'
  )
}

//定义用户数据的个数为200个
let List = []
const count = 200
//for循环遍历200次,使用mock模拟每一个字段的数据
for (let i = 0; i < count; i++) {
  List.push(
    Mock.mock({
      id: Mock.Random.guid(),
      name: Mock.Random.cname(),
      addr: Mock.mock('@county(true)'),
      'age|18-60': 1,
      birth: Mock.Random.date(),
      sex: Mock.Random.integer(0, 1)
    })
  )
}

export default {
  /**
   * 获取列表
   * 要带参数 name, page, limt; name可以不填, page,limit有默认值。
   * @param name, page, limit
   * @return {{code: number, count: number, data: *[]}}
   */
   //getUserList获取用户列表,也是搜索方法
  getUserList: config => {
    const { name, page = 1, limit = 10 } = param2Obj(config.url)
    //这个是在筛选数据,也就是当传入参数name的值不为空,以这个name为条件筛选数据
    const mockList = List.filter(user => {
      if (name && user.name.indexOf(name) === -1 && user.addr.indexOf(name) === -1) return false
      return true
    })
     //这里是在根据传入的page和limit做分页处理
    const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
    return {
      code: 200,
      data: {
        list: pageList,
        count: mockList.length,
      }
    }
  },
  /**
   * 增加用户
   * @param name, addr, age, birth, sex
   * @return {{code: number, data: {message: string}}}
   */
   //创建用户方法
  createUser: config => {
    const { name, addr, age, birth, sex } = JSON.parse(config.body)
    List.unshift({
      id: Mock.Random.guid(),
      name: name,
      addr: addr,
      age: age,
      birth: birth,
      sex: sex
    })
    return {
      code: 200,
      data: {
        message: '添加成功'
      }
    }
  },
  /**0
   * 删除用户
   * @param id
   * @return {*}
   */
   //删除用户方法,需要传入id
  deleteUser: config => {
    const { id } = param2Obj(config.url)
    if (!id) {
      return {
        code: -999,
        message: '参数不正确'
      }
    } else {
      List = List.filter(u => u.id !== id)
      return {
        code: 200,
        message: '删除成功'
      }
    }
  },
  /**
   * 修改用户
   * @param id, name, addr, age, birth, sex
   * @return {{code: number, data: {message: string}}}
   */
   //修改数据方法
  updateUser: config => {
    const { id, name, addr, age, birth, sex } = JSON.parse(config.body)
    const sex_num = parseInt(sex)
    List.some(u => {
      if (u.id === id) {
        u.name = name
        u.addr = addr
        u.age = age
        u.birth = birth
        u.sex = sex_num
        return true
      }
    })
    return {
      code: 200,
      data: {
        message: '编辑成功'
      }
    }
  }
}

5.mock拦截
在api下的mock.js中

 import userApi from "./mockData/user"
Mock.mock(/user\/getUserData/,userApi.getUserList)
Mock.mock(/user\/addUser/,"post",userApi.createUser)
Mock.mock(/user\/updateUser/,"post",userApi.updateUser)
Mock.mock(/user\/deleteUser/,"post",userApi.deleteUser)

6.请求方法

 getUserData(params){
    return request({
        url:"/user/getUserData",
        method:"get",
        data:params,
    })
   } ,
  addUser(params){
    return request({
        url:"/user/addUser",
        method:"post",
        data:params,
    })
   } ,
   updateUser(params){
    return request({
        url:"/user/updateUser",
        method:"post",
        data:params,
    })
   } ,
   deleteUser(params){
    return request({
        url:"/user/deleteUser",
        method:"post",
        data:params,
    })
   },

7.编写user的js部分

<script setup>
import {
  ref,
  computed,
  reactive,
  watch,
  getCurrentInstance,
  onMounted,
} from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
let { proxy } = getCurrentInstance();
//list,table数据
let list = reactive([]);
//这个是分页器要用的,和请求用户列表的对象
let config = reactive({
  total: 0,
  page: 1,
  name: "",
});
//changePage ,当分页器页数发生改变后触发,接收到修改后的页数
let changePage = (page) => {
  //getUserData是请求use数据的方法,下面定义,把config中的page改变,并传入getUserData中
  config.page = page;
  getUserData(config);
};
//tableLabel,table的列数据
const tableLabel = reactive([
  {
    prop: "name",
    label: "姓名",
  },
  {
    prop: "age",
    label: "年龄",
  },
  {
    prop: "sexLabel",
    label: "性别",
  },
  {
    prop: "birth",
    label: "出生日期",
    width: 200,
  },
  {
    prop: "addr",
    label: "地址",
    width: 320,
  },
]);
//getUserData 获取user列表
let getUserData = async (config) => {
  let res = await proxy.$api.getUserData(config);
  //获取到数据后,把总条数赋值为config的total
  config.total = res.data.count;
  //先把list清空
  list.splice(0, list.length);
  //格式化一下数据,接口的sex值是0或1
  list.push(
    ...res.data.list.map((item) => {
      item.sexLabel = item.sex == "0" ? "女" : "男";
      return item;
    })
  );
};
//搜索from的数据
const formInline = reactive({
  keyword: "",
});
//点击搜索时,把搜索from的数据传递给config.name,然后调用getUserData(config)
const handleSerch = () => {
  config.name = formInline.keyword;
  getUserData(config);
};
//弹出窗是否显示,默认不显示
let dialogVisible = ref(false);
//格式化时间的方法,添加数据和修改会有一个时间的控件选择
const timeFormat = (time) => {
      var time = new Date(time);
      var year = time.getFullYear();
      var month = time.getMonth() + 1;
      var date = time.getDate();
      function add(m) {
        return m < 10 ? "0" + m : m;
      }
      return year + "-" + add(month) + "-" + add(date);
    };
//formUser 是弹出窗中的form表单的数据
let formUser = reactive({});
//弹出窗关闭的方法
const handleClose = (done) => {
  ElMessageBox.confirm("确认关闭吗")
    .then(() => {
    //确认关闭后,需要重置表单的数据
       proxy.$refs.userFrom.resetFields()
      done();
    })
    .catch(() => {
      // catch error
    });
};
 //action是add和是edit,默认是add,表示新增
let action=ref("add")
//handleEa,当点击新增或者是编辑时触发
let handleEa=(item,{row}={})=>{
  //首先就是判断第一个参数,更改action 的值
  item=="add"?action.value="add":action.value="edit"
  //然后把弹出窗显示出来
  dialogVisible.value=true
  //然后把弹出窗显示出来,如果是编辑则把row中的数据传递给formUser
  if( item=="edit"){
  //先格式化sex字段
  row.sex=='0'?row.sex='女':row.sex='男'
  //这个给对象赋值的操作要放在$nextTick方法中,以免数据的初始化会出现问题
  proxy.$nextTick(()=>{
    Object.assign(formUser,row)

  })
  }

}
 //submitForm提交的方法
let submitForm=(fromUser)=>{
 //先验证表单数据,是否符合规则
  proxy.$refs.userFrom.validate(async(flag)=>{
      //flag为true表示验证成功
    if(flag){
      //先把出生日期格式化一下
      formUser.birth = timeFormat(formUser.birth);
      let res;
       //判断action的值,可以知道是add的提交还是编辑用户的提交
      if(action.value=="add"){
       //如果是add则触发addUser请求
      res=await proxy.$api.addUser(fromUser)
      }else{
      //如果是edit则触发updateUser方法
      res=await proxy.$api.updateUser(fromUser)
      }
      //如果返回的数据的code为200,表示请求成功
      if(res.code==200){
      //我们需要重新获取用户列表,把弹出窗隐藏,然后清空表单
        getUserData(config);
      dialogVisible.value=false
      proxy.$refs.userFrom.resetFields()
      }
      }else{
        ElMessage({
          showClose:true,
          message:"请输入完整信息",
          type:'error'
        })
      }

  }
  )
   
}

//这个是点击取消触发的方法
let handleclose=()=>{
  dialogVisible.value=false
  proxy.$refs.userFrom.resetFields()
}



//deleteUser点击删除用户触发的方法
let deleteUser= async({row})=>{
  ElMessageBox.confirm("确认删除吗吗")
    .then(async() => {
     //调用deleteUser需要传入当前行的id
    let res=  await proxy.$api.deleteUser({id:row.id})
    if(res.code==200){
      ElMessage({
          showClose:true,
          message:"删除成功",
          type:"success"
        })
         //成功后重新获取数据
      getUserData(config);
    }
    })
    .catch(() => {
      // catch error
    });
}

onMounted(() => {
     //在Mounted时执行一下getUserData
  getUserData(config);
});
</script>

css

<style lang='less' scoped>
.table {
  height: 550px;
  position: relative;
  .el-pagination {
    position: absolute;
    right: 0;
    bottom: 0;
  }
}
.user-header {
  display: flex;
  justify-content: space-between;
}
</style>

实现Tag 标签

在这里插入图片描述
这是每一个页面都可以显示的,所以需要放到main页面中
1.打开views下的main.vue,在原来的基础上修改

<template>
  <div class="common-layout">
    <el-container>
    <comon-aside></comon-aside>
     <el-container>
    <el-header>
        <comon-header></comon-header>
    </el-header>
    
    <el-main>
    <!--把它封装成一个组件,并且在el-main中使用-->
        <ComonTab></ComonTab>
        <router-view></router-view>
    </el-main>
  </el-container>
</el-container>

  </div>


</template>

<script setup>
import ComonHeader from "../components/ComonHeader/index.vue"
import ComonAside from "../components/ComonAside/index.vue"
//引入组件
import ComonTab from "../components/ComonTab/index.vue"

</script>

2.创建组件
src/components/ComonTab/index.vue
3.编写ComonTab下的index.vue

<template>
  <div class="tags">
   <!--使用el-tag,for循环遍历 tagList(也是一个保存路由信息的数组)在下面定义-->
    <!--closable表示是否可以移除,如果这个页面的name是home就不可以移除-->
    <!--effect表示颜色,如果当前路由和item的name一致,那么表示当前显示的页面就是这个tag对应的,让他高亮-->
    <el-tag
      v-for="(item, index) in tagList"
      :key="item.name"
      :closable="item.name != 'home'"
      :disable-transitions="false"
      :effect="route.name == item.name ? 'dark' : 'plain'"
      @click="changeMenu(item)"
      @close="deleteMenu(item, index)"
    >
    <!--click和close方法,就是点击和关闭时的方法,需要传递item进去-->
      {{ item.name }}
    </el-tag>
  </div>
</template>

<script setup>
import { useStore } from "vuex";
import { useRoute, useRouter } from "vue-router";
import { computed, ref } from "vue";
let store = useStore();
let route = useRoute();
let router = useRouter();
//tagList 我们选择保存在vux中,因为需要多个组件联调
let tagList = store.state.tabList;
//当点击时触发changeMenu方法
let changeMenu = (item) => {
  //会触发一个store的selectMenu(之前定义过)方法,把item传进去,也就是tag对应的路由信息
  store.commit("selectMenu", item);
  //然后跳转到item保存的path中
  router.push({
    path: item.path,
  });
};
//当点击关闭时触发deleteMenu 方法
let deleteMenu = (item, index) => {
   //会触发一个store的deleteMenu方法,把item传进去,这个方法会删除taglist对应的元素
  store.commit("deleteMenu", item);
  //下面是处理关闭后tag和面包屑的显示,有两种情况一种是删除的是最后一个元素(这元素也有两种情况如果删除的是当前页面和不是当前页面),第二种是不是最后一个元素(这个也有两种情况,如果删除的是当前页面,和不是当前页面)
  
  //注意index是点击时的索引,此时taglist中已经删除了此元素
  //那么如果这个index等于taglist的长度,那么就说明他是之前的最后一个,如果他还当前页面--index,然后调到前一个页面
  if (index == tagList.length) {
    if(item.name==route.name){
    store.commit("selectMenu", tagList[--index]);
    router.push({
      path: tagList[index].path,
    });
    }
  } else {
  //那么如果不是最后一个,如果删除后,判断是当前页面,删除后调到后面一个页面中,如果不是当前页面,则不变
      if(item.name==route.name){
    store.commit("selectMenu", tagList[index]);
    router.push({
      path: tagList[index].path,
    });
    }
  }
};
</script>

<style lang="less" scoped>
.tags {
  width: 100%;
  margin-bottom: 30px;
  .el-tag {
    margin-right: 10px;
  }
}
</style>

4.store中定义联调数据

store:{
    //在原来的store中添加tabList,有一个初始数据也就是首页
tabList:[
                {
                    path: "/home",
                    name: "home",
                    label: "首页",
                    icon: "home"
                }
            
        ]
}
mutation:{
     //selectMenu之前定义过,修改一下
      selectMenu(state,value){
                if(value.path=="/home"||value.path=="home"){
                    state.currentMenu=null
                }else{
                    state.currentMenu=value
                     //为tablist添加一个路由对象
                    state.tabList.findIndex(item=>
                        item.name==value.name )==-1?state.tabList.push(value):''

                }
            },
       //定义deleteMenu方法
     deleteMenu(state,value){
                //从
                let index=state.tabList.findIndex(item=>
                    item.name==value.name)
                    state.tabList.splice(index,1)
            },

}

  • 7
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Vue 3中实现搜索功能的方法有多种,以下是一种常见的实现方式: 1. 创建一个输入框和一个按钮,用于用户输入搜索关键字和触发搜索操作。 ```html <template> <div> <input type="text" v-model="keyword" placeholder="请输入关键字"> <button @click="search">搜索</button> </div> </template> ``` 2. 在Vue组件的data选项中定义一个变量`keyword`,用于保存用户输入的关键字。 ```javascript export default { data() { return { keyword: '', // 其他数据... } }, // 其他方法... } ``` 3. 在methods选项中定义一个`search`方法,用于处理搜索逻辑。 ```javascript export default { // 其他代码... methods: { search() { // 发起搜索请求或处理搜索逻辑 // 根据关键字进行过滤或发送网络请求等等 // 将搜索结果保存在组件的数据中,供展示使用 }, // 其他方法... } } ``` 4. 在搜索结果展示的地方,使用Vue的指令或计算属性来展示符合搜索条件的数据。 ```html <template> <div> <!-- 根据搜索结果展示数据 --> <ul> <li v-for="item in searchResults" :key="item.id">{{ item.name }}</li> </ul> </div> </template> ``` 5. 在Vue组件的计算属性中定义一个`searchResults`,用于返回符合搜索条件的数据。 ```javascript export default { // 其他代码... computed: { searchResults() { // 根据关键字过滤数据或者根据接口返回的数据进行处理 // 返回符合搜索条件的数据 }, // 其他计算属性... } } ``` 以上是一个简单的搜索功能实现的示例,你可以根据具体需求进行相应的调整和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员有道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值