vue3后台管理系统
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)) {
app.component(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() {
//定义store
let store = useStore();
function handleCollapse(){
//调用vuex中的mutations中的updateIsCollapse方法
store.commit("updateIsCollapse")
}
return {
handleCollapse
};
},
});
</script>
工具类的使用
一、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()
})
- 点击“ ”,创建项目,
- 得到项目的根路径
- 点击新增接口
- 编辑接口路径以及返回的数据
- 最终得到请求拦截地址,与请求得到的数据
- 在组件里的使用:
//axios请求table列表的数据,并且将请求来的数据赋值给tableData
async function getTableList(){
await axios
//该路径为线上mock的接口根路径与对应数据请求的路径的拼接。
.get("https://www.fastmock.site/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: 'https://www.fastmock.site/mock/d32d92a0e177cd10b103d38a2b74d3ec/api',
},
//2、测试环境
test: {
baseApi: '//test.future.com/api',
mockApi: 'https://www.fastmock.site/mock/d32d92a0e177cd10b103d38a2b74d3ec/api',
},
//3、线上环境,企业才会用,
pro: {
baseApi: '//future.com/api',
mockApi: 'https://www.fastmock.site/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文件里的开发环境的baseApi
baseURL: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总开关赋值给isMock
if (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.js
const 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的this
const {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赋值为当前菜单项item
val.name=='home'?(state.currentMenu==null):(state.currentMenu==val)
}
},
})
- 在commonAside组件里通过store调用 selectMenu()方法,并且传入当前的菜单项item
//在点击菜单进行路由跳转时调用vuex中的selectMenu(),并且传入当前的菜单项item
// 点击菜单进行路由跳转方法
function clickMenu(item){
router.push({
name:item.name
})
//vuex来管理路由跳转
store.commit('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 方法返回找到的元素的索引,而不是元素本身。如果没找到,则返回 -1
let 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,就是删除当前的菜单项
store.commit("closeTab",tag)
//做第一个判断,如果当前显示的菜单tag与要删除的tag菜单不一致,不做处理,还是上面的closeTab方法
if(tag.name !== route.name){
return;
}
//如果当前显示的菜单tag与要删除的菜单tag相同
//并且是最后一个菜单tag,则路由跳转到前面一个菜单tag
if(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=val
localStorage.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`
item.component=()=>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(){
//清除菜单
store.commit('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,
//设置token
setToken(state,val){
state.token=val
Cookie.set('token',val)
},
//清除token
clearToken(state){
state.token=''
Cookie.remove('token')
},
//获取token
getToken(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)=>{
store.commit('getToken')
const token=store.state.token
if(!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).length
if(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-column v-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: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.series let 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 = 200
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: 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 false
return 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该为男女,将值赋值给sexLabel
list.value = res.list.map((item) => {
item.sexLabel =(item.sex === 0 ? "女" : "男";)
return item; //并且每遍历一次就返回
});
用户的分页实现
- 使用elment-plus的Pagination 分页
- 定义一个对象congig ,里面有tatol属性,和当前页page属性
//定义当前页和atol的响应数据对象config
const 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-dialog
v-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-picker
v-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
});
}
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhggaibe
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01