admin 管理员组文章数量: 888297
vue3后台管理系统
vue3后台管理系统
- vite构建vue3项目
- 项目中其他需求的引入
- 1. element-plus引入
- 2. vue3引入路由
- 3. element-plus图标的引入和使用
- 静态引入图标
- 动态引入图标
- 4. 引入less
- 5. 基础样式引入
- 6. vuex的引入
- 工具类的使用
- 一、mock的使用
- 本地mock 的使用
- 线上fastmock 的使用
- 二、二次封装axios
- 普通component组价
- CommonHeader.vue(布局里的头部组件)
- 面包屑的实现
- CommonAside.vue(布局里的左侧菜单)
- CommonTab.vue -----(tag标签的展示及切换)
- 路由views组件
- LoginApp.vue(登录页面)
- 动态路由的实现
- 登出功能的实现
- 路由守卫的实现
- mainApp.vue(总体的结构布局组件)
- HomeApp.vue(布局里的主要展示区域)
- 折线图(echart表格)
- 柱状图(echart表格)
- 饼状图(echart表格)
- UserApp.vue(用户管理页面)
- 获取用户数据
- 用户的分页实现
- 增删改查用户数据
- 1. 搜索用户的实现
- 2. 新增用户的实现
- 3. 编辑用户的实现
- 4. 删除用户的实现
vite构建vue3项目
npm create vite@latest
,再回车- 按要求写下项目名;
manage-app
- 选择vue,再回车
- 选择javascript,在回车
cd manage-app
,到该项目目录下,回车- 安装依赖:
npm install
,再回车 - 启动项目:
npm dev
- main.js文件的改变:
createApp(App).mount('#app')
//上面那行改为下面这行,一样的,
const app=createApp(App)
app.mount('#app')
项目中其他需求的引入
1. element-plus引入
1. 全部引入:
- 在终端安转:
npm install element-plus --save
- main.js文件引入:
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'const app=createApp(App)
app.use(VueElementPlus)
app.mount('#app')
- 使用:直接在组件里编写element代码即可
2. 按需引入:
- 在终端安转:
npm install element-plus --save
- 在终端安装插件:
npm install -D unplugin-vue-components unplugin-auto-import
- 在 vite.config.ts文件引入:
//引入部分
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'//plugins数组部分加入plugins: [AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],
- 使用:直接在组件里编写element代码即可
3. 手动引入:
- 在终端安转:
npm install element-plus --save
- 终端安装:
npm install unplugin-element-plus
- 在 vite.config.ts文件引入:
//引入部分
import ElementPlus from 'unplugin-element-plus/vite'//plugins数组部分加入plugins: [ElementPlus()],
- 使用:在使用element的组件里引入具体需要的插件,比如需要引入按钮插件则需要加这段代码:
<script>
import { ElButton } from 'element-plus'export default defineComponent({component:{ ElButton}
})
</script>
2. vue3引入路由
- 终端安装:
npm install vue-router -S
- 新建文件:src / router / index.js:文件配置如下
import {createRouter,createWebHashHistory} from 'vue-router' const routes=[{path:'/',redirect:'/home',component:()=>import('../views/MainApp.vue'),children:[{path:'/home',nsme:'home',component:()=>import('../views/home/HomeApp.vue')}]}
]const router=createRouter({history:createWebHashHistory(),routes
})export default router
- main.js文件引入
import router from './router'const app=createApp(App)
app.use(router)
app.mount('#app')
- 使用:在具体的组件中需要导入
import {useRouter} from 'vue-router' //导入1
export default {setup() {let router=useRouter()//声明2
//下面可以配置方法进行路由跳转}
}
- 需要显示路由组件的地方添加
<router-view></router-view>
3. element-plus图标的引入和使用
- 在终端安转:
npm install element-plus --save
- 终端安转:
npm install @element-plus/icons-vue
- main.js引入:
import * as ElementPlusIconsVue from '@element-plus/icons-vue'const app = createApp(App)for (const [key, component] of Object.entries(ElementPlusIconsVue)) {appponent(key, component)
}app.mount('#app')
静态引入图标
- 直接点击想要的图标图案,就可以复制相关代码
//例如加号图标
<el-icon><Plus /></el-icon>
动态引入图标
<!-- 遍历菜单栏--><el-menu-item :index="item.path" v-for="item in noChildren()" :key="item.path"><!-- 根据遍历得到的item,动态引入图标 --><component class="icons" :is="item.icon"></component></el-menu-item>
4. 引入less
- 终端引入less:
npm install less-loader less --save-dev
5. 基础样式引入
- 在src/assets新建文件夹less
- 在less文件夹新建reset.less文件,这个是全局样式
- 在less文件夹新建index.less文件,里面只写
@import './reset.less';
- 在main.js中引入index.js文件 ,
import './assets/less/index.less'
6. vuex的引入
-
- 终端安装:
npm install vuex -S
- 终端安装:
-
- 新建文件:src / store / index.js,文件配置如下
import {createStore} from 'vuex'
export default createStore({//里面配置数据方法等state:{//数据} ,mutations:{//修改数据的方法},
})
-
- 在main.js文件引入
import store from './store/index.js'const app=createApp(App)
app.use(store)
app.mount('#app')
-
- 使用:使用vuex数据和方法的组件需要引入
<script>
import { useStore } from "vuex";
export default defineComponent ({setup() {//定义storelet store = useStore();function handleCollapse(){//调用vuex中的mutations中的updateIsCollapse方法storemit("updateIsCollapse")}return {handleCollapse};},
});
</script>
工具类的使用
一、mock的使用
-
本地mock 的使用
- 终端安转:
npm install mockjs
- 新建文件:src / api / mockData / home.js(home.js表示的是home组件的mock数据)---------文件配置如下:
export default{getHomeData:()=>{ //导出home 的数据return {code:200,data:{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,},]}}}
}
- 再建文件:src / api / mock.js ,这个文件引入所有的mock数据,并且全部导出。----------- 文件配置如下
//导入mockjs
import Mock from 'mockjs' //导入home的数据
import homeApi from './mockData/home' //拦截请求,两个参数,第一个参数是设置的拦截请求数据的路径,第二个参数是对应数据文件里该数据的方法a
Mock.mock('/home/getData',homeApi.getHomeData)
- 引入:在mian.js文件里引入mock--------如下引入
import './api/mock.js'
- 使用:在响应的组件上使用
async function getTableList(){await axios.get("/home/getData").then((res)=>{tableData.value=res.data.data.tableData})
}
onMounted(()=>{//调用getTableList()方法
getTableList()
})
-
线上fastmock 的使用
- 点击“+”,创建项目,
- 得到项目的根路径
- 点击新增接口
- 编辑接口路径以及返回的数据
- 最终得到请求拦截地址,与请求得到的数据
- 在组件里的使用:
//axios请求table列表的数据,并且将请求来的数据赋值给tableDataasync function getTableList(){await axios//该路径为线上mock的接口根路径与对应数据请求的路径的拼接。.get("/mock/d32d92a0e177cd10b103d38a2b74d3ec/api/home/getTableData").then((res)=>{if (res.data.code == 200){//请求成功之后在进行渲染tableData.value=res.data.data}})
}
onMounted(()=>{//调用getTableList()方法
getTableList()
})
二、二次封装axios
-
二次封装axios的原因:处理接口请求之前或接口请求之后的公共部分
-
终端安装:
npm install axios -S
-
在新建文件(环境配置文件):src /config / index.js------文件如下
/*** 环境配置文件* 一般在企业级项目里面有三个环境* 开发环境* 测试环境* 线上环境*/
// 当前的环境赋值给变量env
const env = import.meta.env.MODE || 'prod'const EnvConfig = {//1、开发环境development: {baseApi: '/api',//线上mock的根路径地址mockApi: '/mock/d32d92a0e177cd10b103d38a2b74d3ec/api',},//2、测试环境test: {baseApi: '//test.future/api',mockApi: '/mock/d32d92a0e177cd10b103d38a2b74d3ec/api',},//3、线上环境,企业才会用,pro: {baseApi: '//future/api',mockApi: '/mock/d32d92a0e177cd10b103d38a2b74d3ec/api',},
}export default {env,// mock的总开关,true则项目的所有接口调用的是mock数据mock: true,...EnvConfig[env]//结构
}
- 新建文件:src / api /request.js-------文件如下
import axios from 'axios'
import config from '../config'
//错误提示
import { ElMessage } from 'element-plus'
const NETWORK_ERROR = '网络请求异常,请稍后重试.....'//创建axios实例对象service
const service=axios.create({//根路径为config里的index.js文件里的开发环境的baseApibaseURL:config.baseApi
})//在请求之前做的一些事情,request
service.interceptors.request.use((req)=>{//可以自定义header//jwt-token认证的时候return req//需要return出去,否则会阻塞程序
})//在请求之后做的一些事情,response
service.interceptors.response.use((res)=>{console.log(res)//解构res的数据const { code, data, msg } = res.data// 根据后端协商,视情况而定if (code == 200) {//返回请求的数据return data} else {// 网络请求错误ElMessage.error(msg || NETWORK_ERROR)// 对响应错误做点什么return Promise.reject(msg || NETWORK_ERROR)}
})//封装的核心函数
function request(options){//默认为get请求options.methods=options.methods || 'get'if (options.method.toLowerCase() == 'get') {options.params = options.data}// 对mock的处理let isMock = config.mock //config的mock总开关赋值给isMockif (typeof options.mock !== 'undefined') { //若组件传来的options.mock 有值,单独对mock定义开关isMock = options.mock //就把options.mock 的值赋给isMock}// 对线上环境做处理if (config.env == 'prod') {// 如果是线上环境,就不用mock环境,不给你用到mock的机会service.defaults.baseURL = config.baseApi} else {//ismock中开关是否为true,若为真,则说明事由mock环境,那么跟路径就要事由mock的根路径service.defaults.baseURL = isMock ? config.mockApi : config.baseApi}return service(options) //函数的返回值
}export default request
- 新建文件 src / api / api.js-----文件如下:
// 整个项目api的管理
import request from "./request";//导出
export default{//home组件左侧表格数据获取getTableData(params){//request就是reuest.js文件中封装的核心函数,里面的对象就是options参数return request({url:'/home/getTableData',method:'get',data:params,//通过getTableData(params)方法的形参传过来mock:true})},
}
- 如果要进行axios请求数据,直接引入api.js即可
- 将api.js的方法挂载到全局,在main.js的文件配置如下
import api from './api/api' //引入api.jsconst app=createApp(App)
app.config.globalProperties.$api = api //全局挂载,将api赋值给$api
app.mount('#app')
- 使用:
import { defineComponent ,getCurrentInstance,onMounted ,ref} from "vue";export default defineComponent({setup() {//proxy类似于vue2的thisconst {proxy}=getCurrentInstance()//左侧表格的tableData数据let tableData =ref([])//getTableList()这个方法里面使用了api.js里面的getTableData方法来请求table里的数据
async function getTableList(){let res=await proxy.$api.getTableData() //通过proxy拿到api的请求数据的方法tableData.value=res
}onMounted(()=>{//调用getTableList()方法
getTableList()
})},
});
</script>
普通component组价
CommonHeader.vue(布局里的头部组件)
-
效果图
-
左侧引入图标,并且图标嵌套在el-button里
-
右侧个人头像,点击出现下拉菜单,个人中心和退出
- 个人头像的图片为动态引入,vite的图片静态资源处理
<img class="user" :src="getImageUrl('user')" alt="" /> //src前面加冒号,表示动态// 动态引入图片路径,参数为图片名字, function getImageUrl(user) {return new URL(`../assets/images/${user}.jpg`, import.meta.url).href;//../assets/images/${user}.jpg是图片相对路径,import.meta.url表示的是当前组件路径,两者进行拼接。 }return {getImageUrl, };
- 下拉菜单的实现:使用elment-plus的Dropdown 下拉菜单
面包屑的实现
- 使用element-plus的Breadcrumb 面包屑
- 实现思路:点击用户管理,首页后面显示用户管理,点击页面1,首页后面出现页面1,由于点击的是commonAside组件,而显示的面包屑出现在commonHeader组件,是跨组件建的通信,所以vuex管理数据。
- 在store / index.js 写如下代码
import {createStore} from 'vuex'
export default createStore({state:{//当前菜单赋值为空currentMenu:'',} ,mutations:{//选择菜单,val是commonAside组件传过来的当前点击的菜单值selectMenu(state,val){//判断,如果当前点击的菜单名为home,home就是首页,就让当前菜单currentMenu还是赋值为空,//否则就让currentMenu赋值为当前菜单项itemval.name=='home'?(state.currentMenu==null):(state.currentMenu==val)}},
})
- 在commonAside组件里通过store调用 selectMenu()方法,并且传入当前的菜单项item
//在点击菜单进行路由跳转时调用vuex中的selectMenu(),并且传入当前的菜单项item// 点击菜单进行路由跳转方法function clickMenu(item){router.push({name:item.name})//vuex来管理路由跳转storemit('selectMenu',item)}
- 在commonHeader组件里得到vuex的 currentMenu数据
<!-- 面包屑 --><el-breadcrumb separator="/">//永远显示首页<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item> //动态显示当前的菜单标签,并且有才current才显示<el-breadcrumb-item :to="current.path" v-if="current">{{current.label}}</el-breadcrumb-item> </el-breadcrumb>import { computed, defineComponent } from "vue";
import { useStore } from "vuex";
export default defineComponent({setup() {let store = useStore()//拿到vuex的 currentMenu数据,用计算属性,这样子菜单改变,页面也会跟着改变const current= computed(()=>{return store.state.currentMenu})}return {current,};},
})
</script>
CommonAside.vue(布局里的左侧菜单)
-
效果图:
-
布局使用:elment-plus的Menu菜单的侧栏
-
具体菜单实现
- 将菜单分为两组,有子菜单和无子菜单,分别遍历菜单数据并动态渲染文字、图标等。
-
功能:点击菜单,跳转到相应的路由组件。
<!-- 没有children的一级菜单 --><el-menu-item:index="item.path"v-for="item in noChildren()":key="item.path"@click="clickMenu(item)" 点击进行跳转的方法,并且传入item>
// 点击菜单进行路由跳转方法function clickMenu(item){router.push({name:item.name,})}
CommonTab.vue -----(tag标签的展示及切换)
- 思路:
- 首页的tag一开始就会存在,而且是不能进行删除的
- 当点击左侧栏的时候,如果tag没有该菜单名称则新增,如果已经有了那么当前tag背景为蓝色。
- 删除当前tag,如果是最后一个,那么路由调整到它前面那个标签并且背景为蓝色,如果不是最后一个,那么路由调整到它后面那个标签并且背景为蓝色。
- 注意tag无论路由如何切换都会存在,所以这个tag一定存在mainApp.vue组件中。
- 使用elment-plus的Tag 标签
- 在store / index.js里存储首页tag数据
import {createStore} from 'vuex'
export default createStore({// 数据state:{//tag数据,一开始只有首页tabsList:[{path:'/',name:'home',label:'首页',icon:'home'}],},
- 在CommonTab.vue组件里拿到首页的tag数据
import { useStore } from "vuex";
export default {setup() {let store = useStore();const tags = store.state.tabsList; //不需要计算属性拿到数据,因为这个值不会变化,永远都有首页这个tag
- 当点击不是首页的菜单时候,查看当前的 tabsList数组是否有该菜单,如果没有则向 tabsList数组添加该菜单项,如果有该菜单,就什么都不做。
//点击菜单进行路由跳转selectMenu(state,val){if(val.name == 'home'){state.currentMenu = null}else{state.currentMenu=val//arr.findIndex 方法返回找到的元素的索引,而不是元素本身。如果没找到,则返回 -1let result= state.tabsList.findIndex(item=>item.name == val.name)//item表示的是数组的每一项result== -1? state.tabsList.push(val):''}},
- 点击对应tag标签进行路由跳转,所以需要tag标签里设置点击事件
<script>
import { useRouter, useRoute } from "vue-router";
export default {setup() {let router=useRouter()//点击tag标签进行路由跳转的方法function changeMenu(item){router.push({name:item.name})}return {changeMenu,};
};
</script>
- 点击关闭tag标签,设置点击事件
//点击tag标签关闭方法function handleClose(tag,index){//让长度与索引保持一致let length=tags.length - 1;//处理vuex的tablelist,就是删除当前的菜单项storemit("closeTab",tag)//做第一个判断,如果当前显示的菜单tag与要删除的tag菜单不一致,不做处理,还是上面的closeTab方法if(tag.name !== route.name){return;}//如果当前显示的菜单tag与要删除的菜单tag相同//并且是最后一个菜单tag,则路由跳转到前面一个菜单tagif(index === length){router.push({name:tags[index-1].name})}else{ //不是最后一个菜单tag,则路由跳转到后面一个菜单tag,因为删除了该菜单,所以后面那个菜单的索引就是被删除菜单的索引。router.push({name:tags[index].name,})}}
- 在store文件里设置关闭标签的方法
closeTab(state,val){//拿到当前菜单的索引let res= state.tabsList.findIndex(item=>item.name === val.name) //在tabsList数组里,从当前菜单索引开始删除 1 个元素,就是删除当前的菜单项目state.tabsList.splice(res,1) },
- 在mainApp.vue组件里引用该tab组件
.
路由views组件
LoginApp.vue(登录页面)
功能:根据不同的用户角色,返回不一样的系统菜单
-
UI效果图
-
先配置路由跳转,登录路由级别与首页路由同级
-
使用element-plus的form表单
- 创建本地mock的permission.js写不同用户登录得到不同菜单的逻辑
- 在api.js里创建接口
//根据用户的用户名不同,返回不一样的菜单列表getMenu(params){ //点击登录时,用户信息会作为参数传过来return request({url:'/permission/getMenu',method:'post',mock: false,data: params})},
- 在mock.js里导入permission.js并且拦截接口,调用permission.js的方法
//导入登录的数据
import permission from './mockData/permissin'
Mock.mock(/permission\/getMenu/, 'post', permission. getMenu)
- 用v-model双向绑定用户的账号和密码,当点击登录时,就调用api.js里的getMenu(params)方法,并把当前的用户信息作为参数传过去,判断用户返回不一样的菜单,如图所示。
- 将返回的菜单数据存储到vuex中,aside组件显示出来。之前的菜单项是写死的,现在需要动态渲染。
- 在store / inde.js里定义方法得到当前用户的菜单,并且
localStorage.setItem
存储菜单数据
//根据不同用户返回不一样的菜单setMenu(state,val){state.menu=vallocalStorage.setItem('menu',JSON.stringify(val)) //将val转化为JSON格式},
-
CommonAside组件里通过store拿到当前的项。
-
在登录页面点击登录时通过store调用该方法,并且进行路由跳转到首页
-
每次刷新时,存储在vuex里的数据会丢失,所以动态得到的菜单也会消失,所以我们要解决数据持久化的问题。
-
所以在vuex里定义一个方法,用
localStorage.getItem
来获取当时localStorage.setItem
存储的菜单数据,并且该方法在每次刷新的时候调用,每次刷新肯定会经过App.vue组件,所以在App.vue组件里调用该方法。
//刷新获取 localStorage.setItem存储的菜单数据addMenu(state){if(!localStorage.getItem('menu')){return}const menu=JSON.parse(localStorage.getItem('menu'))state.menu=menu}
动态路由的实现
功能:根据后台返回的menu数据,动态的添加路由,而不是在router / index.js文件里直接配置要跳转的路由。除了首页和登陆注册页面,其他页面的路由都应该是动态添加的。
思路:
- 定义一个新的数组。用来存储菜单
const menuArray=[]
- 在store / index.js文件里的addmenu方法里遍历所得到的动态菜单,如果当前的菜单有子菜单,那么就用map对子菜单操作,返回当前菜单的路径url,并且使用懒加载的方式引入路由组件,并且把该子菜单项push到菜单空数组里
menu.forEach(item => {if(item.children){item.children=item.children.map(item=>{let url=`../views/${item.url}.vue`itemponent=()=>import(url)return item})menuArray.push(...item.children)//解构出子菜单}
- 如果当前菜单没有子菜单,依然返回路径,和上面操作一样,并且把该菜单项push到菜单空数组里
- 当menuArray里有菜单时,就遍历该菜单,并且使用
addRoute
方法添加一条新的路由记录作为首页路由(首页路由名为home1)的子路由。所以addmenu方法里还需要设置第二个参数router,当刷新时,需要传router过来。
menuArray.forEach(item=>{router.addRoute('home1',item)})
- 则routre / index.js文件里的children路由应该设置为一个空数组。
- addmenu方法应该在main.js文件里使用,因为如果在app.vue里使用,此时,页面已经挂载完毕,此时在进行动态路由添加晚了。
登出功能的实现
思路:
- 点击退出时,添加点击事件。
- 清除掉当前菜单
- 在store / index.js里定义清除菜单的方法,用
localstorage.remove
清除掉当时存储的菜单,
// 清除菜单clearMenu(state){state.menu=[]localStorage.removeItem('menu')},
- 然后在点击退出的时候调用该方法,并且跳转到登录页面。
//退出方法function handleLoginOut(){//清除菜单storemit('clearMenu')router.push({name:'login'})}
路由守卫的实现
思路:
- 即使我们知道首页等其他页面地址,但是如果我们没有进行登录操作,就不能跳转到对应的地址,而是直接跳转到登录页面地址,进行登录操作。根据后端返回的token来进行路由守卫,
- 在vuex里进行管理。先设置token 为空,当我们登录时,就对拿到登录返回的token值,并且赋值给vuex力的token
- 并且要对Token进行持久化,需要下载cookie
- 终端安装cookie:
npm install js-cookie --save
- 引用
import jsCookie from 'js-cookie'
- 将登录时返回的token数据val赋值给当前vuex里的token,
//设置tokensetToken(state,val){state.token=valCookie.set('token',val)},//清除tokenclearToken(state){state.token=''Cookie.remove('token')},//获取tokengetToken(state){//如果当前的vuex里有Token的值,就获取vuex里的token值,如果当前vuex里token值为空。state.token=state.token || Cookie.get('token')}
- 当点击登录按钮时,就调用设置Token的方法,并且拿到当前的Token作为参数对vuex里的token进行赋值
- 在main.js中添加一个全局路由守卫,先调用获取Token的方法,并且的到store里的token值,如果没有token 并且即将要跳转到的路由页面不是登录页面,那就直接让它跳转到登录页面。如果有token值,还要进行判断,如果没有匹配到当前路径,就直接跳转到首页,如果能匹配到当前路径,就跳转到对应的路由页面。
router.beforeEach((to,from,next)=>{storemit('getToken')const token=store.state.tokenif(!token && to.name !=='login'){next({name:'login'})}else if(!checkRouter(to.path)){ //如果没有检测到当前路径,就直接跳转到首页next({name:'home'})} else{next()}
})
- 在mian.js文件里要定义一个检查已有路由的方法,并且该方法要写在动态路由方法下面。
- ,
getRoutes
获取所有 路由记录的完整列表。并且对这些路由进行过滤,输出与输入的路径相同的已有路径的路由,如果该长度为0,说明输入的路径在已有的路由路径里不存在。就return false - 在长度,则说明输入的路径在已有的路由路径里存在,就return true
function checkRouter(path){let hasCheck=router.getRoutes().filter(route=>route.path==path).lengthif(hasCheck){return true}else{return false}
}
mainApp.vue(总体的结构布局组件)
- layout整体布局实现:. 使用element-plus的Container 布局容器
- 引入CommonHeader组件,并且在header区域导入
<CommonHeader/>
- 引入CommonAside组件,并且在Aside区域导入
<CommonAside/>
HomeApp.vue(布局里的主要展示区域)
-
总体使用elment-plus的 layout布局
-
layout布局的大概了解:
一行:
<el-row></el-row>
, 整个页面可以用这个布局
一列:<el-col></el-col>
-
-
用户信息展示使用使用elment-plus的card卡片
- 代码:
<el-card shadow="hover"><!-- 卡片的上部分具体的展示内容 --><div class="user"><!-- 图片展示 --><img src="../../assets/images/user.jpg" alt="" /><!-- 用户信息展示 --><div class="user-info"><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>
- 效果:
-
数据展示使用elment-plus的Table 表格
- 代码:
<el-card shadow="hover" style="margin-top: 20px" height="450px"><!-- 卡片里显示的是表格 --><el-table :data="tableData"><!-- 表格的每列的标题<el-table-column/>,并且根据数据遍历每一列--><el-table-columnv-for="(val, key) in tableLabel":key="key":prop="key":label="val"></el-table-column></el-table></el-card>
//数据 <script> import { defineComponent } from "vue"; export default defineComponent({ setup() { //左侧表格的tableData数据 const tableData = [{name: "oppo",todayBuy: 500,monthBuy: 3500,totalBuy: 22000,},{......}, ........ ]; //tableData数据的表头 const tableLabel = {name: "品类",todayBuy: "今日购买",monthBuy: "本月购买",totalBuy: "总共购买", };return {tableData,tableLabel};},});</script>
- 效果:
-
home组件右侧上面的数据展示,也是用card卡片展示
-
效果图:
-
数据来源于线上fastmock
-
在api.js文件中配置对应的请求数据的方法
<script> import { defineComponent, getCurrentInstance, onMounted, ref } from "vue"; export default defineComponent({ setup() { //proxy类似于vue2的this const { proxy } = getCurrentInstance();let countData = ref([]);//getCountData()这个方法里面使用了api.js里面的getCountData方法来请求table里的数据 async function getCountData() {let res = await proxy.$api.getCountData(); console.log(res); countData.value = res; }onMounted(() => { getCountData(); }); return { countData,};},});</script>
-
-
homeApp组件右侧表格:使用echart表格
-
折线图(echart表格)
具体可查看echart的配置项手册
* 效果:
* 终端下载echart:npm install echarts
* 页面引入echart:import * as echart from 'echarts'
* 装echart的盒子
xml <!-- 装echart折线表格的大卡片 --> <el-card style="height: 280px"> <!-- echart折线表格 --> <div ref="echart" style="height: 280px"></div> </el-card>
-
图形的配置
//折线图和柱状图的echarts配置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: [],});```
-
设置echart表格的空数据
//下面是rchart表格的数据,先设置为双向绑定数据,并且都为空值,后面通过接口获取数据,进行赋值let orderData = reactive({xData: [],series: [],});xData: [],series: [],});let videoData = reactive({series: [],});
-
线上mock配置表格数据
-
在api.js文件中创建方法请求表格数据
getEchartData(params){return request({url:'/home/getEchartData',method:'get',data:params,mock:true}) },
-
-
在homeApp.vue页面中获取数据,并且赋值给3个echart表格的空数据
//通过接口获取echart表格的数据,api.js async function getEchartData(){ let result=await proxy.$api.getEchartData() let orderData=result.orderData //折线图数据 let userData=result.userData //柱状图数据 let videoData=result.videoData //饼状图数据 orderData.xData=orderData.date //将折线图的x坐标的数据进行赋值 //每一条折线的数据是对象形式,每一条折线数据里的键相同,返回其中一条折线数据所有的键的数组。 const keyArray=Object.keys(orderData.data[0]) //定义一个空数组,折线数据的配置 const series=[] //遍历每一个键,并向空数组里添加一个对象,该对象包括折线名字,折线纵坐标的值,还有折线的展现形式 keyArray.forEach((key) => { series.push({name: key, //折线的名字data: orderData.data.map((item) => item[key]), //得到该键的值,就是折线纵坐标的值type: "line", //为折线展示 }); }); }orderData.series=series //对折线图的空数据进行赋值给orderData.series xOptions.xAxis.data=orderData.xData //对折线图的xAxis.data横坐标空数据进行重新赋值xOptions.series=orderData.series //对折线图的orderData.series赋值给空数组series//对折线图进行渲染 let hEcharts = echart.init(proxy.$refs['echart']) //将echart进行赋值给hechart hEcharts.setOption(xOptions) //开始建立折线图渲染 onMounted(() => {getEchartData() });
-
柱状图(echart表格)
- 效果:
- 页面的
<!-- 左侧柱状图 --><el-card style="height:260px"><div ref="userechart" style="height:240px"></div></el-card>
- 柱状图的配置
//折线图和柱状图的echarts配置相同,只是横坐标,纵坐标,series不同,这些数据请求得到。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: [],})
- 请求柱状图的数据
//通过接口获取echart表格的数据,api.js async function getEchartData(){ let result = await proxy.$api. getEchartData(); let userData=result.userData //柱状图数据 }
- 将请求来的数据赋值给当前的图形数据
// 柱状图进行渲染的过程 userData.xData =userData.map((item) => item.date); //柱状图的横坐标的值 userData.series = [{name: "新增用户",data: userData.map((item) => item.new),type: "bar",},{name: "活跃用户",data: userData.map((item) => item.active),type: "bar",}, ]; xOptions.xAxis.data=userData.xData xOptions.series=userData.serieslet uEcharts = echart.init(proxy.$refs['userechart']) uEcharts.setOption(xOptions)
- 效果:
-
饼状图(echart表格)
- 效果:
- 页面的装饼状图的代码
<!-- 右侧饼状图 --><el-card style="height:260px"><div ref="videoechart" style="height:240px"></div></el-card>
- 饼状图的配置
//饼状图的配置let pieOptions = reactive({tooltip: {trigger: "item",},color: ["#0f78f4","#dd536b","#9462e5","#a6a6a6","#e1bb22","#39c362","#3ed1cf",],series: [],});
// 饼状图进行渲染的过程 videoData.series = [{data: videoData,type: "pie",},];pieOptions.series = videoData.series;let vEcharts = echart.init(proxy.$refs["videoechart"]);vEcharts.setOption(pieOptions); };
- 效果:
UserApp.vue(用户管理页面)
使用element-plus里的table表格的固定列
获取用户数据
- 使用本地mock来配置用户数据:在mockData / user.js中
import Mock from 'mockjs'// 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 = 200for (let i = 0; i < count; i++) {List.push(Mock.mock({id: Mock.Random.guid(),name: Mock.Randomame(),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: config => {//每一页数据20条const { name, page = 1, limit = 20 } = param2Obj(config.url)const mockList = List.filter(user => {if (name && user.name.indexOf(name) === -1 && user.addr.indexOf(name) === -1) return falsereturn true})const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))return {code: 200,data: {list: pageList, //返回当前页的数据count: mockList.length, //返回整个数据的长度}}},
- 在mock.js文件里引入user.js文件,拦截请求,请求用户管理列表数据 ,
import userApi from './mockData/user'//本地获取user的数据,第一个参数通过正则来匹配路径,第二个参数是请求方式,第三个参数是请求数据的方法
Mock.mock(/user\/getUser/, 'get', userApi.getUserList)
- 在api.js里利用二次封装的axios进行请求数据
//获取user的table数据getUserData(params) {return request({url: '/user/getUser',method: 'get',// 这个mock如果是true的话 用的就是线上fastmock的数据mock: false,data: params// 分页器的data:{total: 0,page: 1,}//page是1拿到第一页的数据})},
-
在用户管理页面上获取该数据,并赋值给定义的双向绑定的空数据,渲染到页面上。
-
自己设置表头数据
// table 表格表头的数据
//因为返回的性别数据不是我们需要的,后面进行处理const tableLabel = reactive([{prop: "name",label: "姓名",},{prop: "age",label: "年龄",},{prop: "sexLabel",label: "性别",},{prop: "birth",label: "出生日期",width: 200,},{prop: "addr",label: "地址",width: 320,},]);
- 对拿到的性别数据进行处理,因为数据返回0和1,我们需要展示男女
//遍历每一条数据的sex,进行操作,将0,1该为男女,将值赋值给sexLabellist.value = res.list.map((item) => {item.sexLabel =(item.sex === 0 ? "女" : "男";)return item; //并且每遍历一次就返回});
用户的分页实现
- 使用elment-plus的Pagination 分页
- 定义一个对象congig ,里面有tatol属性,和当前页page属性
//定义当前页和atol的响应数据对象configconst config = reactive({page: 0,//默认为1,展示第一页的数据total: 1, //默认为 1,会别请求得到的tatol覆盖});
- 分页器设置点击事件,并且每次点击时候都请求一次用户数据
<!-- 分页器 --><el-pagination background layout="prev, pager, next" :total=config.total //分页器的总数class="pager mt-4" //自己设置样式@current-change="changePage" //点击事件,不用传参,默认点击的时候会得到该分页器的页数,/>//点击分页器跳转方法function changePage(page) {config.page = page;getUserData(config);}
增删改查用户数据
1. 搜索用户的实现
- 搜索框使用element-plus的form表单的行内表单
<!-- 搜索框 --><el-form :inline="true" :model="formInline"> //<el-form-item label="请输入">//<el-input v-model="formInline.keyword" placeholder="请输入用户名" /></el-form-item><el-form-item><el-button type="primary" @click="handleSerch">搜索</el-button></el-form-item></el-form>
- 在搜索按钮里定义点击事件,
//定义当前页和atol的响应数据对象config,这里新增name属性const config = reactive({page: 0,total: 1,name: "", //默认空值});//定义forminline,搜索框的关键字const formInline = reactive({keyword: "",});//点击搜索的方法,把keyword关键字赋值给config的name属性,作为params参数,拿到对应的数据function handleSerch() {config.name = formInline.keyword;getUserData(config);}
2. 新增用户的实现
- 新增用户ui界面实现
-
效果图:
-
使用elmnt-plus的Dialog 对话框
-
Dialog 对话框添加用户信息新增的表单
-
性别选择用表单里的选择框
-
出生日期用表单的选择日期
-
页面代码
<!-- 用户新增布局dialog ,模态框,新增用户和编辑用户信息共用一个模态框,--><el-button type="primary" @click="dialogVisible = true">+新增</el-button><el-dialogv-model="dialogVisible"title="新增用户"width="40%":before-close="handleClose"><!-- 用户信息表单部分 --><el-form :inline="true" :model="formUser" class="userForm"><!-- 一行包含两列 --><el-row><el-col :span="12"><el-form-item label="姓名"><el-input v-model="formUser.name" placeholder="请输入用户名" /></el-form-item></el-col><el-col :span="12"><el-form-item label="年龄"><el-input v-model="formUser.age" placeholder="请输入用户年龄" /></el-form-item></el-col></el-row><!-- 一行包含两列 --><el-row><el-col :span="12"><el-form-item label="性别"><el-select v-model="formUser.sex" placeholder="请输入性别"><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="出生日期"><el-date-pickerv-model="formUser.birth"type="date"placeholder="请选择出生日期"style="width: 100%"/></el-form-item></el-col></el-row><!-- 最后只有一行只有一列地址 --><el-row><el-col :span="12"><el-form-item label="地址"><el-input v-model="formUser.addr" placeholder="请输入地址" /></el-form-item></el-col></el-row><!-- 第四行是取消确定按钮,并且靠右 --><el-row style="justify-content: flex-end"><el-button type="primary" @click="handleCancel">取消</el-button><el-button type="primary" @click="onSubmit">确定</el-button></el-row></el-form></el-dialog>
- 定义用户对象数据,双向定
//添加用户的form数据const formUser = reactive({name: "", //添加的用户姓名age: "", //添加用户年龄sex: "",birth: "",addr: "",});
- 新增用户数据实现
- 要将对话框里填写的用户 提交到用户数据里。
- 本地mock的user.js中已经定义了添加用户的方法,
- 在mock.js里拦截数据,当匹配到该路径的话,就调用该方法
Mock.mock(/user\/add/, 'post', userApi.createUser)
- api.js中利用二次封装的axios提交用户数据。
//新增用户接口addUser(params){return request({url:'/user/add',method:'post',mock: false,data: params})},
- 点击确定提交用户数据成功之后,需要完成以下事情
- 对话框要重置,里面的数据要消失
- 对话框要消失
- 还需要重新调用getuserlist方法 。拿到最新的用户数据
//点击确定提交用户数据
async function onSubmit(){let res = await proxy.$api.addUser(formUser);if(res){//重置表单,<el-form :inline="true" :model="formUser" ref="userForm">,userForm表单数据重置proxy.$refs.userForm.resetFields();//点击确定后关闭对话框dialogVisible.value = false;//重新请求用户数据getUserData(config)}
}
- 新增用户表单验证
:rules就是自己定义的规则<el-form-item label="姓名" prop="name" :rules="[{ required: true, message: '姓名是必填项' }]" ><el-form-item label="年龄" prop="age" :rules="[{ required: true, message: '年龄是必填项' },{ type: 'number', message: '年龄必须是数字' },]"><el-input v-model.number="formUser.age" placeholder="请输入用户年龄" />v-model.number可以使输入的字符串数字转变为number类型。
- 点击确定时
- 每一个用户信息框都进行校验,当有信息框为填时,不能提交数据 , 用表单的validae方法来校验。
//点击确定提交用户数据 function onSubmit(){//如果能拿到形参,就是如果所有的必选框都填了,就提交数据proxy.$refs.userForm.validate(async (valid)=>{if(valid){//调用日期格式化方法 formUser.birth = timeFormat(formUser.birth);let res = await proxy.$api.addUser(formUser);if(res){//重置表单proxy.$refs.userForm.resetFields();//点击确定后关闭对话框dialogVisible.value = false;//重新请求用户数据getUserData(config)}}}) }
- 点击取消按钮时候,应该完成以下事情
- 重置表单
- 关闭对话框
//点击取消,重置表单,关闭对话框function handleCancel() {proxy.$refs.userForm.resetFields();dialogVisible.value = false;}
- 当有信息未填写完成,点击确定时,应该提示错误信息,使用element-plus的Message
3. 编辑用户的实现
思路:
- 点击编辑时候,显示对话框。并且复用新增用户的对话框,新增用户的对话框变成编辑用户的对话框
- 定义一个变量来区分当前是编辑用户还是新增用户。在点击新增和编辑的时候改变该变量。
- 点击编辑的时候要把拿到当前行的用户信息,可以用插槽的方法
#default="scope"
拿到当前表格的数据,再用scope.row
拿到当前行的数据。
//#default="scope"可以拿到当前表格的数据<template #default="scope"> //@click="handleEdit(scope.row)"点击编辑时候可以拿到当前行scope.row 的数据 <el-button type="primary" size="small" @click="handleEdit(scope.row)" >编辑</el-button ></template>
- . 把拿到的这些数据赋值到编辑用户的对话框里。用浅拷贝
object.assign(目标对象,源对象)
//把当前行的数据赋值当formUser用户对象上。Object.assign(formUser, row);
- 点击取消,再点击新增时,对话框框一个为空,所以一个异步操作浅拷贝
proxy.$nextTick(() => {//拿到每一行的用户数据,可以用浅拷贝Object.assign(formUser, row);});
- 点击确定时,需要用编辑用户的接口
- 在api.js里利用二次封装的axios调用编辑用户的接口
//编辑用户接口aditUser(params){return request({url:'/user/edit',method:'post',mock: false,data: params})},
- 在mock.js里通过匹配路径拦截请求,调用本地mock的user.js文件的编辑用户方法。
Mock.mock(/user\/edit/, 'post', userApi.updateUser)
- 在点击确定的时候的点击事件需要进行判断,判断如果当前是编辑用户的对话框,就调用编辑用户的接口
4. 删除用户的实现
思路
- 在api.js里利用二次封装的axios调用删除用户的接口
//删除用户接口deleteUser(params){return request({url:'/user/delete',method:'get',mock: false,data: params})},
- 在mock.js里通过匹配路径拦截请求,调用本地mock的user.js文件的删除用户方法。
Mock.mock(/user\/delete/, 'get', userApi. deleteUser)
- 点击删除时,设置点击事件,并且获取当前行的数据
<template #default="scope"><el-button type="primary" size="small" @click="handleDelete(scope.row)" >编辑</el-button ></template>
- 点击删除时,判断你确定删除吗,如果确定删除,就调用删除用户的接口,并且需要传入当前数据的id值,删除成功就message提示删除成功。在重新调用获取用户信息接口。
//删除用户方法function handleDelete(row){//判断是否删除吗ElMessageBox.confirm("确定删除吗")//确认删除则以下做法.then(async() => {//调用删除用户接口await proxy.$api. deleteUser({ id:row.id })//弹出删除成功信息ElMessage({showClose:true,Message:'删除成功',type:"success"})//重新获取用户数据getUserData(config)}).catch(() => {// catch error});}
本文标签: vue3后台管理系统
版权声明:本文标题:vue3后台管理系统 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1693585545h230741.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论