效果:
#src/api/index
import request from '../utils/request'
// 发送验证码
export const getCode =(data) =>{
return request.post('/get/code',data)
}
// 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)
}
// 权限管理列表 这段****
// https:/v3pz.itndedu.com/v3pz/auth/admin
export const authAdmin = (params)=>{
return request.get('/auth/admin',{params})
}
// 数型菜单,权限管理
// https:/v3pz.itndedu.com/v3pz/user/getmenu
export const userGetmenu=()=>{
return request.get('/user/getmenu')
}
// 菜单权限的修改
// https:/v3pz.itndedu.com/v3pz/user/setmenu
export const userSetmenu=(data)=>{
return request.post('/user/setmenu',data)
}
// 菜单权限列表
export const menuList=(params) =>{
return request.get('/menu/list',{params})
}
// 权限下拉列表
// menu/selectlist
export const menuSelectlist=()=>{
return request.get('menu/selectlist')
}
// 用户数据修改
export const updateUser=(data) =>{
return request.post('/update/user', data)
}
// 用户菜单权限
// menu/permissions
export const menuPermissions=()=>{
return request.get('/menu/permissions')
}
// 获取陪护师头像列表
// https:/v3pz.itndedu.com/v3pz/photo/list
export const photoList = ()=>{
return request.get('/photo/list')
}
// 陪护师创建
// https:/v3pz.itndedu.com/v3pz/companion
export const companion=(data)=>{
return request.post('/companion',data)
}
// 陪护师列表
// https:/v3pz.itndedu.com/v3pz/companion/list
export const companionList=(params)=>{
return request.get('companion/list',{params})
}
// 陪护师删除
// https:/v3pz.itndedu.com/v3pz/delete/companion
export const deleteCompanion=(data)=>{
return request.post('/delete/companion',data)
}
#src/component/aside.vue
<script setup>
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
import TreeMenu from '../components/treeMenu.vue'
import { useRouter } from 'vue-router'
import { reactive,computed } from 'vue'
import { useStore } from 'vuex'
const router=useRouter()
// 获取展开侧边栏中的属性(存在store中)
const store=useStore()
const isCollapse=computed(()=>store.state.menu.isCollapse)
// 拿控制台的数据,将menu传给父组件
// const menuData = reactive(router.options.routes[0].children)
const menuData=computed(()=>store.state.menu.routerList)
// 默认选中颜色
const active=computed(()=>store.state.menu.menuActive)
// console.log(router);
const handleOpen = () => {
}
const handleClose = () => {
}
</script>
<template>
<el-menu
:style="{width: !isCollapse ? '230px': '64px'}"
active-text-color="#ffd04b"
background-color="#545c64"
class="aside-container"
text-color="#fff"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
:default-active="active"
>
<p class="logo-lg">{{ isCollapse ? 'DIDI':'DIDI陪诊' }}</p>
<TreeMenu :index="1" :menuData="menuData" />
<!-- 组件化 -->
<!-- <el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group title="Group One">
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<span>Navigator Three</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<span>Navigator Four</span>
</el-menu-item> -->
</el-menu>
</template>
<style lang="less" scoped>
.aside-container{
height: 100%;
.logo-lg {
font-size: 20px;
text-align: center;
height: 50px;
line-height: 50px;
color: #fff;
}
}
</style>
#src/component/navHead.vue
<script setup>
import { useStore } from 'vuex'
import {computed} from 'vue'
import { useRoute,useRouter } from 'vue-router'
// 当前路由对象
const route=useRoute()
const router=useRouter()
// 拿到store实例
const store=useStore()
// 通过computed拿到数据
const selectMenu=computed(()=>store.state.menu.selectMenu)
const userInfo = JSON.parse(localStorage.getItem('pz_userInfo'))
// 点击关闭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
})
}
}
const handleClick =(command)=>{
if(command === "cancel"){
localStorage.removeItem('pz_token')
localStorage.removeItem('pz_userInfo')
// 清除持久化信息v
localStorage.removeItem('pz_v3pz')
window.location.href=window.location.origin
}
}
</script>
<template>
<div class="header-container">
<div class="header-left flex-box">
<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="{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" @click="closeTab(item,index)"><Close/></el-icon>
</li>
</ul>
</div>
<div class="header-right">
<el-dropdown @command="handleClick">
<div class="el-dropdown-link flex-box" >
<el-avatar
:src="userInfo.avatar"
/>
<p class="user-name">{{ userInfo.name }}</p>
</div>
<template #dropdown>
<el-dropdown-menu >
<el-dropdown-item command="cancel">退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<style lang="less" scoped>
.flex-box{
display: flex;
align-items: center;
height: 100%;
}
.header-container{
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
background-color: #fff;
padding-right: 25px;
.header-left{
height: 100%;
.icon {
width: 45px;
height: 18px;
}
.con:hover {
background-color: #f5f5f5;
cursor: pointer;
}
.tab {
padding: 0 10px;
height: 100%;
.text {
margin: 0 5px;
}
.close{
visibility: hidden;
}
&.selected {
a{
color: #409eff;
}
i{
color: #409eff;
}
background-color: #f5f5f5;
}
}
.tab:hover{
background-color: #f5f5f5;
.close {
visibility: inherit;
cursor: pointer;
color: #000;
}
}
}
.header-right{
.user-name{
margin-left: 10px;
}
}
a {
height: 100%;
color: #333;
font-size: 15px;
}
}
</style>
#panelHead.vue
<script setup>
const props=defineProps({
route:{
type:Object
}
})
</script>
<template>
<div class="panel-heading">
<div class="panel-lead">
<div class="title">{{ props.route.meta.name }}</div>
<p class="description">{{ props.route.meta.describe }}</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>
#treeMenu.vue
<script setup>
import { useRouter } from 'vue-router'
import {useStore} from'vuex'
const props=defineProps(['menuData','index'])
const router=useRouter()
const store=useStore()
// console.log(props,'props');
// 点击侧边栏跳转
const handleClick=(item,active) =>{
// console.log(item);
store.commit('addMenu',item.meta)
store.commit('updateMenuActive',active)
router.push(item.meta.path)
}
</script>
<template>
<!-- <el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group title="Group One">
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<span>Navigator Three</span>
</el-menu-item> -->
<template v-for="(item,index) in props.menuData">
<!-- 没有子菜单的获取数组 -->
<el-menu-item
@click="handleClick(item,`${props.index}-${item.meta.id}`)"
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>
<!-- <el-menu-item index="4">
<el-icon><setting /></el-icon>
<span>Navigator Four</span>
</el-menu-item> -->
</template>
<style lang="less" scoped>
</style>
#router/index.vue
import Layout from '../views/Main.vue'
import Login from '../views/login/index.vue'
import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../views/dashboard/index.vue'
import Admin from '../views/auth/admin/index.vue'
import Group from '../views/auth/group/index.vue'
import Staff from '../views/vppz/staff/index.vue'
import Order from '../views/vppz/order/index.vue'
const localData = localStorage.getItem('pz_v3pz')
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Layout,
name: 'main',
redirect:to =>{
if(localData){
// 有子菜单情况
const child=JSON.parse(localData).menu.routerList[0].children
if(child){
return child[0].meta.path
}else{
return JSON.parse(localData).menu.routerList[0].meta.path
}
}else{
return '/'
}
},
children: [
// {
// path: 'dashboard',
// meta: { id: '1', name: '控制台', icon: 'Platform', path: '/dashboard', describe: '用于展示当前系统中的统计数据、统计报表及重要实时数据' },
// component: Dashboard
// },
// {
// path: 'auth',
// meta: { id: '2' ,name: '权限管理', icon: 'Grid' },
// children: [
// {
// path: '',
// alias: ['admin'],
// meta: { id: '1', name: '账号管理', icon: 'Avatar', path: '/auth/admin', describe: '管理员可以进行编辑,权限修改后需要登出才会生效' },
// component: Admin
// },
// {
// path: 'group',
// meta: { id: '2', name: '菜单管理', icon: 'Menu', path: '/auth/group', describe: '菜单规则通常对应一个控制器的方法,同时菜单栏数据也从规则中获取' },
// component: Group
// }
// ]
// },
// {
// path: 'vppz',
// meta: { id: '3', name: 'DIDI陪诊', icon: 'BellFilled' },
// children: [
// {
// path: '',
// alias: ['staff'],
// meta: { id: '1', name: '陪护管理', icon: 'Checked', path: '/vppz/staff', describe: '陪护师可以进行创建和修改,设置对应生效状态控制C端选择' },
// component: Staff
// },
// {
// path: 'order',
// meta: { id: '2', name: '订单管理', icon: 'List', path: '/vppz/order', describe: 'C端下单后可以查看所有订单状态,已支付的订单可以完成陪护状态修改' },
// component: Order
// }
// ]
// }
]
},
{
path: '/login',
component: Login
},
],
// strict: true, // applies to all routes
})
export default router
#store/index.js
import { createStore } from "vuex"
import menu from "./menu"
// 导入持久化配置
import createPersistedstate from 'vuex-persistedstate'
// 返回store实例,对外暴露
export default createStore({
plugins:[new createPersistedstate({
key:'pz_v3pz'
})],
modules:{
menu
}
})
#menu.js
// 先看看本地有没有数据,没有再用初始化数据
const localData = localStorage.getItem('pz_v3pz')
const state= localData ? localData.menu:{
isCollapse:false,
selectMenu:[],
routerList:[],
menuActive:'1-1'
}
const mutations={
collapsMenu(state){
state.isCollapse=!state.isCollapse
},
addMenu(state,payload){
// 对数据进行去重
if(state.selectMenu.findIndex(item=>item.path === payload.path) === -1){
state.selectMenu.push(payload)
}
},
closeMenu(state,payload){
// 找到点击数据的索引
const index = state.selectMenu.findIndex(val=>val.name === payload.name)
// 通过索引删除数组指定元素
state.selectMenu.splice(index,1)
},
// dynamicMenu(state,payload){
// console.log(payload,'payload')
// // 通过glob导入文件
// const modules = import.meta.glob('../views/**/**/*.vue')
// console.log(modules,'modules')
// function routerSet(router){
// router.forEach(route=>{
// // 判断没有子路由 拼接路由数据
// if(!route.children){
// const url=`../views${route.meta.path}/index.vue`
// // 拿到获取的vue组件
// route.component=modules[url]
// }else{
// routerSet(route.children)
// }
// })
// }
// routerSet(payload)
// // // 拿到完整的路由数据
// state.routerList=payload
// }
dynamicMenu(state, payload) {
const modules = import.meta.glob('../views/**/**/*.vue')
function routerSet(router) {
router.forEach(route => {
if (!route.children) {
const url = `../views${route.meta.path}/index.vue`
route.component = modules[url]
} else {
routerSet(route.children)
}
})
}
routerSet(payload)
state.routerList = payload
},
updateMenuActive(state, payload){
state.menuActive = payload
}
}
export default {state,mutations}
#utils/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)
}
if(response.data.code === -2){
localStorage.removeItem('pz_token')
localStorage.removeItem('pz_userInfo')
// 清除持久化信息v
localStorage.removeItem('pz_v3pz')
window.location.href=window.location.origin
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
export default instance
#admin/index.vue
<script setup>
import { authAdmin,menuSelectlist, updateUser }from '../../../api/index'
import dayjs from 'dayjs'
import { ref, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route=useRoute()
console.log(route,'route')
const paginationData = reactive({
pageNum: 1,
pageSize: 10
})
const handleSizeChange=(val)=>{
paginationData.pageSize=val
getListData()
}
const handleCurrentChange=(val)=>{
paginationData.pageNum=val
getListData()
}
// 编辑表单
const formRef=ref()
const form = reactive({
name:'',
permissions_id:''
})
// 表单提交
const confirm= async(formEI)=>{
if(!formEI) return
await formEI.validate((valid,fields)=>{
if(valid){
const { name, permissions_id} = form
updateUser({name,permissions_id}).then(({data})=>{
if(data.code === 10000){
dialogFormVisable.value=false
getListData()
}
})
}else{
console.log('error submit!',fields)
}
})
}
const options=ref([])
// 弹窗
const dialogFormVisable = ref(false)
// 关闭弹窗
const beforeClose =()=>{
dialogFormVisable.value=false
}
const tableData=reactive({
list:[],
total:0
})
onMounted(()=>{
getListData()
menuSelectlist().then(({data})=>{
options.value=data.data
})
})
// 请求列表的封装
const getListData = ()=>{
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
})
}
// 根据权限id匹配权限名称
const permissionName = (id) =>{
const data = options.value.find(el=>el.id === id)
return data ? data.name : '超级管理员'
}
const open = (rowData)=>{
dialogFormVisable.value=true
Object.assign(form,{mobile:rowData.mobile,name:rowData.name,permissions_id:rowData.permissions_id})
}
// 规则
const rules = reactive({
name:[{required: true,trigger: 'blur', message: '请填写昵称'}],
permissions_id:[{required: true,trigger: 'blur', message: '请选择菜单权限'}]
})
</script>
<template>
<panel-head :route="route"></panel-head>
<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>
<!-- 分页 -->
<div class="pagination-info">
<el-pagination
v-model:current-page="paginationData.pageNum"
:page-size="paginationData.pageSize"
size="small"
:background="false"
layout="total, prev, pager, next"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 弹窗 -->
<el-dialog
v-model="dialogFormVisable"
:before-close="beforeClose"
title="添加权限"
width="500">
<!-- 表单 -->
<el-form
ref="formRef"
label-width="100px"
label-position="left"
:model="form"
:rules="rules">
<el-form-item label="手机号" prop="mobile">
<el-input v-model="form.mobile" disabled/>
</el-form-item>
<el-form-item label="昵称" prop="name">
<el-input v-model="form.name"/>
</el-form-item>
<el-form-item label="菜单权限" prop="permissions_id">
<el-select
v-model="form.permissions_id"
placeholder="请选择菜单权限"
style="width: 240px">
<el-option
v-for="item in options"
:key="item.id"
:label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirm(formRef)">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="less" scoped>
.flex-box {
display: flex;
align-items: center;
}
</style>
#group/index.vue
<script setup>
import { nextTick, onMounted, reactive, ref } from 'vue'
import { userGetmenu,userSetmenu,menuList } from '../../../api/index.js'
import { Plus } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router'
const route=useRoute()
onMounted(()=>{
// 菜单数据
userGetmenu().then(({data})=>{
// console.log(data)
permissionData.value=data.data
})
getListData()
})
// 列表数据
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)
}
})
}
const paginationData = reactive({
pageNum: 1,
pageSize: 10
})
const handleSizeChange=(val)=>{
paginationData.pageSize=val
getListData()
}
const handleCurrentChange=(val)=>{
paginationData.pageNum=val
getListData()
}
// 请求列表数据
const getListData =() =>{
menuList(paginationData).then(({data})=>{
// console.log(data,'data')
const{list,total}=data.data
tabelData.list=list
tabelData.total=total
})
}
const formRef=ref()
// form表单数据
const form = reactive({
id:'',
name:'',
permissions:''
})
// 树型菜单权限数据
const permissionData = ref([])
// 弹窗的显示与隐藏
const dialogFormVisable= ref(false)
// 关闭弹窗的回调
const beforeClose=()=>{
dialogFormVisable.value=false
// 表单重置
formRef.value.resetFields()
// tree选中重置
treeRef.value.setCheckedKeys(defaultKeys)
}
const treeRef=ref()
const rules= reactive({
name:[{required:true, trigger:'blur',message:'请输入权限名称'}]
})
// 选中权限
const defaultKeys=[4,5]
// 表单提交
const confirm = async(formEI) =>{
if(!formEI) return
await formEI.validate((valid,fields)=>{
if(valid){
// 获取到选择的checkbox数据
const permissions=JSON.stringify(treeRef.value.getCheckedKeys())
userSetmenu({name:form.name,permissions,id:form.id}).then(({data})=>{
console.log(data)
// 新增
beforeClose()
// 编辑
getListData()
})
}else{
console.log('error submit!',fields)
}
})
}
</script>
<template>
<panel-head :route="route"/>
<div class="btns">
<el-button :icon="Plus" type="primary" @click="open(null)" size="small">新增</el-button>
</div>
<!-- 表单数据 -->
<el-table :data="tabelData.list" style="width: 100%;">
<el-table-column prop="id" label="id"/>
<el-table-column prop="name" label="昵称"/>
<el-table-column prop="permissionName" label="菜单权限" width="500px"/>
<el-table-column label="操作">
<template #default="scope">
<el-button type="primary" @click="open(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-info">
<el-pagination
v-model:current-page="paginationData.pageNum"
:page-size="paginationData.pageSize"
size="small"
:background="false"
layout="total, prev, pager, next"
:total="tabelData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 弹窗 -->
<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 v-show="false" prop="id">
<el-input v-model="form.id"></el-input>
</el-form-item>
<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-checked-keys="defaultKeys"
:default-expanded-keys="[2]"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirm(formRef)">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="less" scoped>
.btns{
padding: 10px 0 10px 10px;
background-color: #fff;
}
</style>
#login/index.vue
<script setup>
// import { UserFilled } from '@element-plus/icons-vue/dist/types';
import { ElMessage } from 'element-plus';
import { computed, reactive, ref, toRaw } from 'vue'
import { getCode, userAuthentication, login,menuPermissions } from '../../api/index'
import { UserFilled, Lock} from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
const imgUrl = new URL('../../../public/login-head.png', import.meta.url).href
const formType=ref(0) //切换表单(0:登录,1:注册)
const store = useStore()
// 拿到的路由数据
const routerList = computed(()=>store.state.menu.routerList)
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'
})
}
// 倒计时
const timer=setInterval(()=>{
if(countdown.time<=0){
countdown.time = 60
countdown.validText='获取验证码'
flag=false
clearInterval(timer)
}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('发送成功')
}
})
}
// 账号校验
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 router=useRouter()
const loginFormRef=ref()
// const submitForm =async(formEl)=>{
// if (!formEl) return
// // 手动触发校验
// await formEl.validate((valid,fields) => {
// if (valid) {
// // console.log('submit!')
// // 注册页面
// if(formType.value){
// userAuthentication(loginForm).then(({data})=>{
// if(data.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转成字符串
// // 拿到菜单权限
// menuPermissions().then(({data})=>{
// store.commit('dynamicMenu',data.data)
// console.log(routerList,'routerList')
// // toRaw将响应式数据===》普通路由数据
// toRaw(routerList.value).forEach(item =>{
// router.addRoute('main'.item)
// })
// router.push('/')
// })
// // router.push('/')
// }
// })
// }
// } else {
// console.log('error submit!',fields)
// }
// })
// }
const submitForm = async (formEl) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
if (formType.value) {
userAuthentication(loginForm).then(({ data }) => {
if (data.code === 10000) {
ElMessage.success('注册成功,请登录')
formType.value = 0
}
})
} else {
login(loginForm).then(({ data }) => {
if (data.code === 10000) {
ElMessage.success('登录成功')
localStorage.setItem('pz_token', data.data.token)
localStorage.setItem('pz_userInfo', JSON.stringify(data.data.userInfo))
menuPermissions().then(({ data }) => {
store.commit('dynamicMenu', data.data)
toRaw(routerList.value).forEach(item => {
router.addRoute('main', item)
})
router.push('/')
})
}
})
}
} else {
console.log('error submit!', fields)
}
})
}
</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
ref="loginFormRef"
: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(loginFormRef)">
{{ 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>
#staff.vue
<script setup>
import { nextTick, onMounted, reactive, ref } from 'vue'
import { Plus, InfoFilled, Delete } from '@element-plus/icons-vue'
import { photoList, companion, companionList, deleteCompanion } from '../../../api/index'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
const route = useRoute()
onMounted(()=>{
photoList().then(({data})=>{
fileList.value=data.data
})
getListData()
})
const paginationData = reactive({
pageNum: 1,
pageSize: 10
})
// 列表数据
const tableData = reactive({
list:[],
total:0
})
const getListData=()=>{
companionList(paginationData).then(({data})=>{
const {list, total}=data.data
tableData.list=list
tableData.total=total
})
}
const dialogTableVisible = ref(false)
const beforeClose = ()=>{
dialogTableVisible.value=false
formRef.value.resetFields()
}
const formRef = ref()
const form = reactive({
id:'',
mobile:'',
active: 1,
age:28,
avatar:'',
name:'',
sex:''
})
const rules=reactive({
name:[{required: true,trigger: 'blur', message: '请填写昵称'}],
avatar:[{required: true,message: '请选择头像'}],
sex:[{required: true,trigger: 'change', message: '请选择性别'}],
mobile:[{required: true,trigger: 'blur', message: '请填写手机号'}]
})
// 表单提交
const confirm = async(formEI) =>{
if(!formEI) return
await formEI.validate((valid,fields)=>{
if(valid){
companion(form).then(({data})=>{
if(data.code === 10000){
ElMessage.success('成功')
beforeClose()
getListData()
}else{
ElMessage.error(data.message)
}
})
}else{
console.log('error submit!',fields)
}
})
}
const open=(rowData={})=>{
dialogTableVisible.value=true
// 异步操作
nextTick(()=>{
// 如果是编辑
if(rowData){
Object.assign(form,rowData)
}
})
}
// 选择图片弹窗
const dialogImageVisible = ref(false)
const fileList = ref([])
const selectIndex = ref(0)
// 确认图片上传
const confirmImage=()=>{
form.avatar=fileList.value[selectIndex.value].url
dialogImageVisible.value=false
}
const handleSizeChange=(val)=>{
paginationData.pageSize=val
getListData()
}
const handleCurrentChange=(val)=>{
paginationData.pageNum=val
getListData()
}
// 删除功能
const selectTableData = ref([])
// 批量勾选
const handleSelectionChange=(val)=>{
selectTableData.value =val.map(item=>({id:item.id}))
}
const confirmEvent = ()=>{
// 没有选择数据
if(!selectTableData.value.length){
return ElMessage.warning('请选择至少一项数据')
}
deleteCompanion({id: selectTableData.value}).then(({data})=>{
if(data.code === 10000){
getListData()
}
})
}
</script>
<template>
<panel-head :route="route"/>
<div class="btns">
<el-button :icon="Plus" type="primary" @click="open(null)" size="small">新增</el-button>
<el-popconfirm
confirm-button-text="是"
cancel-button-text="否"
:icon="InfoFilled"
icon-color="#626AEF"
title="是否确认删除?"
@confirm="confirmEvent"
>
<template #reference>
<el-button :icon="Delete" type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</div>
<!-- 回显数据表格 -->
<el-table :data="tableData.list" style="width: 100%;" @selection-change="handleSelectionChange">
<!-- 批量勾选 -->
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="id"/>
<el-table-column prop="name" label="昵称"/>
<el-table-column label="头像">
<template #default="scope">
<el-image
style="width: 50px; height: 50px;"
:src="scope.row.avatar"
/>
</template>
</el-table-column>
<el-table-column label="性别" prop="sex">
<template #default="scope">
{{ scope.row.sex=== '1' ? '男':'女' }}
</template>
</el-table-column>
<el-table-column prop="mobile" label="手机号"/>
<el-table-column label="状态" prop="active">
<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>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-tag :type="primary" @click="open(scope.row)">编辑</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-info">
<el-pagination
v-model:current-page="paginationData.pageNum"
:page-size="paginationData.pageSize"
size="small"
:background="false"
layout="total, prev, pager, next"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<el-dialog
v-model="dialogTableVisible"
:before-close="beforeClose"
title="陪护师添加"
width="500"
>
<!-- 表单 -->
<el-form
ref="formRef"
label-width="100px"
label-position="left"
:model="form"
:rules="rules"
>
<el-form-item v-show="false" prop="id">
<el-input v-model="form.id"/>
</el-form-item>
<el-form-item label="昵称" prop="name">
<el-input v-model="form.name" placeholder="请输入昵称"/>
</el-form-item>
<el-form-item label="头像" prop="avatar">
<el-button v-if="!form.avatar" type="primary" @click="dialogImageVisible=true">点击上传</el-button>
<el-image
v-else
:src="form.avatar"
style="width: 100px;height: 100px"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="form.age" :min="18" :max="50" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="form.mobile" placeholder="请输入手机号"/>
</el-form-item>
<el-form-item label="是否生效" prop="active">
<el-radio-group v-model="form.active">
<el-radio :value="0">失效</el-radio>
<el-radio :value="1">生效</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirm(formRef)">确认</el-button>
</div>
</template>
</el-dialog>
<!-- 图片上传 -->
<el-dialog
v-model="dialogImageVisible"
:before-close="beforeClose"
title="选择图片"
width="680"
>
<div class="image-list">
<div v-for="(item,index) in fileList" :key="item.name" class="img-box" @click="selectIndex = index">
<div v-if="selectIndex === index" class="select">
<el-icon color="#fff"><Check /></el-icon>
</div>
<el-image
style="width: 148px; height: 148px"
:src="item.url"
/>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogImageVisible">取消</el-button>
<el-button type="primary" @click="confirmImage">确认</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="less" scoped>
.btns {
padding: 10px 0 10px 10px;
background-color: #fff;
}
.image-list {
display: flex;
align-items: center;
flex-wrap: wrap;
.img-box {
position: relative;
.select {
position: absolute;
left: 0px;
top: 0px;
width: 24px;
height: 24px;
background-color: #67c23a;
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
}
}
.el-image {
margin-right: 10px;
margin-bottom: 10px;
}
}
</style>
#Main.vue
<script setup>
import Aside from '../components/aside.vue'
import Header from '../components/navHeader.vue'
</script>
<template>
<div class="common-layout">
<el-container>
<!-- <el-aside width="200px">Aside</el-aside> -->
<Aside></Aside>
<el-container>
<!-- 右侧 -->
<el-header>
<!-- 组件化 -->
<Header></Header>
</el-header>
<el-main>
<router-view/>
</el-main>
</el-container>
</el-container>
</div>
</template>
<style lang="less" scoped>
.common-layout {
height: 100%;
.el-container{
height: 100%;
}
}
</style>
#main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 引入router
import router from './router'
// 下载图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 挂载vuex实例
import store from './store'
// 引入头部组件
import PanelHead from './components/panelHead.vue'
// 刷新后的动态路由添加
const localData=localStorage.getItem('pz_v3pz')
if(localData){
store.commit('dynamicMenu',JSON.parse(localData).menu.routerList)
store.state.menu.routerList.forEach(item => {
router.addRoute('main',item)
})
}
// 拦截器
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
}
})
// 路由挂载
// createApp(App).mount('#app')
const app=createApp(App)
app.use(router)
app.mount('#app')
app.use(store)
app.component('PanelHead',PanelHead)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// main.ts
// 如果您正在使用CDN引入,请删除下面一行。