赞
踩
首先我们要安装一些依赖
其中的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 是一个基于 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来模拟后端,返回数据了
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页面来做布局
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
}
}
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>
这是每一个页面都可以显示的,所以需要放到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) }, }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。