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;
}
}
浙公网安备 33010602011771号