vue3项目之大事件管理系统(二) 一级路由:登录页的实现和首页的实现,el-dropdown下拉菜单中如何绑定点击事件

一.登录页的静态布局和切换

1.静态布局

先下载icon组件:pnpm i @element-plus/icons-vue
粘贴静态页面代码并按需引入:import { User, Lock } from '@element-plus/icons-vue'

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
const isRegister = ref(true)//改成false就变成登录页了
</script>

<template>
  <el-row class="login-page">
<!-- grid布局:一行分成24,左边的图片独占12,登录页占6份并居中 -->
<!-- el-row+el-col -->
    <!--:图片占12,通过背景图片插入bg图和logo图 -->
    <el-col :span="12" class="bg">
    </el-col>
    <!--:登录页占6份并居中,offset是间隙 -->
    <el-col :span="6" :offset="3" class="form">
      <!-- 登录和注册的互相切换:isRegister -->
        <!-- ***这里是注册页部分*** -->
      <el-form ref="form" size="large" autocomplete="off" v-if="isRegister">
        <!-- el-form-item:表单的一行(一个表单域==互不影响的独立盒子) -->
        <el-form-item>
          <h1>注册</h1>
        </el-form-item>
        <el-form-item>
            <!-- input:表单元素--配置图标prefix-icon -->
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入再次密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button class="button" type="primary" auto-insert-space>
            注册
          </el-button>
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = false">
            ← 返回
          </el-link>
        </el-form-item>
      </el-form>


        <!-- ***这里是登录页部分*** -->
      <el-form ref="form" size="large" autocomplete="off" v-else>
        <el-form-item>
          <h1>登录</h1>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            name="password"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></el-input>
        </el-form-item>
        <el-form-item class="flex">
          <div class="flex">
            <el-checkbox>记住我</el-checkbox>
            <el-link type="primary" :underline="false">忘记密码?</el-link>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button class="button" type="primary" auto-insert-space
            >登录</el-button
          >
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = true">
            注册 →
          </el-link>
        </el-form-item>
      </el-form>
    </el-col>
  </el-row>
</template>

<style lang="scss" scoped>
.login-page {
  height: 100vh;
  background-color: #fff;
  .bg {
    background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
      url('@/assets/logo_bg.jpg') no-repeat center / cover;
    border-radius: 0 20px 20px 0;
  }
  .form {
    display: flex;
    flex-direction: column;
    justify-content: center;
    user-select: none;
    .title {
      margin: 0 auto;
    }
    .button {
      width: 100%;
    }
    .flex {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }
  }
}
</style>
2.登录和注册的切换
const isRegister = ref(true)//改成false就变成登录页了
<!-- ***这里是注册页部分*** -->
<el-form ref="form" size="large" autocomplete="off" v-if="isRegister"></el-form>
<!-- ***这里是登录页部分*** -->
<el-form ref="form" size="large" autocomplete="off" v-else></el-form>

二.注册功能的实现:校验+注册

1.添加校验

常见的校验规则:非空,长度,正则,自定义

1.1.el-form语法介绍
  • el-form:用于构建表单的容器组件,支持对整个表单进行校验和布局

常用属性:

        :model:绑定表单的数据对象
        :rules:定义表单的校验规则
        label-width:设置所有表单项的标签宽度

常用方法:

		validate(callback):对整个表单进行校验
  • el-form-item:是el-form的子组件,包裹具体的表单项,可以单独定义校验规则,支持嵌套在el-colel-row

常见属性:

		prop:指定表单项对应的数据字段名(需要和:model中绑定的对象保持一致)
  • el-row:栅格系统组件,定义一行布局
  • el-col:略
1.2.添加校验的步骤:以username为例
// step1:准备formModel对象--代表整个用于提交的form数据对象
const formModel = ref({//提供三个和注册相关的字段(查看文档>注册>请求参数)
  username: '',
  password: '',
  repassword: ''
})
// step2:再准备校验规则rules对象--可以对formModel的每个属性设置校验,校验规则写成数组,随时添加
const rules = {
  username: [
    { required: true, message: "请输入用户名", trigger: 'blur' },//失焦时校验也可改成change
    { min: 5, max: 10, message: "用户名长度在5到10个字符", trigger: 'blur' }//长度校验
  ]
}

......
<!-- ***这里是注册页部分*** -->
<!-- step3:在el-form中绑定formModel和rules -->
<el-form :model="formModel" :rules="rules" ref="form" size="large" autocomplete="off" v-if="isRegister">
  <!-- el-form-item:表单的一行(一个表单域==互不影响的独立盒子) -->
  <el-form-item>
    <h1>注册</h1>
  </el-form-item>
  <!-- step4:配置prop来绑定具体的校验规则,以示生效的是哪条规则 -->
  <el-form-item prop="username">
    <!-- input:表单元素--配置图标prefix-icon -->
    <!-- step5:在与username相关的el-input中v-model绑定formModel的子属性username -->
    <el-input v-model='formModel.username' :prefix-icon="User" placeholder="请输入用户名"></el-input>
  </el-form-item>
  <el-form-item>
    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码"></el-input>
  </el-form-item>
  <el-form-item>
    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码"></el-input>
  </el-form-item>
  <el-form-item>
    <el-button class="button" type="primary" auto-insert-space>
      注册
    </el-button>
  </el-form-item>
  <el-form-item class="flex">
    <el-link type="info" :underline="false" @click="isRegister = false">
      ← 返回
    </el-link>
  </el-form-item>
</el-form>
1.3.对password(非空+正则)和repassword(非空+正则+自定义)添加校验
password: [
  { required: true, message: "请输入密码", trigger: 'blur' },//非空
  { pattern: /^.{6,15}$/, message: "密码必须是6-15位的非空字符", trigger: 'blur' }//正则
],
repassword: [
  { required: true, message: "请再次输入密码", trigger: 'blur' },
  { pattern: /^.{6,15}$/, message: "密码必须是6-15位的非空字符", trigger: 'blur' },//正则
  {
    validator: (rule, value, callback) => {//自定义校验规则
      if (value !== formModel.value.password) {
        callback(new Error('两次输入密码不一致!'))
      } else {
        callback()
      }//回调函数,校验通过就什么都不做(callback())
    }, trigger: 'blur'
  }
]
<!-- 密码 -->
<el-form-item prop="password">
  <el-input v-model="formModel.password" :prefix-icon="Lock" type="password" placeholder="请输入密码"></el-input>
</el-form-item>
<!-- 确认密码 -->
<el-form-item prop="repassword">
  <el-input v-model="formModel.repassword" :prefix-icon="Lock" type="password" placeholder="请输入再次密码"></el-input>
</el-form-item>

踩坑:遇到了正则校验不通过的问题–语法错误(引号包裹问题)
当正则表达式被写成字符串形式 '/^.{6,15}$/' 时,实际会被解析为字符串而非正则对象。
在 JavaScript 中,引号包裹的正则表达式会被当作普通字符串,导致匹配逻辑完全失效

2.注册预校验

之前的校验是每个单独的el-form-item的校验,
但是我们要求,点击注册按钮的时候也要对上面用户输入的内容进行再次校验
只有通过了注册之前的预校验,才会发起注册请求

拿到当前的form组件的实例

知识点:模板引用–ref标识的两个作用:ref写在标签上,拿到DOM对象;ref写在子组件占位符上,拿到子组件实例对象

const form = ref(null)//表单ref 用于表单校验

<el-form ref='form'></el-form>
给注册按钮绑定点击事件
<el-button @click='register' class="button" type="primary" auto-insert-space>
  注册
</el-button>

const register = async () => {
  // 1.校验表单
  await form.value.validate()//校验通过后才执行下一步,校验不通过就阻止提交
  // 2.发送请求
  console.log('注册成功,下一步发送请求...')
}
3.发起注册请求
封装api

在这里插入图片描述

//新建api/user.js

import request from '@/utils/request'
// 注册接口
export const userRegisterService = ({ username, password, repassword }) => {
  return request.post('/api/reg', { username, password, repassword })
}

页面调用
import { userRegisterService } from '@/api/user'
import { ElMessage } from 'element-plus'
const register = async () => {
  // 1.校验表单
  await form.value.validate()//校验通过后才执行下一步,校验不通过就阻止提交
  // 2.发送请求
  await userRegisterService(formModel.value)//发送请求:调用注册接口并传入formModel.value作为参数
  ElMessage.success('注册成功')
  isRegister.value = false//注册成功后自动切换到登录页
}
优化:在eslint.config.js中声明全局变量名,就不用对常用的element-plus组件按需导入了
//eslint.config.js

  globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }

三.登录功能的实现:校验+登录+提交+pinia中存token

1.登录校验
直接复用注册校验的规则
 <!-- ***这里是登录页部分*** -->
<el-form :model="formModel" :rules="rules" ref="form" size="large" autocomplete="off" v-else>
		......
        <el-form-item prop="username">
          <el-input v-model="formModel.username" :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input v-model="formModel.password" name="password" :prefix-icon="Lock" type="password"
            placeholder="请输入密码"></el-input>
        </el-form-item>
        ......
</el-form>
优化:在切换注册/登录的过程中,用户输入的信息不应该保留

思路:在切换的时候(isRegister值更新),重置输入框

import { watch } from 'vue'
// 监听isRegister,切换时要重置表单
watch(() => isRegister.value, () => {
  formModel.value = {}//重置表单数据对象
})
2.登录前预校验
拿到当前的form组件的实例
const form = ref(null)//已存在
//下面是登录的el-form
<el-form ref='form'></el-form>
给登录按钮绑定点击事件
// 登录点击事件login
const login = async () => {
  // 1.校验表单
  await form.value.validate()
  alert('登录成功')
  // 2.发送请求
  console.log("封装登录接口...");
}
3.提交登录
封装api接口

在这里插入图片描述

//api/user.js

// 登录接口
export const userLoginService = ({ username, password }) => {
  return request.post('/api/login', { username, password })
}
页面调用
const login = async () => {
  // 1.校验表单
  await form.value.validate()
  // 2.发送请求
  const res = await userLoginService(formModel.value)
  console.log(res.data.token);//查看是否成功传token
  ElMessage.success('登录成功')
}
4.存token

存token的方法已存在,直接调用stores/modules/user.js中的setToken()方法即可

import { userRegisterService, userLoginService } from '@/api/user'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'
const router = useRouter()
const userStore = useUserStore()
const login = async () => {
  // 1.校验表单:登录前预校验
  await form.value.validate()
  // 2.发送请求
  const res = await userLoginService(formModel.value)
  console.log(res.data.token);//查看是否成功传token
  // 3.存储token:存到pinia并自动本地化
  userStore.setToken(res.data.token)
  // 4.跳转到首页(路由)
  router.push('/')
  ElMessage.success('登录成功')
}

四.首页的静态布局

1.了解el-menu并搭起首页架子
1. el-container--架子组件列表:看成更语义化的div
el-container 是一个基础的布局容器,用于将页面划分为不同的区域。它可以嵌套使用,并且必须包含子组件如
el-header、el-aside、el-footer 和 el-main 中的一个或多个
 
2.el-menu --整个菜单组件
el-menu 是一个导航菜单组件,支持水平和垂直两种模式。
通过设置 mode 属性为 horizontal 或 vertical 来指定菜单的方向。default-active 属性用于指定默认激活的菜单项
        active-text-color:激活时文字颜色
        :default-active:'$route.path'(重要)
                配置默认的菜单项(可以理解为router-link的to)
                    router选项开启,el-menu-item的index就是点击跳转的路径
2.静态页面布局
<script setup>
import {
  Management,
  Promotion,
  UserFilled,
  User,
  Crop,
  EditPen,
  SwitchButton,
  CaretBottom
} from '@element-plus/icons-vue'
</script>


<template>
  <el-container class="layout-container">
    <el-aside width="200px">
      <div class="el-aside__logo"></div>
      <el-menu active-text-color="#ffd04b" background-color="#232323" :default-active="$route.path" text-color="#fff"
        router>
        <el-menu-item index="/article/channel">
          <el-icon>
            <Management />
          </el-icon>
          <span>文章分类</span>
        </el-menu-item>
        <el-menu-item index="/article/manage">
          <el-icon>
            <Promotion />
          </el-icon>
          <span>文章管理</span>
        </el-menu-item>
        <el-sub-menu index="/user">
          <!--.....多级菜单的标签,本质上是一个具名插槽title -->
          <!-- 配置el-sub-menu的大标题 -->
          <template #title>
            <el-icon>
              <UserFilled />
            </el-icon>
            <span>个人中心</span>
          </template>
          <!-- ....展开的内容,本质上是一个默认插槽.... -->
          <el-menu-item index="/user/profile">
            <el-icon>
              <User />
            </el-icon>
            <span>基本资料</span>
          </el-menu-item>
          <el-menu-item index="/user/avatar">
            <el-icon>
              <Crop />
            </el-icon>
            <span>更换头像</span>
          </el-menu-item>
          <el-menu-item index="/user/password">
            <el-icon>
              <EditPen />
            </el-icon>
            <span>重置密码</span>
          </el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-aside>
    <el-container>
      <el-header>
        <div>前端开发:<strong>张三</strong></div>
        <el-dropdown placement="bottom-end">
          <span class="el-dropdown__box">
            <el-avatar
              src="https://img2.baidu.com/it/u=2527990145,2068681281&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500" />
            <el-icon>
              <CaretBottom />
            </el-icon>
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item>
              <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
              <el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
              <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-header>
      <el-main>
        <router-view></router-view>
      </el-main>
      <el-footer>© 2025 @前端OnTheRun. 版权所有</el-footer>
    </el-container>
  </el-container>
</template>


<style lang="scss" scoped>
.layout-container {
  height: 100vh;

  .el-aside {
    background-color: #232323;

    &__logo {
      height: 120px;
      background: url('https://img2.baidu.com/it/u=2527990145,2068681281&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500') no-repeat center / 120px auto;
    }

    .el-menu {
      border-right: none;
    }
  }

  .el-header {
    background-color: #fff;
    display: flex;
    align-items: center;
    justify-content: space-between;

    .el-dropdown__box {
      display: flex;
      align-items: center;

      .el-icon {
        color: #999;
        margin-left: 10px;
      }


      &:active,
      &:focus {
        outline: none;
      }
    }
  }

  .el-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #666;
  }
}
</style>

五.首页的登录访问拦截

当前没有登录,没有任何token,但也能访问到首页数据,这不合理,应该做一个拦截跳转到登录页

1.语法介绍
//router/index.js

//官网位置:VueRouter4>导航守卫>全局前置守卫
//添加访问拦截:
router.beforeEach((to,from)=>{})

放行还是拦截,是根据返回值来决定的,返回值分为几种情况:

  • undefined或true:直接放行
  • false:返回from的地址页面(打哪来回哪去)
  • 具体路径'/login’或路径对象{name:'login'}:拦截到对应地址
2.具体实现
//router/index.js
// 添加访问拦截
router.beforeEach((to) => {
  const userStore = useUserStore()
  // 如果没有token且访问非登录页,就拦截并跳转登录页
  if (!userStore.token && to.path !== '/login') {
    return '/login'
  }
})

六.用户基本信息的获取和渲染

最终效果如下:
在这里插入图片描述

封装api接口

在这里插入图片描述

// 获取用户基本信息
export const userGetInfoService = () => {
  return request.get('/my/userinfo')
}
存仓库
//stores/modules/user.js

export const useUserStore = defineStore('big-user', () => {
	......
  // 获取用户数据的方法
  const user = ref({})//把用户信息存入user对象中
  const getUser = async () => {
    const res = await userGetInfoService()//请求获取用户信息
    // console.log("获取到了什么:", res.data.data);
    user.value = res.data.data
  }
  ......
}
页面调用
//LayoutContainer.vue
import {useUserStore} from "@/stores"
import { onMounted } from 'vue'

const userStore=useUserStore()
//在钩子中调用
onMounted(()=>{
    userStore.getUser()
})
动态渲染
<div>前端开发:<strong>{{ userStore.user.username || userStore.user.nickname }}</strong></div>
<el-dropdown placement="bottom-end">
<span class="el-dropdown__box">
  <el-avatar
    :src="userStore.user.user_pic || 'https://img2.baidu.com/it/u=2527990145,2068681281&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500'" />
  <el-icon>
    <CaretBottom />
  </el-icon>
</span>

七.退出功能的实现

在这里插入图片描述

了解el-dropdown下拉菜单组件

el-dropdown 是 Element-UI 提供的一个下拉菜单组件,用于实现点击或悬停触发的下拉菜单交互

子组件
  • el-dropdown-menu:用于包裹下拉菜单的内容。
  • el-dropdown-item:表示下拉菜单中的单个选项。
如何在el-dropdown中绑定点击事件?

el-dropdown 提供了 @command 事件,当用户点击某个 el-dropdown-item 时会触发该事件,并将 command 属性值传递给事件处理函数
使用示例:

    handleCommand(command) {
      console.log('选择了:', command); // 输出:选项一 或 选项二
    }
    
  <el-dropdown @command="handleCommand">
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item command="option1">选项一</el-dropdown-item>
      <el-dropdown-item command="option2">选项二</el-dropdown-item>
    </el-dropdown-menu>
 </el-dropdown>
在首页中使用下拉菜单组件
        <el-dropdown placement="bottom-end">
          <span class="el-dropdown__box">
            <el-avatar
              :src="userStore.user.user_pic || 'https://img2.baidu.com/it/u=2527990145,2068681281&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500'" />
            <el-icon>
              <CaretBottom />
            </el-icon>
          </span>
          <!-- 这是折叠的下拉部分 -->
          <template #dropdown>
            <!-- el-dropdown-menu用于包裹下拉菜单的内容 -->
            <el-dropdown-menu>
              <!-- el-dropdown-item是折叠部分的选项 -->
              <el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item>
              <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
              <el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
              <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
实现退出登录
  • 提供退出登录后清空用户数据的方法
//stores/modules/user.js
  // 退出登录,清空用户数据
  const setUser = (obj) => {
    user.value = obj
  }
  return {
	setUser
  }
  • 使用在@command实现在el-dropdown中绑定点击事件
<el-dropdown placement="bottom-end" @command='handleCommand'>
	......
</el-dropdown>
  • 在点击事件中实现退出登录的相关操作
import { useUserStore } from '@/stores'
const userStore = useUserStore()
import { useRouter } from 'vue-router'
const router = useRouter()
const handleCommand = (command) => {
  if (command === 'logout') {//用户点击的是设置了command='logout'的那个item标签
    userStore.removeToken()//清空token
    userStore.setUser({})//清空用户信息
    router.push('/login')//跳转登录页
  } else {//用户点击的是文章分类,文章管理,更换头像
    router.push(`/user/${command}`)//跳转到该二级路由
  }
}
优化:用ElMessageBox优化退出确认框
const handleCommand = (command) => {
  if (command === 'logout') {//用户点击的是设置了command='logout'的那个item标签
    // 弹出确认框
    ElMessageBox.confirm('确认要退出登录吗?', "温馨提示", {
      type: 'warning',
      comfirmButtonText: '确认',
      cancalBUttonText: '取消'
    })
    userStore.removeToken()//清空token
    userStore.setUser({})//清空用户信息
    router.push('/login')//跳转登录页
  } else {//用户点击的是文章分类,文章管理,更换头像
    router.push(`/user/${command}`)//跳转到该二级路由
  }
}
vue3的前端页面<template> <!-- 权限检查:只有 SYSADMIN 能看到页面内容 --> <div v-if="form.usepermission === &#39;SYSADMIN&#39;"> <div style="width: 100%; overflow-x: auto; padding: 16px; box-sizing: border-box;"> <!-- 查询区域:添加Flex布局实现左右分布 --> <div class="search-area"> <el-form :inline="true" :model="queryParams" class="demo-form-inline"> <!-- 左侧区域:关键词、查询、重置 --> <div class="form-left"> <el-form-item label="关键词"> <el-input v-model="queryParams.keyword" placeholder="课程关键词/工号/姓名"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery" icon="el-icon-search">查询</el-button> </el-form-item> <el-form-item> <el-button @click="handleReset">重置</el-button> </el-form-item> </div> <!-- 右侧区域:审核按钮、下载按钮 --> <div class="form-right"> <el-form-item> <el-button type="primary" @click="handleParticipate"> {{ isAuditMode ? &#39;取消审核&#39; : &#39;参训审核&#39; }} </el-button> </el-form-item> <el-form-item> <el-button @click="handleDownload">DOWNLOAD <i class="el-icon-download"></i></el-button> </el-form-item> </div> </el-form> </div> <!-- 当前筛选条件显示 --> <div v-if="hasActiveFilters" class="active-filters"> <span class="filters-label">当前筛选:</span> <el-tag v-for="(value, column) in filters" :key="column" closable type="primary" @close="clearFilter(column)" class="filter-tag" > {{ getColumnLabel(column) }}: {{ value }} </el-tag> <el-button type="text" @click="clearAllFilters" class="clear-all">清除所有</el-button> </div> <!-- 表格区域 --> <div class="table-area"> <el-table ref="multipleTableRef" :data="filteredTableData" border style="width: 100%" @selection-change="handleSelectionChange" > <el-table-column type="selection" width="35"></el-table-column> <!-- 序号列 --> <el-table-column prop="id" label="序号" width="55"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.id }"> <span>序号</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.id }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;id&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.id }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="id in getUniqueValues(&#39;id&#39;)" :key="id" :command="{ column: &#39;id&#39;, value: id }" :class="{ &#39;dropdown-item-active&#39;: filters.id === id }" > {{ id }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 区分列 --> <el-table-column prop="category" label="区分" width="100"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.category }"> <span>区分</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.category }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;category&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.category }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="category in getUniqueValues(&#39;category&#39;)" :key="category" :command="{ column: &#39;category&#39;, value: category }" :class="{ &#39;dropdown-item-active&#39;: filters.category === category }" > {{ category }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 等级列 --> <el-table-column prop="level" label="等级" width="80"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.level }"> <span>等级</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.level }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;level&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.level }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="level in getUniqueValues(&#39;level&#39;)" :key="level" :command="{ column: &#39;level&#39;, value: level }" :class="{ &#39;dropdown-item-active&#39;: filters.level === level }" > {{ level }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 课程名称列 --> <el-table-column prop="coursename" label="课程名称" width="120"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.coursename }"> <span>课程名称</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.coursename }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;coursename&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.coursename }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="course in getUniqueValues(&#39;coursename&#39;)" :key="course" :command="{ column: &#39;coursename&#39;, value: course }" :class="{ &#39;dropdown-item-active&#39;: filters.coursename === course }" > {{ course }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 期数列 --> <el-table-column prop="period" label="期数" width="80"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.period }"> <span>期数</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.period }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;period&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.period }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="period in getUniqueValues(&#39;period&#39;)" :key="period" :command="{ column: &#39;period&#39;, value: period }" :class="{ &#39;dropdown-item-active&#39;: filters.period === period }" > {{ period }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 姓名列 --> <el-table-column prop="name" label="姓名" width="80"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.name }"> <span>姓名</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.name }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;name&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.name }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="name in getUniqueValues(&#39;name&#39;)" :key="name" :command="{ column: &#39;name&#39;, value: name }" :class="{ &#39;dropdown-item-active&#39;: filters.name === name }" > {{ name }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 工号列 --> <el-table-column prop="employeeid" label="工号" width="80"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.employeeid }"> <span>工号</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.employeeid }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;employeeid&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.employeeid }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="empId in getUniqueValues(&#39;employeeid&#39;)" :key="empId" :command="{ column: &#39;employeeid&#39;, value: empId }" :class="{ &#39;dropdown-item-active&#39;: filters.employeeid === empId }" > {{ empId }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- Team列 --> <el-table-column prop="team" label="Team" width="80"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.team }"> <span>Team</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.team }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;team&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.team }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="team in getUniqueValues(&#39;team&#39;)" :key="team" :command="{ column: &#39;team&#39;, value: team }" :class="{ &#39;dropdown-item-active&#39;: filters.team === team }" > {{ team }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- Group列 --> <el-table-column prop="grouptext" label="Group" width="100"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.grouptext }"> <span>Group</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.grouptext }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;grouptext&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.grouptext }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="group in getUniqueValues(&#39;grouptext&#39;)" :key="group" :command="{ column: &#39;grouptext&#39;, value: group }" :class="{ &#39;dropdown-item-active&#39;: filters.grouptext === group }" > {{ group }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- Part列 --> <el-table-column prop="department" label="Part" width="110"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.department }"> <span>Part</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.department }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;department&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.department }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="dept in getUniqueValues(&#39;department&#39;)" :key="dept" :command="{ column: &#39;department&#39;, value: dept }" :class="{ &#39;dropdown-item-active&#39;: filters.department === dept }" > {{ dept }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 联系方式列 --> <el-table-column prop="contact" label="联系方式" width="90"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.contact }"> <span>联系方式</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.contact }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;contact&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.contact }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="contact in getUniqueValues(&#39;contact&#39;)" :key="contact" :command="{ column: &#39;contact&#39;, value: contact }" :class="{ &#39;dropdown-item-active&#39;: filters.contact === contact }" > {{ contact }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> <!-- 参训现况列 --> <el-table-column prop="status" label="参训现况" width="120"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.status }"> <span>参训现况</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.status }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;status&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.status }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="status in getUniqueValues(&#39;status&#39;)" :key="status" :command="{ column: &#39;status&#39;, value: status }" :class="{ &#39;dropdown-item-active&#39;: filters.status === status }" > {{ status }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> <template #default="scope"> <el-select v-model="scope.row.status" placeholder="请选择" @change="handleStatusChange(scope.row)"> <el-option label="参加" value="参加"></el-option> <el-option label="未参加" value="未参加"></el-option> </el-select> </template> </el-table-column> <!-- 审核状态列 --> <el-table-column prop="auditstatus" label="审核状态" width="120"> <template #header> <div class="filter-header" :class="{ &#39;filter-active&#39;: filters.auditstatus }"> <span>审核状态</span> <el-dropdown trigger="click" @command="handleFilter"> <el-icon :class="{ &#39;filter-icon-active&#39;: filters.auditstatus }"><Filter /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :command="{ column: &#39;auditstatus&#39;, value: &#39;all&#39; }" :class="{ &#39;dropdown-item-active&#39;: !filters.auditstatus }" > 全部 </el-dropdown-item> <el-dropdown-item v-for="auditStatus in getUniqueValues(&#39;auditstatus&#39;)" :key="auditStatus" :command="{ column: &#39;auditstatus&#39;, value: auditStatus }" :class="{ &#39;dropdown-item-active&#39;: filters.auditstatus === auditStatus }" > {{ auditStatus }} </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </template> </el-table-column> </el-table> <!-- 审核操作按钮:仅isAuditMode为true时显示 --> <div v-if="isAuditMode" class="action-buttons" style="margin: 10px 0;"> <el-button type="primary" @click="handleApprove">审核通过</el-button> <el-button type="danger" @click="handleReject">审核不通过</el-button> </div> <div class="pagination"> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.pageNum" :page-sizes="[10, 20, 50]" :page-size="queryParams.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="filteredTotal"> </el-pagination> </div> </div> </div> </div> <!-- 无权限时的提示 --> <div v-else class="permission-denied"> <el-result icon="warning" title="权限不足" sub-title="您没有访问此页面的权限,请联系管理员。" > <template #extra> <el-button type="primary" @click="handleGoBack">返回</el-button> <el-button @click="handleContactAdmin">联系管理员</el-button> </template> </el-result> </div> </template> <script setup> import { ref, reactive, onMounted, computed } from &#39;vue&#39; import { useRouter } from &#39;vue-router&#39; import axios from &#39;axios&#39; import { ElMessage, ElMessageBox } from &#39;element-plus&#39; import { Filter } from &#39;@element-plus/icons-vue&#39; import { useUserStore } from &#39;@/stores/auth&#39; const router = useRouter() const userStore = useUserStore() // 用户权限信息 const form = ref({ usepermission: userStore.permission || &#39;&#39; }) console.log(&#39;我的权限:&#39;, form.value.usepermission) // 查询参数 const queryParams = reactive({ keyword: &#39;&#39;, pageNum: 1, pageSize: 10 }) // 表格数据 const tableData = ref([]) // 筛选条件 const filters = reactive({}) // 总条数 const total = ref(0) // 选中数据 const multipleSelection = ref([]) // 表格引用 const multipleTableRef = ref(null) // 核心状态变量:仅通过"参训审核/取消审核"按钮修改 const isAuditMode = ref(false) // 列名映射 const columnLabels = { id: &#39;序号&#39;, category: &#39;区分&#39;, level: &#39;等级&#39;, coursename: &#39;课程名称&#39;, period: &#39;期数&#39;, name: &#39;姓名&#39;, employeeid: &#39;工号&#39;, team: &#39;Team&#39;, grouptext: &#39;Group&#39;, department: &#39;Part&#39;, contact: &#39;联系方式&#39;, status: &#39;参训现况&#39;, auditstatus: &#39;审核状态&#39; } // 获取列标签 const getColumnLabel = (column) => { return columnLabels[column] || column } // 检查是否有活跃的筛选条件 const hasActiveFilters = computed(() => { return Object.keys(filters).length > 0 }) // 获取唯一值列表 const getUniqueValues = (column) => { const values = tableData.value.map(item => item[column]).filter(Boolean) return [...new Set(values)] } // 处理筛选 const handleFilter = (command) => { const { column, value } = command if (value === &#39;all&#39;) { // 删除该列的筛选条件 delete filters[column] } else { // 设置筛选条件 filters[column] = value } // 重置到第一页 queryParams.pageNum = 1 } // 清除单个筛选条件 const clearFilter = (column) => { delete filters[column] } // 清除所有筛选条件 const clearAllFilters = () => { Object.keys(filters).forEach(key => { delete filters[key] }) } // 筛选后的数据 const filteredTableData = computed(() => { let filtered = tableData.value // 应用所有筛选条件 Object.keys(filters).forEach(column => { filtered = filtered.filter(item => item[column] === filters[column]) }) return filtered }) // 筛选后的总条数 const filteredTotal = computed(() => filteredTableData.value.length) // 获取表格数据 const fetchData = async () => { // 如果没有权限,不执行数据获取 if (form.value.usepermission !== &#39;SYSADMIN&#39;) { return } try { const res = await axios.get(&#39;/api/training/list&#39;, { params: queryParams }) // 处理数据:接口返回的status为空时,默认设为"未参加" const formattedData = res.data.data.records.map(record => ({ ...record, status: record.status ?? &#39;未参加&#39; })) tableData.value = formattedData total.value = res.data.total // 重置筛选条件 Object.keys(filters).forEach(key => delete filters[key]) } catch (error) { console.error(error) ElMessage.error(&#39;获取数据失败&#39;) } } // 查询按钮点击事件 const handleQuery = () => { queryParams.pageNum = 1 fetchData() } // 重置按钮点击事件 const handleReset = () => { queryParams.keyword = &#39;&#39; queryParams.pageNum = 1 // 重置筛选条件 Object.keys(filters).forEach(key => delete filters[key]) fetchData() } // 多选框选中数据 const handleSelectionChange = (selection) => { multipleSelection.value = selection } // 分页大小变化 const handleSizeChange = (val) => { queryParams.pageSize = val fetchData() } // 当前页变化 const handleCurrentChange = (val) => { queryParams.pageNum = val fetchData() } // 参训审核/取消审核按钮点击事件 const handleParticipate = () => { isAuditMode.value = !isAuditMode.value } // 审核通过按钮点击事件 const handleApprove = () => { if (multipleSelection.value.length === 0) { ElMessage.warning(&#39;请先选择需要审核的记录&#39;) return } const hasUnparticipated = multipleSelection.value.some(row => row.status !== &#39;参加&#39;) if (hasUnparticipated) { ElMessage.warning(&#39;未参加培训,不能通过&#39;) return } batchUpdateStatus(&#39;通过&#39;) } // 审核不通过按钮点击事件 const handleReject = () => { if (multipleSelection.value.length === 0) { ElMessage.warning(&#39;请先选择需要审核的记录&#39;) return } batchUpdateStatus(&#39;不通过&#39;) } // 批量更新审核状态 const batchUpdateStatus = async (status) => { const selectedIds = multipleSelection.value.map(item => item.id) ElMessageBox.confirm(`确定要将选中记录审核为"${status}"吗?`, &#39;提示&#39;, { confirmButtonText: &#39;确定&#39;, cancelButtonText: &#39;取消&#39;, type: &#39;warning&#39; }).then(async () => { try { await axios.put(&#39;/api/training/batchStatus&#39;, { ids: selectedIds, status }) ElMessage.success(&#39;审核成功&#39;) fetchData() } catch (error) { console.error(error) ElMessage.error(&#39;审核失败&#39;) } }).catch(() => { // 用户取消操作 }) } // 审核状态变化(单行更新) const handleStatusChange = async (row) => { try { await axios.put(`/api/training/status/${row.id}`, { status: row.status }) ElMessage.success(&#39;更新成功&#39;) } catch (error) { console.error(error) ElMessage.error(&#39;更新失败&#39;) fetchData() } } // 下载按钮点击事件 const handleDownload = () => { window.open(&#39;/api/training/download&#39;, &#39;_blank&#39;) } // 返回上一页 const handleGoBack = () => { router.back() } // 联系管理员 const handleContactAdmin = () => { ElMessage.info(&#39;请联系系统管理员获取权限&#39;) } // 初始化数据 onMounted(() => { // 只有 SYSADMIN 权限才获取数据 if (form.value.usepermission === &#39;SYSADMIN&#39;) { fetchData() } else { console.log(&#39;当前用户无权限访问此页面,权限为:&#39;, form.value.usepermission) } }) </script> <style scoped> .search-area { margin-bottom: 10px; } .demo-form-inline { display: flex; justify-content: space-between; align-items: center; width: 100%; } .form-left .el-form-item { margin-right: 10px; } .form-right .el-form-item { margin-left: 10px; } .action-buttons { text-align: left; } .pagination { margin-top: 10px; text-align: right; } /* 当前筛选条件样式 */ .active-filters { margin-bottom: 15px; padding: 10px; background-color: #f8f9fa; border-radius: 4px; border-left: 4px solid #409EFF; } .filters-label { font-weight: bold; color: #606266; margin-right: 10px; } .filter-tag { margin-right: 8px; margin-bottom: 5px; } .clear-all { margin-left: 10px; color: #409EFF; } /* 筛选头部样式 */ .filter-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; padding: 8px 0; transition: all 0.3s ease; } .filter-header.filter-active { background-color: #ecf5ff; border-radius: 4px; padding: 8px 12px; margin: -8px -12px; } .filter-header .el-icon { color: #c0c4cc; font-size: 14px; transition: all 0.3s ease; } .filter-header:hover .el-icon { color: #409EFF; } .filter-icon-active { color: #409EFF !important; } /* 下拉菜单选中项样式 */ :deep(.dropdown-item-active) { background-color: #ecf5ff !important; color: #409EFF !important; } :deep(.dropdown-item-active:hover) { background-color: #d9ecff !important; } /* 无权限提示样式 */ .permission-denied { display: flex; justify-content: center; align-items: center; height: 60vh; padding: 20px; background-color: #f5f7fa; } .permission-denied .el-result { padding: 40px 30px; } </style>,请帮我生成一个完整的自适应浏览器的vue3 html
11-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端OnTheRun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值