yl-frontend/src/views/LoginView.vue

754 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="login-container">
<!-- 背景动画 -->
<div class="bg-animation">
<div v-for="n in 10" :key="n" class="circle-container">
<div class="circle"></div>
</div>
</div>
<div class="login-content">
<div class="login-header">
<div class="logo-wrapper">
<img src="@/assets/logo.svg" alt="Logo" class="logo">
</div>
<h2>智慧养老服务平台</h2>
<p class="subtitle">让生活更智慧,让关爱更便捷</p>
</div>
<el-card class="login-card">
<template #header>
<el-tabs v-model="activeTab" class="login-tabs">
<el-tab-pane label="账号密码登录" name="account">
<el-form
ref="accountFormRef"
:model="accountForm"
:rules="accountRules"
label-width="0"
>
<el-form-item prop="username">
<el-input
v-model="accountForm.username"
placeholder="请输入用户名/手机号/邮箱"
:prefix-icon="User"
class="custom-input"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="accountForm.password"
type="password"
placeholder="请输入密码"
:prefix-icon="Lock"
show-password
class="custom-input"
/>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="手机号登录" name="phone">
<el-form
ref="phoneFormRef"
:model="phoneForm"
:rules="phoneRules"
label-width="0"
>
<el-form-item prop="phone">
<div class="verify-input">
<el-input
v-model="phoneForm.phone"
placeholder="请输入手机号"
:prefix-icon="Phone"
class="custom-input"
/>
<el-button
type="primary"
:disabled="phoneCooldown > 0"
@click="sendPhoneCode"
>
{{ phoneCooldown > 0 ? `${phoneCooldown}s` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
<el-form-item prop="code">
<el-input
v-model="phoneForm.code"
placeholder="请输入验证码"
:prefix-icon="Key"
class="custom-input"
/>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="邮箱登录" name="email">
<el-form
ref="emailFormRef"
:model="emailForm"
:rules="emailRules"
label-width="0"
>
<el-form-item prop="email">
<div class="verify-input">
<el-input
v-model="emailForm.email"
placeholder="请输入邮箱"
:prefix-icon="Message"
class="custom-input"
/>
<el-button
type="primary"
:disabled="emailCooldown > 0"
@click="sendEmailCode"
>
{{ emailCooldown > 0 ? `${emailCooldown}s` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
<el-form-item prop="code">
<el-input
v-model="emailForm.code"
placeholder="请输入验证码"
:prefix-icon="Key"
class="custom-input"
/>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="刷脸登录" name="face">
<div class="face-login">
<div class="camera-container">
<video
ref="videoRef"
class="camera-view"
:class="{ 'scanning': isScanning }"
autoplay
muted
></video>
<div class="scan-overlay">
<div class="scan-line"></div>
</div>
<div class="face-guide">
<el-icon :size="60"><Avatar /></el-icon>
</div>
</div>
<div class="camera-controls">
<el-button
type="primary"
:loading="isScanning"
@click="startFaceLogin"
>
{{ isScanning ? '识别中...' : '开始识别' }}
</el-button>
</div>
<div class="face-tips">
<p>请确保光线充足正对摄像头</p>
<p>保持面部在框内眨眨眼</p>
</div>
</div>
</el-tab-pane>
</el-tabs>
</template>
<div class="remember-forgot">
<el-checkbox v-model="rememberMe">记住我</el-checkbox>
<el-link type="primary" :underline="false" @click="forgotPassword">忘记密码</el-link>
</div>
<el-button
type="primary"
class="login-button"
:loading="loading"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
<div class="register-link">
还没有账号
<router-link to="/register">立即注册</router-link>
</div>
</el-card>
<div class="footer">
<p>© 2024 智慧养老服务平台 All Rights Reserved</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Phone, Message, Key, Avatar } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import { userApi } from '@/api/user'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const rememberMe = ref(false)
const activeTab = ref('account')
const userStore = useUserStore()
// 账号密码登录表单
const accountFormRef = ref<FormInstance>()
const accountForm = reactive({
username: '',
password: ''
})
// 手机号登录表单
const phoneFormRef = ref<FormInstance>()
const phoneForm = reactive({
phone: '',
code: ''
})
// 邮箱登录表单
const emailFormRef = ref<FormInstance>()
const emailForm = reactive({
email: '',
code: ''
})
// 验证码倒计时
const phoneCooldown = ref(0)
const emailCooldown = ref(0)
// 表单验证规则
const accountRules = {
username: [
{ required: true, message: '请输入用户名/手机号/邮箱', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
const phoneRules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
}
const emailRules = {
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
}
// 发送验证码
const startCooldown = (type: 'phone' | 'email') => {
const cooldown = type === 'phone' ? phoneCooldown : emailCooldown
cooldown.value = 60
const timer = setInterval(() => {
cooldown.value--
if (cooldown.value <= 0) {
clearInterval(timer)
}
}, 1000)
}
const sendPhoneCode = async () => {
if (!phoneForm.phone) {
ElMessage.warning('请先输入手机号')
return
}
// TODO: 调用发送验证码API
ElMessage.success('验证码已发送')
startCooldown('phone')
}
const sendEmailCode = async () => {
if (!emailForm.email) {
ElMessage.warning('请先输入邮箱')
return
}
await userApi.sendVerifyEmailCode(emailForm.email)
ElMessage.success('验证码已发送')
startCooldown('email')
}
// 登录处理
const handleLogin = async () => {
let formRef: FormInstance | undefined
let formData: any
switch (activeTab.value) {
case 'account':
formRef = accountFormRef.value
formData = { ...accountForm, activeTab: activeTab.value }
break
case 'phone':
formRef = phoneFormRef.value
formData = { ...phoneForm, activeTab: activeTab.value }
break
case 'email':
formRef = emailFormRef.value
formData = { ...emailForm, activeTab: activeTab.value }
break
}
if (!formRef) return
await formRef.validate(async (valid) => {
if (valid) {
loading.value = true
try {
const token = await userApi.login(formData)
console.log(token)
userStore.setToken(token)
const res = await userApi.getCurrentUser()
console.log(res)
ElMessage.success('登录成功')
router.push('/')
} catch (error) {
console.error('登录失败:', error)
} finally {
loading.value = false
}
}
})
}
const forgotPassword = () => {
router.push('/forgot-password')
}
const videoRef = ref<HTMLVideoElement>()
const isScanning = ref(false)
let stream: MediaStream | null = null
// 初始化摄像头
const initCamera = async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user' }
})
if (videoRef.value) {
videoRef.value.srcObject = stream
}
} catch (error) {
ElMessage.error('无法访问摄像头,请检查权限设置')
}
}
// 关闭摄像头
const closeCamera = () => {
if (stream) {
stream.getTracks().forEach(track => track.stop())
stream = null
}
}
// 开始人脸识别
const startFaceLogin = async () => {
if (!stream) {
await initCamera()
return
}
isScanning.value = true
try {
// TODO: 调用人脸识别API
await new Promise(resolve => setTimeout(resolve, 2000))
// 模拟登录成功
ElMessage.success('人脸识别成功')
localStorage.setItem('token', 'dummy-token')
router.push(route.query.redirect as string || '/')
} catch (error) {
ElMessage.error('人脸识别失败,请重试')
} finally {
isScanning.value = false
}
}
// 监听标签页切换
watch(activeTab, (newTab) => {
if (newTab === 'face') {
initCamera()
} else {
closeCamera()
}
})
// 组件卸载时关闭摄像头
onUnmounted(() => {
closeCamera()
})
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
position: relative;
overflow: hidden;
}
/* 背景动画 */
.bg-animation {
position: absolute;
width: 100%;
height: 100%;
}
.circle-container {
position: absolute;
transform: translateY(10%);
}
.circle {
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
position: absolute;
animation: float 8s infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0) scale(1);
opacity: 0.5;
}
50% {
transform: translateY(-20px) scale(1.1);
opacity: 0.3;
}
}
.login-content {
width: 100%;
max-width: 440px;
padding: 20px;
position: relative;
z-index: 1;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.logo-wrapper {
width: 90px;
height: 90px;
margin: 0 auto 20px;
padding: 12px;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
.logo {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.1));
}
.login-header h2 {
color: white;
font-size: 28px;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
color: rgba(255, 255, 255, 0.9);
margin: 10px 0 0;
font-size: 16px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.login-card {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
}
.card-header {
text-align: center;
font-size: 20px;
font-weight: 600;
color: #1890ff;
padding: 10px 0;
}
.custom-input :deep(.el-input__wrapper) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border-radius: 8px;
padding: 8px 15px;
}
.custom-input :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.1);
}
.login-button {
width: 100%;
margin-top: 24px;
height: 44px;
font-size: 16px;
border-radius: 8px;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
border: none;
transition: all 0.3s;
}
.login-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
.remember-forgot {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 2px;
color: #666;
}
.register-link {
margin-top: 20px;
text-align: center;
font-size: 14px;
color: #666;
}
.register-btn {
color: #1890ff;
text-decoration: none;
margin-left: 4px;
font-weight: 500;
}
.register-btn:hover {
text-decoration: underline;
}
.footer {
text-align: center;
margin-top: 30px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
/* 生成10个不同位置和大小的圆 */
.circle-container:nth-child(1) { left: 10%; animation-delay: 0s; }
.circle-container:nth-child(2) { left: 20%; animation-delay: 2s; }
.circle-container:nth-child(3) { left: 30%; animation-delay: 4s; }
.circle-container:nth-child(4) { left: 40%; animation-delay: 6s; }
.circle-container:nth-child(5) { left: 50%; animation-delay: 8s; }
.circle-container:nth-child(6) { left: 60%; animation-delay: 10s; }
.circle-container:nth-child(7) { left: 70%; animation-delay: 12s; }
.circle-container:nth-child(8) { left: 80%; animation-delay: 14s; }
.circle-container:nth-child(9) { left: 90%; animation-delay: 16s; }
.circle-container:nth-child(10) { left: 95%; animation-delay: 18s; }
.circle-container:nth-child(odd) .circle {
width: 150px;
height: 150px;
}
/* 响应式适配 */
@media (max-width: 480px) {
.login-content {
padding: 15px;
}
.login-header h2 {
font-size: 24px;
}
.subtitle {
font-size: 14px;
}
.logo-wrapper {
width: 80px;
height: 80px;
}
}
/* 添加新样式 */
.login-tabs {
margin-bottom: -20px;
}
.login-tabs :deep(.el-tabs__header) {
margin-bottom: 25px;
}
.verify-input {
display: flex;
gap: 10px;
}
.verify-input .el-input {
flex: 1;
}
.verify-input .el-button {
width: 120px;
height: 50px;
}
/* 优化输入框样式 */
.custom-input {
height: 50px;
}
.custom-input :deep(.el-input__wrapper) {
background: white;
border: 1px solid #e4e7ed;
box-shadow: none !important;
transition: all 0.3s;
}
.custom-input :deep(.el-input__wrapper:hover) {
border-color: #c0c4cc;
}
.custom-input :deep(.el-input__wrapper.is-focus) {
border-color: #409EFF;
}
/* 优化按钮样式 */
.el-button {
border-radius: 8px;
font-weight: 500;
transition: all 0.3s;
}
.el-button:not(:disabled):hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
/* 人脸识别相关样式 */
.face-login {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.camera-container {
width: 320px;
height: 240px;
position: relative;
border-radius: 12px;
overflow: hidden;
background: #000;
}
.camera-view {
width: 100%;
height: 100%;
object-fit: cover;
}
.camera-view.scanning {
animation: scanning 2s ease-in-out infinite;
}
.scan-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px solid #409EFF;
border-radius: 12px;
}
.scan-line {
position: absolute;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #409EFF, transparent);
animation: scan 2s linear infinite;
}
.face-guide {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(255, 255, 255, 0.3);
}
.camera-controls {
width: 100%;
display: flex;
justify-content: center;
}
.face-tips {
text-align: center;
color: #909399;
font-size: 14px;
}
.face-tips p {
margin: 5px 0;
}
@keyframes scan {
0% {
top: 0;
}
100% {
top: 100%;
}
}
@keyframes scanning {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
</style>