学习进度条

今日所花时间:一小时
今日代码量:100行
博客量:一篇
了解到的知识点:
前几天发布的博客,将团队项目的后端代码基本完成,接下来要进行前端页面的开发
首先是登录页面

Vue3 + Element Plus 实现运维管理系统前端架构

项目背景

我深入学习了 Vue3 的组合式 API、Pinia 状态管理、Element Plus UI 组件库以及前端路由控制等技术。

核心代码实现

1. 登录页面 Login.vue

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { userLoginService } from '@/api/user.js'
import { useTokenStore } from '@/stores/token.js'
import { useRouter } from 'vue-router'

// 数据模型
const loginData = ref({
  username: '',
  password: ''
})

// 表单校验规则
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 5, max: 16, message: '长度在 5 到 16 个非空字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, max: 16, message: '长度在 5 到 16 个字符', trigger: 'blur' }
  ]
}

const tokenStore = useTokenStore();
const router = useRouter();
const form = ref(null)
const loading = ref(false)

// 登录函数
const login = async () => {
  try {
    loading.value = true
    await form.value.validate()

    const response = await userLoginService({
      username: loginData.value.username,
      password: loginData.value.password
    })

    console.log('登录响应:', response)

    // 检查响应状态
    if (response && response.code === 1) {
      // 存储token
      if (response.data) {
        tokenStore.setToken(response.data)
        // 保存用户信息到 localStorage
        localStorage.setItem('username', loginData.value.username)
        if (response.data.userId) {
          localStorage.setItem('userId', response.data.userId)
        }
        ElMessage.success('登录成功')
        router.push('/')
      } else {
        throw new Error('登录响应中没有token数据')
      }
    } else {
      throw new Error(response?.message || '登录失败')
    }
  } catch (error) {
    console.error('登录错误:', error)
    // 显示具体的错误信息
    if (error.response?.data?.message) {
      ElMessage.error(error.response.data.message)
    } else if (error.message) {
      ElMessage.error(error.message)
    } else {
      ElMessage.error('登录失败,请重试')
    }
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <el-row class="login-page">
    <el-col :span="12" class="bg"></el-col>
    <el-col :span="6" :offset="3" class="form">
      <el-form ref="form" size="large" autocomplete="off" :model="loginData" :rules="rules">
        <el-form-item>
          <h1>登录</h1>
        </el-form-item>
        <el-form-item prop="username">
          <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="loginData.username" clearable></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="loginData.password"
            show-password></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 @click="login" :loading="loading">
            登录
          </el-button>
        </el-form-item>
      </el-form>
    </el-col>
  </el-row>
</template>

<style lang="scss" scoped>
.login-page {
  height: 100vh;
  background-color: #fff;

  .bg::after {
    content: "运维管理子系统";
    /* 添加文字内容 */
    position: absolute;
    top: 50%;
    left: 30%;
    transform: translate(-50%, -50%);
    color: #333;
    font-size: 24px;
    font-weight: bold;
    text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
  }

  .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>

技术要点

  • 使用 Vue3 的 <script setup> 语法糖简化代码
  • 采用 Element Plus 的表单组件实现登录界面
  • 实现表单验证功能,包括必填项检查和长度限制
  • 使用 Pinia 管理登录状态和 token
  • 处理登录过程中的加载状态和错误提示
  • 响应式设计,适配不同屏幕尺寸

2. 路由配置 index.js

import { createRouter, createWebHistory } from 'vue-router'

import LoginVue from '@/views/Login.vue'
import LayoutVue from '@/views/Layout.vue'

import { useTokenStore } from '@/stores/token'
import ManagerDeviceManageVue from '@/views/manager/ManagerDeviceManage.vue'
import EngineerRepairOrderVue from '@/views/engineer/EngineerRepairOrder.vue'
import EngineerCheckVue from '@/views/engineer/EngineerCheck.vue'
import MaintenancePlanVue from '@/views/engineer/MaintenancePlan.vue'
import InspectionPlanVue from '@/views/engineer/InspectionPlan.vue'
import UserAvatarVue from '@/views/center/UserAvatar.vue'
import UserInfoVue from '@/views/center/UserInfo.vue'
import UserResetPasswordVue from '@/views/center/UserResetPassword.vue'
import InspectionOrderVue from '@/views/engineer/InspectionOrder.vue'
import MaintenanceOrderVue from '@/views/engineer/MaintenanceOrder.vue'
import CheckOrderVue from '@/views/engineer/CheckOrder.vue'

const routes = [
  {
    path: '/login',
    name: 'login', // 添加路由名称
    component: LoginVue
  },
  {
    path: '/',
    component: LayoutVue,
    redirect: '/managerDeviceManage',
    children: [
      {
        path: '/managerDeviceManage',
        name: 'managerDeviceManage',
        component: ManagerDeviceManageVue
      },
      {
        path: '/engineerRepairOrder',
        name: 'engineerRepairOrder',
        component: EngineerRepairOrderVue
      },
      {
        path: '/EngineerCheck',
        name: 'EngineerCheck',
        component: EngineerCheckVue
      },
      {
        path: '/MaintenancePlan',
        name: 'MaintenancePlan',
        component: MaintenancePlanVue
      },
      {
        path: '/InspectionPlan',
        name: 'InspectionPlan',
        component: InspectionPlanVue
      },
      {
        path: '/inspectionOrder',
        name: 'inspectionOrder',
        component: InspectionOrderVue
      },
      {
        path: '/maintenanceOrder',
        name: 'maintenanceOrder',
        component: MaintenanceOrderVue
      },
      {
        path: '/checkOrder',
        name: 'checkOrder',
        component: CheckOrderVue
      },
      {
        path: '/user/avatar',
        name: 'UserAvatar',
        component: UserAvatarVue
      },
      {
        path: '/user/info',
        name: 'UserInfo',
        component: UserInfoVue
      },
      {
        path: '/user/resetPassword',
        name: 'UserResetPassword',
        component: UserResetPasswordVue
      },
      
    ]
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes: routes
})

export default router

技术要点

  • 使用 Vue Router 4 的路由配置
  • 实现嵌套路由,Layout 作为主框架包含子路由
  • 采用路由懒加载提高性能
  • 使用命名路由方便编程式导航
  • 基于 HTML5 History 模式的路由

3. API 请求模块 user.js

import request from '@/utils/request'
import { useTokenStore } from '@/stores/token.js'
import useUserInfoStore from '@/stores/userInfo'

// 用户登录
export const userLoginService = (loginData) => {
    return request({
        url: '/login',
        method: 'post',
        data: loginData
    })
}

// 获取用户信息
export const userInfoService = () => {
    return request({
        url: '/userInfo',
        method: 'get'
    })
}

// 重置密码
export const userPasswordUpdateService = (data) => {
    // 直接使用前端表单数据,因为字段名已经匹配后端要求
    return request({
        url: '/updatePwd',
        method: 'post',
        data: {
            old_pwd: data.old_pwd,
            new_pwd: data.new_pwd,
            re_pwd: data.re_pwd
        }
    })
}

技术要点

  • 封装 API 请求模块,统一管理接口
  • 使用 async/await 处理异步请求
  • 接口参数规范化
  • 与后端 RESTful API 对接

4. 主布局组件 Layout.vue

<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" text-color="#fff" router>
                <el-menu-item index="/managerDeviceManage">
                    <el-icon>
                        <Management />
                    </el-icon>
                    <span>设备管理</span>
                </el-menu-item>

                <el-menu-item index="/engineerRepairOrder">
                    <el-icon>
                        <Tools />
                    </el-icon>
                    <span>维修管理</span>
                </el-menu-item>

                <!-- 计划管理子菜单 -->
                <el-sub-menu>
                    <template #title>
                        <el-icon>
                            <Calendar />
                        </el-icon>
                        <span>计划管理</span>
                    </template>
                    <el-menu-item index="/EngineerCheck">
                        <el-icon>
                            <Promotion />
                        </el-icon>
                        <span>检测计划管理</span>
                    </el-menu-item>
                    <el-menu-item index="/MaintenancePlan">
                        <el-icon>
                            <Calendar />
                        </el-icon>
                        <span>保养计划管理</span>
                    </el-menu-item>
                    <el-menu-item index="/InspectionPlan">
                        <el-icon>
                            <List />
                        </el-icon>
                        <span>巡检计划管理</span>
                    </el-menu-item>
                </el-sub-menu>

                <!-- 工单管理子菜单 -->
                <el-sub-menu>
                    <template #title>
                        <el-icon>
                            <Document />
                        </el-icon>
                        <span>工单管理</span>
                    </template>
                    <el-menu-item index="/inspectionOrder">
                        <el-icon>
                            <Document />
                        </el-icon>
                        <span>巡检工单管理</span>
                    </el-menu-item>
                    <el-menu-item index="/maintenanceOrder">
                        <el-icon>
                            <Document />
                        </el-icon>
                        <span>保养工单管理</span>
                    </el-menu-item>
                    <el-menu-item index="/checkOrder">
                        <el-icon>
                            <Document />
                        </el-icon>
                        <span>检测工单管理</span>
                    </el-menu-item>
                </el-sub-menu>

                <!-- 子菜单 -->
                <el-sub-menu>
                    <template #title>
                        <el-icon>
                            <UserFilled />
                        </el-icon>
                        <span>个人中心</span>
                    </template>
                    <el-menu-item index="/user/resetPassword">
                        <el-icon>
                            <EditPen />
                        </el-icon>
                        <span>重置密码</span>
                    </el-menu-item>
                </el-sub-menu>
            </el-menu>
        </el-aside>
        <!-- 右侧主区域 -->
        <el-container>
            <!-- 头部区域 -->
            <el-header>
                <div>运维管理:<strong>{{ userInfoStore.info?.username || '未登录' }}</strong></div>
                <!-- 下拉菜单 -->
                <el-dropdown placement="bottom-end" @command="handleCommand" v-if="userInfoStore.info">
                    <span class="el-dropdown__box">
                        <!-- avatar头像 -->
                        <el-avatar :src="userInfoStore.info?.userPic || avatar" />
                        <el-icon>
                            <CaretBottom />
                        </el-icon>
                    </span>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item command="resetPassword" :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>运维管理 ©2023 Created by 石家庄铁道大学</el-footer>
        </el-container>
    </el-container>
</template>

<script setup>
import {
    Management,
    Promotion,
    Tools,
    UserFilled,
    User,
    Crop,
    EditPen,
    SwitchButton,
    CaretBottom,
    Calendar,
    List,
    Setting,
    Document
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'

import { userInfoService } from '@/api/user.js'
import useUserInfoStore from '@/stores/userInfo.js'
import { useTokenStore } from '@/stores/token.js'
import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'

const tokenStore = useTokenStore();
const userInfoStore = useUserInfoStore();
const router = useRouter();

// 调用函数获取用户信息
const getUserInfo = async () => {
    try {
        // 调用接口
        let result = await userInfoService();
        if (result && result.data) {
            // 数据存储到pinia中
            userInfoStore.setInfo(result.data);
        } else {
            console.warn('获取用户信息失败:返回数据为空');
            // 如果获取用户信息失败,跳转到登录页
            router.push('/login');
        }
    } catch (error) {
        console.error('获取用户信息失败:', error);
        // 如果获取用户信息失败,跳转到登录页
        router.push('/login');
    }
}

// 检查是否有token,有则获取用户信息
if (tokenStore.token) {
    getUserInfo();
} else {
    // 如果没有token,直接跳转到登录页
    router.push('/login');
}

// 条目被点击后,调用的函数
const handleCommand = (command) => {
    // 判断指令
    if (command === 'logout') {
        // 退出登录,清除token,跳转到登录页面
        ElMessageBox.confirm(
            '你确认要退出吗?',
            '温馨提示',
            {
                confirmButtonText: '确认',
                cancelButtonText: '取消',
                type: 'warning',
            }
        )
            .then(async () => {
                // 退出登录
                // 1.清空pinia中存储的token以及个人信息
                tokenStore.removeToken()
                userInfoStore.removeInfo();
                // 2.跳转到登录页面
                router.push('/login')
                ElMessage({
                    type: 'success',
                    message: '退出登录成功',
                })
            })
            .catch(() => {
                ElMessage({
                    type: 'info',
                    message: '用户取消退出登录',
                })
            })
    } else {
        // 路由
        router.push('/user/' + command)
    }
}
</script>

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

    .el-aside {
        background-color: #232323;
        position: relative;
        min-height: auto;
        padding-bottom: 10px; // 添加底部内边距控制黑色区域

        &__logo {
            height: 100px;
            margin: 0 auto;
            padding: 5px 0;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #fff; // 白色文字
            font-size: 18px; // 字体大小
            font-weight: bold; // 加粗
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); // 文字阴影增加可读性
            letter-spacing: 2px; // 字间距
            border-radius: 4px; // 圆角
            margin: 10px; // 边距
        }

        .el-menu {
            border-right: none;
            margin-top: -30px; // 上移菜单减少间距
            padding: 5px 0; // 调整菜单内边距
        }
    }

    .el-header {
        background-color: #fff;
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0 20px;
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);

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

            .el-avatar {
                width: 40px;
                height: 40px;
                border: 2px solid #fff;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }

            .el-icon {
                color: #999;
                margin-left: 10px;
                transition: transform 0.3s;
            }

            &:hover .el-icon {
                transform: rotate(180deg);
            }

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

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

技术要点

  • 使用 Element Plus 的布局组件构建整体框架
  • 实现动态导航菜单,支持多级菜单
  • 用户头像和下拉菜单交互
  • 路由视图渲染
  • 用户登录状态管理和权限控制
  • 响应式设计和样式优化

学到的关键技术

1. Vue3 组合式 API

  • 使用 <script setup> 语法简化组件代码
  • 响应式数据管理 (ref, reactive)
  • 生命周期钩子的使用
  • 组合式函数的封装和复用

2. Pinia 状态管理

  • 集中管理应用状态 (token, 用户信息)
  • 状态持久化 (localStorage)
  • 模块化的 store 设计
  • 与 Vue 组件的高效集成

3. Element Plus UI 组件库

  • 表单组件和验证规则
  • 消息提示和弹窗组件
  • 布局系统和菜单组件
  • 图标系统的使用
  • 主题定制和样式覆盖

4. Vue Router 4

  • 动态路由配置
  • 导航守卫实现权限控制
  • 嵌套路由布局
  • 编程式导航
  • 路由懒加载优化性能

5. 前端工程化

  • API 请求的封装和错误处理
  • 组件化开发思想
  • SCSS 预处理器的使用
  • 响应式设计原则
  • 代码组织和模块划分

开发心得

通过这个项目的开发,我深入理解了 Vue3 的现代前端开发模式,掌握了组合式 API 的使用技巧,学会了如何构建一个完整的前端架构。特别是 Pinia 状态管理的使用,让我对前端状态管理有了更清晰的认识。
在开发过程中,我遇到了路由权限控制、表单验证、API 错误处理等挑战,通过查阅文档和实践,最终都得到了解决。Element Plus 组件库的使用大大提高了开发效率,同时也让我认识到 UI 组件库标准化的重要性。
这个项目让我对前端工程化有了更深的理解,特别是在项目结构组织、状态管理和组件设计方面积累了宝贵经验。未来我还计划加入更多的功能,如动态路由权限、主题切换等,进一步完善这个系统。

posted @ 2025-05-25 21:01  haoyinuo  阅读(13)  评论(0)    收藏  举报