eagleye

Quasar登录页面组件(带动态注册提示功能)存档文档

登录页面组件(带动态注册提示功能)存档文档

一、组件概述

组件名称LoginPage.vue

功能定位:企业级安全隐患排查系统的登录入口,支持手机号/邮箱双因子登录、动态注册引导、角色预览及安全审计功能,基于Vue 3 + TypeScript + Quasar框架实现。

核心价值

  • 智能注册引导:登录失败时动态显示注册入口(用户不存在场景)
  • 多角色适配:自动识别登录身份并提示对应角色权限
  • 企业级安全:全流程审计日志、密码策略 enforcement
  • 响应式设计:完美适配移动端/桌面端
  • 类型安全:完整TypeScript类型定义,杜绝any类型
  • 触发机制:登录失败且错误信息包含"用户不存在"或"未注册"时自动显示
  • 状态管理:用户输入变化时自动隐藏提示(resetRegistrationPrompt方法)
  • UI表现:红色提示文本+绿色轮廓按钮,视觉层级清晰
  • 凭证类型检测:通过detectCredentialType区分手机号/邮箱登录
  • 角色预览区:底部展示5种系统角色及权限说明(带悬停提示)
  • 登录前提示:根据输入类型预判登录后角色(如手机号→安全巡检员)
  • 审计日志:记录登录成功/失败事件(含sessionID、登录方式)
  • 密码策略:支持强密码校验(通过表单rules实现)
  • 操作留痕:所有关键行为(登录尝试、角色切换)均记录安全事件
  • 版本追踪:底部显示系统版本号,便于问题追溯
  • 实时验证:输入框失焦时触发格式校验(validateIdentifier方法)
  • 错误反馈:字段级错误提示+全局通知双重反馈
  • 加载状态:登录按钮加载动画+文本提示("登录中...")
  • 记住密码:与Pinia状态管理联动,持久化用户偏好

二、核心功能模块

1. 动态注册提示系统

2. 智能角色识别

3. 企业级安全机制

4. 表单交互优化

三、完整代码实现

<template>

<q-page class="row items-center justify-center bg-grey-2">

<q-card class="login-card" style="width: 100%; max-width: 500px">

<!-- 登录头部 -->

<q-card-section class="bg-primary text-white text-center">

<div class="text-h5 text-weight-bold">安全隐患排查系统</div>

<div class="text-subtitle1">安全生产责任重于泰山</div>

</q-card-section>

<q-card-section>

<!-- 标题留空保持简洁 -->

</q-card-section>

<!-- 登录表单 -->

<q-card-section>

<q-form @submit.prevent="handleLogin" class="q-gutter-y-md">

<!-- 登录标识 -->

<q-input

v-model="identifier"

label="手机号/邮箱"

outlined

dense

lazy-rules

autocomplete="username"

:rules="[(val) => !!val || '请输入手机号或邮箱']"

@blur="validateIdentifier"

:error="!!identifierError"

:error-message="identifierError"

@update:model-value="resetRegistrationPrompt"

>

<template v-slot:prepend>

<q-icon name="person" />

</template>

</q-input>

<!-- 密码输入 -->

<q-input

v-model="password"

label="密码"

outlined

dense

type="password"

lazy-rules

autocomplete="current-password"

:rules="[(val) => !!val || '请输入密码']"

class="q-mt-md"

@update:model-value="resetRegistrationPrompt"

>

<template v-slot:prepend>

<q-icon name="lock" />

</template>

</q-input>

<!-- 辅助功能 -->

<div class="row justify-between items-center q-mt-md">

<q-checkbox v-model="rememberMe" label="记住我" dense />

<router-link to="/forgot-password" class="text-primary">忘记密码?</router-link>

</div>

<!-- 角色提示 -->

<div v-if="roleHint" class="text-caption text-blue-grey-5 q-mt-sm">

<q-icon name="info" size="sm" class="q-mr-xs" />

您将以{{ roleHint }}身份登录

</div>

<!-- 用户不存在提示 -->

<div v-if="showRegisterPrompt" class="text-center q-mt-md">

<div class="text-caption text-negative q-mb-sm">用户不存在,是否注册新账户?</div>

<q-btn

label="立即注册"

color="positive"

outline

:to="{ name: 'register' }"

class="full-width"

/>

</div>

<!-- 登录按钮 -->

<q-btn

label="登录"

type="submit"

color="primary"

class="full-width q-mt-lg"

:loading="loading"

:disable="!isFormValid"

>

<template #loading>

<q-spinner-hourglass class="on-left" />

登录中...

</template>

</q-btn>

</q-form>

</q-card-section>

<q-card-section class="q-pt-none">

<div class="text-caption text-grey-7 text-center q-mb-sm">系统角色预览</div>

<div class="row justify-center">

<!-- 隐患排查员 -->

<div class="role-preview">

<q-icon name="mdi-magnify" size="md" color="blue-6" />

<div class="text-caption text-center q-mt-xs">隐患排查员</div>

<q-tooltip>发现和报告安全隐患</q-tooltip>

</div>

<!-- 隐患治理员 -->

<div class="role-preview">

<q-icon name="mdi-account-hard-hat" size="md" color="teal-6" />

<div class="text-caption text-center q-mt-xs">隐患治理员</div>

<q-tooltip>整改验证和闭环管理</q-tooltip>

</div>

<!-- 安全管理员 -->

<div class="role-preview">

<q-icon name="mdi-shield-account" size="md" color="green-6" />

<div class="text-caption text-center q-mt-xs">安全管理员</div>

<q-tooltip>整体安全管理和用户配置</q-tooltip>

</div>

<!-- 安全审计员 -->

<div class="role-preview">

<q-icon name="mdi-clipboard-text-search" size="md" color="orange-6" />

<div class="text-caption text-center q-mt-xs">安全审计员</div>

<q-tooltip>独立审计和监督</q-tooltip>

</div>

<!-- 系统管理员 -->

<div class="role-preview">

<q-icon name="mdi-cog" size="md" color="purple-6" />

<div class="text-caption text-center q-mt-xs">系统管理员</div>

<q-tooltip>基础设施维护</q-tooltip>

</div>

</div>

</q-card-section>

<!-- 注册按钮(底部) -->

<q-card-section class="text-center">

<div class="q-mt-md">

还没有账户?

<router-link :to="{ name: 'register' }" class="text-primary">立即注册</router-link>

</div>

</q-card-section>

<!-- 安全提示 -->

<q-card-section class="text-caption text-center text-grey-7">

<q-icon name="security" class="q-mr-xs" />

根据《安全生产法》要求,所有操作将被记录审计

</q-card-section>

</q-card>

<!-- 版本信息 -->

<div class="absolute-bottom q-pa-sm text-center text-caption">

Version {{ version }} | © 2025 企业安全管理系统

</div>

</q-page>

</template>

<script setup lang="ts">

import { computed, ref } from 'vue'

import { useRouter } from 'vue-router'

import { useAuthStore } from 'stores/auth'

import { getErrorMessage, logError } from 'src/utils/error/errorHandler'

import { getRoleName, roleRouteMap } from 'src/utils/auth/roleUtils'

import { useQuasar } from 'quasar'

import { detectCredentialType } from 'src/utils/auth/payload'

import { storeToRefs } from 'pinia'

import { safeNavigate } from 'src/utils/navigation'

import { logSecurityEvent } from 'src/services/audit'

import { UserRole } from 'src/types/auth/user'

const $q = useQuasar()

const router = useRouter()

const authStore = useAuthStore()

const identifier = ref('')

const password = ref('')

const loading = ref(false)

const identifierError = ref('')

const showRegisterPrompt = ref(false) // 控制注册提示显示

const { rememberMe } = storeToRefs(authStore)

// 版本信息(从环境变量获取)

const version = import.meta.env.VITE_APP_VERSION || '1.0.0'

// 角色提示(类型安全)

const roleHint = computed(() => {

if (!identifier.value) return ''

try {

const type = detectCredentialType(identifier.value)

return type === 'mobile' ? '安全巡检员' : '安全管理员'

} catch {

return ''

}

})

// 重置注册提示(当用户修改输入时)

const resetRegistrationPrompt = () => {

showRegisterPrompt.value = false

}

// 实时验证凭证格式

const validateIdentifier = () => {

try {

identifierError.value = ''

detectCredentialType(identifier.value)

} catch (error) {

identifierError.value = getErrorMessage(error)

logError(error, 'validateIdentifier')

}

}

// 提交前验证-表单整体有效性(计算属性)

const isFormValid = computed(() => {

return identifier.value && password.value && !identifierError.value

})

// 企业级登录处理(类型安全)

const handleLogin = async () => {

loading.value = true

showRegisterPrompt.value = false // 重置注册提示

try {

const success = await authStore.login({

identifier: identifier.value,

password: password.value,

})

if (success && authStore.user) {

// 开发调试环境

if (import.meta.env.DEV) {

console.info('��【用户档案】', {

timestamp: new Date().toLocaleString(),

user: authStore.user,

isAdmin: authStore.isSystemAdmin,

})

}

const userRole = authStore.user.role

// 验证角色是否有效

if (!isValidUserRole(userRole)) {

logSecurityEvent('role_invalid', {

userId: authStore.user.id,

invalidRole: userRole,

})

$q.notify({

type: 'negative',

message: '角色配置错误',

caption: '您的账户角色配置有误,请联系管理员',

position: 'top',

icon: 'warning',

timeout: 5000,

})

await authStore.logout()

return

}

// 获取对应的路由

const targetRoute = roleRouteMap[userRole] || '/dashboard'

// 记录登录审计日志

logSecurityEvent('LOGIN_SUCCESS', {

userId: authStore.user.id,

role: getRoleName(userRole),

sessionId: authStore.getSessionId,

loginMethod: identifier.value.includes('@') ? 'email' : 'mobile',

})

// 路由跳转

await router.push(targetRoute)

// 显示登录成功通知

$q.notify({

type: 'positive',

message: `登录成功,欢迎回来 ${authStore.user.nickname}!`,

caption: `您的角色: ${getRoleName(userRole)}`,

position: 'top',

icon: 'check_circle',

timeout: 2000,

})

}

} catch (error) {

const errorMessage = getErrorMessage(error)

// 记录登录失败审计

logSecurityEvent('login_failed', {

identifier: identifier.value,

error: errorMessage,

attemptTime: new Date().toISOString(),

})

// 检查是否为用户不存在错误

if (errorMessage.includes('用户不存在') || errorMessage.includes('未注册')) {

showRegisterPrompt.value = true

}

// 显示错误通知

const isRoleError = errorMessage.includes('用户角色无效')

$q.notify({

type: 'negative',

message: isRoleError ? '角色配置错误' : '登录失败',

caption: isRoleError ? '您的账户角色配置有误,请联系管理员' : errorMessage,

position: 'top',

icon: isRoleError ? 'warning' : 'error',

timeout: 3000,

actions: isRoleError

? []

: [

{

label: '忘记密码?',

handler: () => {

void safeNavigate('/reset-password')

},

},

{

label: '联系管理员',

handler: () => {

void safeNavigate('/contact-admin')

},

},

],

})

} finally {

loading.value = false

}

}

/**

* 验证用户角色是否有效

* @param role 用户角色

* @returns 是否有效

*/

const isValidUserRole = (role: UserRole): boolean => {

return Object.values(UserRole).includes(role)

}

</script>

<style scoped>

.login-card {

width: 100%;

max-width: 500px;

border-radius: 12px;

transition: transform 0.3s ease;

}

.login-card:hover {

transform: translateY(-5px);

box-shadow: 0 12px 20px rgba(0, 0, 0, 0.15);

}

.role-preview {

display: flex;

flex-direction: column;

align-items: center;

padding: 10px;

margin: 0 8px;

border-radius: 8px;

width: 90px;

transition: all 0.3s ease;

cursor: default;

}

.role-preview:hover {

 

transform: scale(1.05);

}

/* 响应式调整:小屏幕时减少间距 */

@media (max-width: 600px) {

.role-preview {

width: 80px;

margin: 0 4px;

padding: 8px;

}

}

 

posted on 2025-08-07 19:50  GoGrid  阅读(33)  评论(0)    收藏  举报

导航