把数据渲染到侧边栏
先通过外层拿到唯一值index
<template v-for="(item,index) in props.menuData">
<!-- 没有子菜单的获取数组 -->
<el-menu-item
v-if="!item.children || item.children.length ==0"
:index="`${props.index}-${item.meta.id}`"
:key="`${props.index}-${item.meta.id}`">
<!-- 图标 -->
<el-icon size="20">
<!-- vue的动态组件语法 -->
<!-- 结合数据动态渲染图标 -->
<component :is="item.meta.icon"></component>
</el-icon>
<!-- 菜单内容 -->
<span>{{ item.meta.name }}</span>
</el-menu-item>
<!-- 有子菜单的 -->
<el-sub-menu v-else :index="`${props.index}-${item.meta.id}`">
<template #title>
<el-icon size="20">
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{ item.meta.name }}</span>
</template>
<!-- 有子菜单下面需要渲染的 -->
<!-- 递归 -->
<tree-menu :index="`${props.index}-${item.meta.id}`"
:menu-data="item.children" />
</el-sub-menu>
</template>
渲染之后的结果是:
点击路由跳转:
Main.vue
显示图标:
下载并注册:
npm install @element-plus/icons-vue
main.js:
// main.ts
// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
下载vuex,作为状态管理:
#安装
npm install vuex@next --save
#menu.js
const state={
isCollapse:false,
selectMenu:[]
}
const mutations={
collapsMenu(state){
state.isCollapse=!state.isCollapse
}
}
export default {state,mutations}
# store/index.js
const state={
isCollapse:false,
selectMenu:[]
}
const mutations={
collapsMenu(state){
state.isCollapse=!state.isCollapse
}
}
export default {state,mutations}
#挂载到main.js
// 挂载vuex实例
import store from './store'
app.use(store)
控制菜单展开与收起功能:
通过elementPlus中的某个属性::collapse="isCollapse"
效果:
点击菜单,添加到顶部:
#menu.js
#mutation:
addMenu(state,payload){
// 对数据进行去重
if(state.selectMenu.findIndex(item=>item.path === payload.path) === -1){
state.selectMenu.push(payload)
}
}
#在treeMenu.vue中调用这个方法
import {useStore} from'vuex'
const store=useStore()
// 点击侧边栏跳转
const handleClick=(item,active) =>{
// console.log(item);
store.commit('addMenu',item.meta)
router.push(item.meta.path)
}
#在navHeader中显示
import { useStore } from 'vuex'
import {computed} from 'vue'
// 拿到store实例
const store=useStore()
// 通过computed拿到数据
const selectMenu=computed(()=>store.state.menu.selectMenu)
<div class="header-left">
<el-icon class="icon" size="20" @click="store.commit('collapsMenu')"><Fold/></el-icon>
<!-- 顶部标签 -->
<ul class="flex-box">
<li
v-for="(item,index) in selectMenu"
:key="item.path"
class="tab flex-box">
<el-icon class="icon" size="20"><component :is="item.icon"/></el-icon>
{{ item.name }}
<!-- 关闭按钮 -->
<el-icon class="icon" size="20"><Close/></el-icon>
</li>
</ul>
</div>
点击高亮:让url中的数据与点击选中对应起来
#navHeader.vue
import { useRoute } from 'vue-router'
// 当前路由对象
const route=useRoute()
ul class="flex-box">
<li
v-for="(item,index) in selectMenu"
:key="item.path"
:class="{selected : route.path === item.path}"
class="tab flex-box">
<el-icon size="12"><component :is="item.icon"/></el-icon>
<!-- 点击跳转 -->
<router-link class="text flex-box" to="{path: item.path}">
{{ item.name }}
</router-link>
<!-- 关闭按钮 -->
<el-icon size="12" class="close"><Close/></el-icon>
</li>
.close{
visibility: hidden;
}
&.selected {
a{
color: #409eff;
}
i{
color: #409eff;
}
background-color: #f5f5f5;
}
tag的点击关闭功能:
普通的直接关闭,高亮的点击关闭之后,高亮后移
#navHeader.vue添加点击事件
<!-- 关闭按钮 -->
<el-icon size="12" class="close" @click="closeTab(item,index)"><Close/></el-icon>
#方法
import { useRoute,useRouter } from 'vue-router'
// 当前路由对象
const router=useRouter()
// 拿到store实例
const store=useStore()
// 通过computed拿到数据
const selectMenu=computed(()=>store.state.menu.selectMenu)
// 点击关闭tag
const closeTab=(item,index)=>{
store.commit('closeMenu',item)
// 删除非当前页tag
if(route.path !== item.path){
return
}
const selectMenuData = selectMenu.value
// 删除最后一项
if(index === selectMenuData.length){
// 如果tag只有一个元素
if(!selectMenuData.length){
router.push('/')
}else{
router.push({
path:selectMenuData[index-1].path
})
}
}else{//删除的是中间位置tag
router.push({
path:selectMenuData[index].path
})
}
}
#menu.js存储状态
closeMenu(state,payload){
// 找到点击数据的索引
const index = state.selectMenu.findIndex(val=>val.name === payload.name)
// 通过索引删除数组指定元素
state.selectMenu.splice(index,1)
}
效果图:
卡片中登录和注册的切换:
#login/index.vue
<div class="jump-link">
<el-link type="primary" @click="handleChange">{{ formType ? '返回登录':'注册账号' }}</el-link>
</div>
import { ref } from 'vue'
const imgUrl = new URL('../../../public/login-head.png', import.meta.url).href
const formType=ref(0) //切换表单(0:登录,1:注册)
const handleChange=()=>{
formType.value=formType.value ? 0:1
}
注册/登录页面:
<script setup>
// import { UserFilled } from '@element-plus/icons-vue/dist/types';
import { ElMessage } from 'element-plus';
import { reactive, ref } from 'vue'
const imgUrl = new URL('../../../public/login-head.png', import.meta.url).href
const formType=ref(0) //切换表单(0:登录,1:注册)
const handleChange=()=>{
formType.value=formType.value ? 0:1
}
// 表单数据
const loginForm = reactive({
userName:'',
passWord:'',
validCode:''
})
// 发送短信
const countdown = reactive({
validText:'获取验证码',
time:60
})
let flag=false
// 倒计时技术
const countdownChange =()=>{
// 如果已发送,不做处理
if(flag) return
// 判断手机号是否正确
const phoneReg=/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/
if(!loginForm.userName || !phoneReg.test(loginForm.userName)){
return ElMessage({
message:'请检查手机号受否正确',
type:'warning'
})
}
// 倒计时
setInterval(()=>{
if(countdown.time<1){
countdown.time-=60
countdown.validText='获取验证码'
flag=true
}else{
countdown.time-=1
countdown.validText=`剩余${countdown.time}s`
}
},1000)
}
// 账号校验
const validateUser =(rule, value, callback)=>{
// 不能为空
if(value === ''){
callback(new Error('请输入账号'))
}else{
const phoneReg=/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/
phoneReg.test(value) ? callback():callback(new Error('手机格式合适不对'))
}
}
// 密码校验
const validatePass = (rule, value, callback)=>{
if(value === ''){
callback(new Error('请输入密码'))
}else{
const passReg=/^[a-zA-Z0-9_-]{4,16}$/
passReg.test(value) ? callback():callback(new Error('密码为4-16位字符'))
}
}
// 表单校验
const rules=reactive({
userName:[{validator: validateUser, trigger: 'blur' }],
passWord:[{validator: validatePass, trigger: 'blur' }],
})
// 提交
const submitForm =()=>{
}
</script>
<template>
<el-row class="login-container" justify="center" :align="'middle'">
<el-card style="max-width: 480px">
<template #header>
<div class="card-header">
<!-- 静态资源的引入 -->
<img :src="imgUrl" alt="">
</div>
</template>
<div class="jump-link">
<el-link type="primary" @click="handleChange">{{ formType ? '返回登录':'注册账号' }}</el-link>
</div>
<!-- 表单 -->
<el-form
:rules="rules"
:model="loginForm"
style="max-width: 600px"
class="demo-ruleForm">
<el-form-item prop="userName">
<el-input v-model="loginForm.userName" placeholder="手机号" :prefix-icon="UserFilled"></el-input>
</el-form-item>
<el-form-item prop="passWord">
<el-input v-model="loginForm.passWord" type="password" placeholder="密码" :prefix-icon="Lock"></el-input>
</el-form-item>
<el-form-item prop="validCode" v-if="formType">
<el-input v-model="loginForm.validCode" placeholder="验证码" :prefix-icon="Lock">
<template #append>
<span @click="countdownChange">{{ countdown.validText }}</span>
</template>
</el-input>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<el-button type="primary" :style="{width:'100%'}" @click="submitForm">
{{ formType? '注册账号':'登录' }}
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-row>
</template>
<style lang="less" scoped>
:deep(.el-card__header) {
padding: 0
}
.login-container {
height: 100%;
.card-header{
background-color: #899fe1;
img {
width: 430px;
}
}
.jump-link {
text-align: right;
margin-bottom: 10px;
}
}
</style>
表单校验:
倒计时效果的制作:
效果图:
对axios进行二次封装:
#request.js
// 对axios进行简单的二次封装
import axios from "axios"
import { ElMessage } from "element-plus"
const instance = axios.create({
baseURL: 'https:/v3pz.itndedu.com/v3pz',
timeout: 10000,
});
// 添加拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
const token=localStorage.getItem('pz_token')
// 不需要添加token的api
const whiteUrl=['get/code','user/authentication','login']
if(token && !whiteUrl.includes(config.url)){
config.headers['x-token']=token
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对接口异常的数据,给用户提示
if(response.data.code === -1){
ElMessage.warning(response.data.message)
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
export default instance
#index.js
import request from '../utils/request'
// 发送验证码
export const getCode =(data) =>{
return request.post('/get/code',data)
}
#login/index.vue
import { getCode } from '../../api/index'
const countdownChange =()=>{
// 如果已发送,不做处理
if(flag) return
// 判断手机号是否正确
const phoneReg=/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/
if(!loginForm.userName || !phoneReg.test(loginForm.userName)){
return ElMessage({
message:'请检查手机号受否正确',
type:'warning'
})
}
// 倒计时
setInterval(()=>{
if(countdown.time<=0){
countdown.time = 60
countdown.validText='获取验证码'
flag=false
}else{
countdown.time -= 1
countdown.validText=`剩余${countdown.time}s`
}
},1000)
flag = true
// 获取验证码,与接口连桥
getCode({tel:loginForm.userName}).then((data)=>{
// console.log(data,'data')
if(data.code === 10000){
ElMessage.success('发送成功')
}
})
}
注册接口,登录接口的联调:
#indx.js
// https:/v3pz.itndedu.com/v3pz/user/authentication
// 注册用户
export const userAuthentication =(data)=>{
return request.post('/user/authentication',data)
}
// https:/v3pz.itndedu.com/v3pz/login
// 登录
export const login=(data)=>{
return request.post('/login',data)
}
#/login/index.vue
const submitForm =async(formEl,fields)=>{
if (!formEl) return
// 手动触发校验
await formEl.validate((valid) => {
if (valid) {
// console.log('submit!')
// 注册页面
if(formType.value){
userAuthentication(loginForm).then(({date})=>{
if(date.code === 10000){
ElMessage.success('注册成功,请登录')
formType.value=0
}
})
}else{
// 登录页面
login(loginForm).then(({data})=>{
if(data.code === 10000){
ElMessage.success('登录成功')
console.log(data)
// 将token和用户信息缓存到浏览器
localStorage.setItem('pz_token',data.data.token)
localStorage.setItem('pz_userInfo',JSON.stringify(data.data.userInfo))
// localStorage无法传引用数据类型,因此用stringify转成字符串
}
})
}
} else {
console.log('error submit!',fields)
}
})
}
添加前置路由守卫,并且如果如果token过期,返回登录页:
亮点在于,在清除路由信息的时候,可以 通过: window.location.href=window.location.origin
#main.js
// 拦截器
router.beforeEach((to,from)=>{
const token = localStorage.getItem('pz_token')
// 非登录页面,token不存在
if(!token && to.path !== '/login'){
return '/login'
}else if(token && to.path === '/login'){
return '/'
}else {
return true
}
})
#api/index.js
// 权限管理列表
// https:/v3pz.itndedu.com/v3pz/auth/admin
export const authAdmin = (params)=>{
return request.get('/auth/admin',{params})
}
#admin/index.vue
<script setup>
import { authAdmin }from '../../../api/index'
import { reactive, onMounted } from 'vue'
const paginationData = reactive({
pageNum: 1,
pageSize: 10
})
onMounted(()=>{
authAdmin(paginationData).then(({data})=>{
console.log(data,'authAdmin')
})
})
</script>
<template>
<div>admin</div>
</template>
<style lang="less" scoped>
</style>
#request.js
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对接口异常的数据,给用户提示
if(response.data.code === -1){
ElMessage.warning(response.data.message)
}
if(response.data.code === -2){
localStorage.removeItem('pz_token')
localStorage.removeItem('pz_userInfo')
window.location.href=window.location.origin
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
菜单添加管理弹窗:
#group/index.vue
<template>
<button @click="dialogFormVisable=true">打开</button>
<!-- 弹窗 -->
<el-dialog
v-model="dialogFormVisable"
:before-close="beforeClose"
title="添加权限"
width="500">
<!-- form表单 -->
<el-form
ref="formRef"
label-width="100px"
label-position="left"
:model="form"
:rules="rules">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请填写权限名称"></el-input>
</el-form-item>
<el-form-item label="权限" prop="permissions">
<el-tree
ref="treeRef"
style="max-width: 600px"
:data="permissionData"
node-key="id"
show-checkbox
:default-check-keys="defaultKeys"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirm()">
确认
</el-button>
</div>
</template>
</el-dialog>
// 弹窗的显示与隐藏
const dialogFormVisable= ref(false)
// 关闭弹窗的回调
const beforeClose=()=>{
dialogFormVisable.value=false
}
// form表单数据
const form = reactive({
id:'',
name:'',
permissions:''
})
// 树型菜单权限数据
const permissionData = ref([])
onMounted(()=>{
// 菜单数据
userGetmenu().then(({data})=>{
// console.log(data)
permissionData.value=data.data
})
getListData()
})
#index.js
// 数型菜单,权限管理
// https:/v3pz.itndedu.com/v3pz/user/getmenu
export const userGetmenu=()=>{
return request.get('/user/getmenu')
}
弹窗默认情况下,权限管理及其子菜单,默认选中:
菜单管理的列表页:
#group.vue
<template>
<button @click="open(null)">打开</button>
<!-- 表单数据 -->
<el-table :data="tabelData.list">
<el-table-column prop="id" label="id"/>
<el-table-column prop="name" label="昵称"/>
<el-table-column prop="permissionName" label="菜单权限"/>
<el-table-column label="操作">
<template #default="scope">
<el-button type="primary" @click="open(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
// 请求列表数据
const getListData =() =>{
menuList(paginationData).then(({data})=>{
// console.log(data,'data')
const{list,total}=data.data
tabelData.list=list
tabelData.total=total
})
// 列表数据
const tabelData = reactive({
list:[],
total:0
})
// 打开编辑弹窗
const open = (rowData={})=>{
dialogFormVisable.value=true
// 弹窗打开form是异步的
nextTick(()=>{
if(rowData){
Object.assign(form,{id:rowData.id,name:rowData.name})
treeRef.value.setCheckedKeys(rowData.permission)
}
})
}
onMounted(()=>{
// 菜单数据
userGetmenu().then(({data})=>{
// console.log(data)
permissionData.value=data.data
})
getListData()
})
#index.js
export const userSetmenu=(data)=>{
return request.post('/user/setmenu',data)
}
// 菜单权限列表
export const menuList=(params) =>{
return request.get('/menu/list',{params})
}
表单重置: formRef.value.resetFields()
头部做成一个组件:
#paneHead.vue
<script setup>
</script>
<template>
<div class="panel-heading">
<div class="panel-lead">
<div class="title">菜单管理</div>
<p class="description">菜单规则通常对应一个控制器的方法</p>
</div>
</div>
</template>
<style lang="less" scoped>
.panel-heading {
padding: 15px;
background: #e8edf0;
border-color: #e8edf0;
position: relative;
.panel-lead {
font-size: 14px;
.title {
font-weight: bold;
font-style: normal;
}
.description {
margin-top: 5px;
}
}
}
</style>
#引入作为全局组件
// 引入头部组件
import PanelHead from './components/panelHead.vue'
app.component('PanelHead',PanelHead)
#group.vue调用
<panel-head />
<div class="btns">
<el-button :icon="Plus" type="primary" @click="open(null)" size="small">新增</el-button>
</div>
效果:
根据以下,写权限列表:
#admin.vue
<script setup>
import { authAdmin,menuSelectlist }from '../../../api/index'
import dayjs from 'dayjs'
import { ref, reactive, onMounted } from 'vue'
const paginationData = reactive({
pageNum: 1,
pageSize: 10
})
const tableData=reactive({
list:[],
total:0
})
onMounted(()=>{
authAdmin(paginationData).then(({data})=>{
console.log(data,'authAdmin')
const { list,total} = data.data
list.forEach(item =>{
item.create_time=dayjs(item.create_time).format('YYYY-MM-DD')
})
tableData.list=list
tableData.total=total
})
menuSelectlist().then(({data})=>{
options.value=data.data
})
})
const options=ref([])
// 根据权限id匹配权限名称
const permissionName = (id) =>{
const data = options.value.find(el=>el.id === id)
return data ? data.name : '超级管理员'
}
const open = ()=>{
}
</script>
<template>
<div>admin</div>
<el-table :data="tableData.list" style="width: 100%;">
<el-table-column prop="id" label="id"/>
<el-table-column prop="name" label="昵称"/>
<el-table-column prop="permissionName" label="所属组别">
<template #default="scope">
{{ permissionName(scope.row.permissions_id)}}
</template>
</el-table-column>
<el-table-column prop="mobile" label="手机号"/>
<el-table-column prop="active" label="状态">
<template #default="scope">
<el-tag :type="scope.row.active ? 'success' : 'danger'">{{ scope.row.active? '正常':'失效' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<div class="flex-box">
<el-icon><Clock/></el-icon>
<span style="margin-left: 10px">{{ scope.row.create_time }}</span>
</div>
<!-- <el-tag :type="scope.row.active ? 'success' : 'danger'">{{ scope.row.active? '正常':'失效' }}</el-tag> -->
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button type="primary" @click="open(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</template>
<style lang="less" scoped>
.flex-box {
display: flex;
align-items: center;
}
</style>
#index.js
// 权限下拉列表
// menu/selectlist
export const menuSelectlist=()=>{
return request.get('menu/selectlist')
}