就诊医疗系统

把数据渲染到侧边栏

先通过外层拿到唯一值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')
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值