vue3实现通用后台管理(傻瓜式一步一步记录代码实现过程)
傻瓜式记录用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)
},
}
更多推荐
所有评论(0)